# How to Integrate Replit with Worldpay

- Tool: Replit
- Difficulty: Advanced
- Time required: 2-3 hours
- Last updated: March 2026

## TL;DR

To integrate Replit with Worldpay, obtain your Worldpay Access API credentials from the Worldpay merchant portal, store them in Replit Secrets, and build a server-side Node.js or Python backend to handle payment authorization, 3DS2 authentication, and settlement flows. Worldpay's enterprise-grade global processing requires careful server-side implementation. Deploy on Replit Reserved VM for always-on payment infrastructure.

## Why Integrate Worldpay with Replit?

Worldpay processes over 40 billion transactions annually and is the preferred payment processor for many large enterprises, particularly in the UK, Europe, and global multi-currency environments. For merchants already using Worldpay as their acquirer, integrating Worldpay into a custom application built on Replit enables direct control over the payment flow — custom checkout experiences, server-to-server payment processing for recurring billing, and integration of Worldpay's advanced fraud prevention and 3D Secure 2 authentication.

The Worldpay Access API is a modern REST API that covers the complete payment lifecycle from authorization through settlement. Unlike consumer payment tools, Worldpay is designed for enterprises processing high volumes of transactions across multiple currencies and payment methods. The API supports Visa, Mastercard, American Express, and many regional payment methods. 3DS2 is mandatory for card-not-present transactions under PSD2 in Europe, and the Worldpay Access API provides the full 3DS2 authentication flow including the Challenge and Frictionless paths.

Building on Replit gives you a cloud server environment to implement the Worldpay backend without managing your own infrastructure. However, because you're handling payment flows, PCI DSS compliance is critical — never let raw card numbers touch your Replit server. Always use Worldpay's hosted payment page or the Access Checkout JavaScript SDK to collect card details client-side, passing only payment session tokens to your server. For production workloads, deploy on Replit Reserved VM to ensure payment endpoints are always available without cold start delays.

## Before you start

- A Worldpay merchant account with Access API credentials (username and password from your Worldpay merchant portal or onboarding team)
- Your Worldpay merchant entity reference and the API environment URLs (sandbox and production) from your Worldpay documentation pack
- A Replit account with a Node.js or Python Repl created
- Node.js with axios and express, or Python with requests and flask — install via Shell
- Understanding of PCI DSS compliance requirements: card data must never be processed or stored on your server; use Worldpay's hosted fields or Access Checkout SDK for card collection

## Step-by-step guide

### 1. Obtain Worldpay Access API Credentials and Store in Replit Secrets

The Worldpay Access API uses HTTP Basic Authentication — every request includes a Base64-encoded username:password pair in the Authorization header. Your API credentials (a service username and password) are provided by your Worldpay onboarding team or are available in the Worldpay Business Gateway under your account settings. There are two sets of credentials: sandbox credentials for testing (pointing to Worldpay's test environment) and production credentials for live processing. Open your Replit project and click the lock icon (🔒) in the left sidebar to open Secrets. Add the following secrets: WORLDPAY_SERVICE_KEY (your API username), WORLDPAY_CLIENT_KEY (your API password), WORLDPAY_MERCHANT_CODE (your merchant entity reference), and WORLDPAY_ENV with the value 'sandbox' or 'production'. You'll also need the entity reference for your merchant profile, which is included in Worldpay's API request URLs. Never hardcode these values in your source code — Replit's Secret Scanner monitors your files and will flag exposed credentials. For sandbox testing, Worldpay provides test card numbers (such as 4111111111111111 for a successful Visa transaction) that you can use without real card data.

```
// Node.js: credential setup and authentication helper
const WORLDPAY_SERVICE_KEY = process.env.WORLDPAY_SERVICE_KEY;
const WORLDPAY_CLIENT_KEY = process.env.WORLDPAY_CLIENT_KEY;
const WORLDPAY_MERCHANT_CODE = process.env.WORLDPAY_MERCHANT_CODE;
const WORLDPAY_ENV = process.env.WORLDPAY_ENV || 'sandbox';

const WORLDPAY_BASE_URL = WORLDPAY_ENV === 'production'
  ? 'https://access.worldpay.com'
  : 'https://try.access.worldpay.com';

// HTTP Basic Auth header
const basicAuth = Buffer.from(`${WORLDPAY_SERVICE_KEY}:${WORLDPAY_CLIENT_KEY}`).toString('base64');

const worldpayHeaders = {
  'Authorization': `Basic ${basicAuth}`,
  'Content-Type': 'application/json',
  'Accept': 'application/json'
};

if (!WORLDPAY_SERVICE_KEY || !WORLDPAY_CLIENT_KEY || !WORLDPAY_MERCHANT_CODE) {
  console.error('ERROR: Worldpay credentials not set in Replit Secrets. Add WORLDPAY_SERVICE_KEY, WORLDPAY_CLIENT_KEY, and WORLDPAY_MERCHANT_CODE.');
  process.exit(1);
}
```

**Expected result:** Your Worldpay credentials are stored as Replit Secrets, accessible as environment variables, and your server logs an error if any required credential is missing at startup.

### 2. Build the Payment Authorization Endpoint

The core of the Worldpay Access API integration is the payment authorization endpoint. The payment flow works as follows: the customer's browser collects card data using Worldpay's Access Checkout JavaScript SDK (which returns a session reference, not raw card data), your server sends an authorization request to Worldpay's /payments endpoint with the session reference, amount, currency, and order details, and Worldpay returns either an approved authorization, a decline, or a 3DS2 challenge required response. The authorization request body includes the transaction value and currency, the merchant entity reference, the payment instrument (session reference from the client), and risk/fraud prevention data. If Worldpay returns a 3DS2 challenge required response, you must redirect the customer to the issuing bank's authentication page. Install axios by running npm install express axios in the Replit Shell.

```
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// Worldpay config (from worldpay-config.js or inline)
const WORLDPAY_SERVICE_KEY = process.env.WORLDPAY_SERVICE_KEY;
const WORLDPAY_CLIENT_KEY = process.env.WORLDPAY_CLIENT_KEY;
const WORLDPAY_MERCHANT_CODE = process.env.WORLDPAY_MERCHANT_CODE;
const WORLDPAY_BASE_URL = process.env.WORLDPAY_ENV === 'production'
  ? 'https://access.worldpay.com'
  : 'https://try.access.worldpay.com';

const basicAuth = Buffer.from(`${WORLDPAY_SERVICE_KEY}:${WORLDPAY_CLIENT_KEY}`).toString('base64');
const worldpayHeaders = {
  'Authorization': `Basic ${basicAuth}`,
  'Content-Type': 'application/json',
  'Accept': 'application/json'
};

// Authorize a payment
app.post('/api/payments/authorize', async (req, res) => {
  const { sessionHref, amount, currencyCode, orderDescription, merchantReference } = req.body;

  if (!sessionHref || !amount || !currencyCode) {
    return res.status(400).json({ error: 'sessionHref, amount, and currencyCode are required' });
  }

  const reference = merchantReference || `ORDER-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;

  const authorizationPayload = {
    transactionReference: reference,
    merchant: { entity: WORLDPAY_MERCHANT_CODE },
    instruction: {
      narrative: { line1: orderDescription || 'Order payment' },
      value: {
        currency: currencyCode,
        amount: Math.round(amount * 100) // Convert to minor units (cents/pence)
      },
      paymentInstrument: {
        type: 'card/checkout',
        sessionHref: sessionHref
      }
    }
  };

  try {
    const response = await axios.post(
      `${WORLDPAY_BASE_URL}/payments/authorizations`,
      authorizationPayload,
      { headers: worldpayHeaders, timeout: 30000 }
    );

    const result = response.data;
    const outcome = result.outcome;

    if (outcome === 'authorized') {
      res.json({
        success: true,
        transactionReference: reference,
        authorizationCode: result.authorizationCode,
        outcome: outcome
      });
    } else if (outcome === 'challenged') {
      // 3DS2 challenge required — return challenge details to frontend
      res.json({
        success: false,
        requires3DS: true,
        transactionReference: reference,
        challenge: result._links?.['payments:challenge'] || null,
        outcome: outcome
      });
    } else {
      res.json({
        success: false,
        transactionReference: reference,
        outcome: outcome,
        description: result.description
      });
    }
  } catch (error) {
    const apiError = error.response?.data;
    console.error('Worldpay authorization error:', apiError || error.message);
    res.status(error.response?.status || 500).json({
      error: 'Payment authorization failed',
      details: apiError?.message || error.message
    });
  }
});

app.listen(3000, '0.0.0.0', () => console.log('Worldpay payment server running on port 3000'));
```

**Expected result:** POST /api/payments/authorize with valid session data returns either an authorization confirmation, a 3DS2 challenge object, or a decline response.

### 3. Handle 3DS2 Authentication

3D Secure 2 (3DS2) is mandatory for most card-not-present transactions in Europe under PSD2 Strong Customer Authentication requirements. The Worldpay Access API supports both the frictionless path (where the card issuer approves without user interaction based on risk scoring) and the challenge path (where the customer is presented with an authentication challenge from their bank, typically a one-time code). When your authorization endpoint receives an 'outcome: challenged' response, it means the card issuer requires the customer to complete an authentication step. The response includes a challenge URL and a three-domain secure object. Your frontend must redirect the customer to this challenge URL (or display it in an iframe) so the issuer can authenticate them. After authentication, the issuer redirects back to your specified return URL with a session reference. You then complete the payment by submitting a second authorization request with the completed 3DS authentication data. This step confirms the authentication was successful and finalizes the payment.

```
// Handle 3DS2 completion after customer authenticates with their bank
app.post('/api/payments/3ds-complete', async (req, res) => {
  const { transactionReference, authenticationValue, eci, dsTransactionId } = req.body;

  if (!transactionReference) {
    return res.status(400).json({ error: 'transactionReference is required' });
  }

  const completionPayload = {
    transactionReference: transactionReference,
    merchant: { entity: WORLDPAY_MERCHANT_CODE },
    authentication: {
      version: '2.1.0',
      authenticationValue: authenticationValue,
      eci: eci,
      dsTransactionId: dsTransactionId
    }
  };

  try {
    const response = await axios.post(
      `${WORLDPAY_BASE_URL}/payments/authorizations/threeDS/complete`,
      completionPayload,
      { headers: worldpayHeaders, timeout: 30000 }
    );

    const result = response.data;
    res.json({
      success: result.outcome === 'authorized',
      transactionReference: transactionReference,
      outcome: result.outcome,
      authorizationCode: result.authorizationCode || null
    });
  } catch (error) {
    console.error('3DS completion error:', error.response?.data || error.message);
    res.status(error.response?.status || 500).json({
      error: '3DS completion failed',
      details: error.response?.data?.message || error.message
    });
  }
});

// Inquiry endpoint to check transaction status
app.get('/api/payments/:reference', async (req, res) => {
  try {
    const response = await axios.get(
      `${WORLDPAY_BASE_URL}/payments/authorizations/${req.params.reference}`,
      { headers: worldpayHeaders, timeout: 15000 }
    );
    res.json(response.data);
  } catch (error) {
    console.error('Inquiry error:', error.response?.data || error.message);
    res.status(error.response?.status || 500).json({ error: 'Transaction inquiry failed' });
  }
});
```

**Expected result:** POST /api/payments/3ds-complete successfully completes a 3DS2 authenticated transaction, returning an authorized status and authorization code.

### 4. Implement Capture and Refund

Worldpay's authorization flow by default performs an immediate authorization and capture (settlement). However, for physical goods or scenarios where you want to authorize first and capture only when goods ship, you can use a two-stage authorization/capture flow. To capture a previously authorized but not yet captured transaction, POST to the captures endpoint with the transaction reference. To issue a full or partial refund on a settled transaction, POST to the refunds endpoint. Both operations are idempotent — re-submitting the same request with the same reference won't double-charge or double-refund. Build these endpoints in Python to show the Flask alternative as well, so teams using Python can implement the full payment lifecycle.

```
import os
import base64
import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

SERVICE_KEY = os.environ['WORLDPAY_SERVICE_KEY']
CLIENT_KEY = os.environ['WORLDPAY_CLIENT_KEY']
MERCHANT_CODE = os.environ['WORLDPAY_MERCHANT_CODE']
ENV = os.environ.get('WORLDPAY_ENV', 'sandbox')

BASE_URL = 'https://access.worldpay.com' if ENV == 'production' else 'https://try.access.worldpay.com'
AUTH_HEADER = base64.b64encode(f'{SERVICE_KEY}:{CLIENT_KEY}'.encode()).decode()
HEADERS = {
    'Authorization': f'Basic {AUTH_HEADER}',
    'Content-Type': 'application/json',
    'Accept': 'application/json'
}

@app.route('/api/payments/<reference>/capture', methods=['POST'])
def capture_payment(reference):
    data = request.json or {}
    capture_value = data.get('amount')

    payload = {
        'transactionReference': reference,
        'merchant': {'entity': MERCHANT_CODE}
    }
    if capture_value:
        payload['value'] = {
            'currency': data.get('currency', 'GBP'),
            'amount': int(float(capture_value) * 100)
        }

    try:
        resp = requests.post(
            f'{BASE_URL}/payments/settlements/captures',
            json=payload, headers=HEADERS, timeout=30
        )
        resp.raise_for_status()
        return jsonify({'success': True, 'reference': reference, 'result': resp.json()})
    except requests.RequestException as e:
        error_data = e.response.json() if e.response else {'message': str(e)}
        return jsonify({'error': 'Capture failed', 'details': error_data}), 500

@app.route('/api/payments/<reference>/refund', methods=['POST'])
def refund_payment(reference):
    data = request.json or {}
    if not data.get('amount') or not data.get('currency'):
        return jsonify({'error': 'amount and currency are required'}), 400

    payload = {
        'transactionReference': reference,
        'merchant': {'entity': MERCHANT_CODE},
        'value': {
            'currency': data['currency'],
            'amount': int(float(data['amount']) * 100)
        }
    }

    try:
        resp = requests.post(
            f'{BASE_URL}/payments/settlements/refunds',
            json=payload, headers=HEADERS, timeout=30
        )
        resp.raise_for_status()
        return jsonify({'success': True, 'refund_reference': reference, 'result': resp.json()})
    except requests.RequestException as e:
        error_data = e.response.json() if e.response else {'message': str(e)}
        return jsonify({'error': 'Refund failed', 'details': error_data}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)
```

**Expected result:** POST /api/payments/{reference}/refund successfully issues a refund and returns the refund reference from Worldpay.

### 5. Configure Replit for Production Payment Processing

For a production payment backend, Replit Reserved VM is the required deployment type — never use Autoscale for payment infrastructure, as Autoscale can scale to zero and experience cold starts during periods of low traffic, which is unacceptable for checkout flows. Reserved VM provides a persistent server with no cold starts, guaranteed uptime, and fixed resource allocation. Configure your .replit file to specify the deployment target and port binding. Before going live with production Worldpay credentials, complete the following checklist: update WORLDPAY_ENV in Replit Secrets to 'production', replace sandbox credentials with production credentials (update all three secrets), ensure your server enforces HTTPS (Replit deployment provides SSL automatically), add rate limiting to your payment endpoints to prevent abuse, implement request idempotency using merchant transaction references to prevent duplicate charges, and add comprehensive logging for all payment attempts and outcomes for audit purposes. Worldpay requires that you log transaction references for reconciliation. Also confirm that your PCI DSS implementation is correct — if your Replit server handles only session tokens and never raw card numbers, you're in scope for SAQ A-EP rather than full SAQ D.

```
# .replit configuration for production payment server
# Update this in your .replit file

# [deployment]
# run = ["node", "server.js"]
# deploymentTarget = "cloudrun"
#
# [[ports]]
# internalPort = 3000
# externalPort = 80

# Add rate limiting to Express (npm install express-rate-limit)
const rateLimit = require('express-rate-limit');

const paymentLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 10, // max 10 payment attempts per minute per IP
  message: { error: 'Too many payment attempts. Please wait before trying again.' },
  standardHeaders: true
});

app.post('/api/payments/authorize', paymentLimiter, async (req, res) => {
  // ... payment authorization code
});

# Idempotency: generate deterministic transaction references
function generateMerchantReference(orderId, customerId) {
  // Include order and customer IDs to make reference deterministic
  // Re-submitting with the same reference won't duplicate the charge
  return `ORD-${orderId}-CUST-${customerId}-${Date.now()}`;
}
```

**Expected result:** Your Worldpay payment server is deployed on Replit Reserved VM, accessible via a stable HTTPS URL, with rate limiting and idempotency controls in place.

## Best practices

- Store all Worldpay credentials (service key, client key, merchant code) in Replit Secrets — never hardcode them in source files or commit them to Git
- Use Replit Reserved VM for production payment endpoints — Autoscale's scale-to-zero behavior causes cold starts that are unacceptable in checkout flows
- Never allow raw card numbers to reach your Replit server — use Worldpay's Access Checkout SDK to collect card data in the browser and pass only session tokens to your backend
- Always use minor currency units (pence/cents) in API requests — convert £12.99 to 1299 before sending, never send decimal amounts
- Implement rate limiting on payment endpoints to prevent fraud and accidental double-submissions — use express-rate-limit or Flask-Limiter to cap attempts per IP
- Generate deterministic merchant transaction references (including order ID and timestamp) so that Worldpay can detect and reject duplicate submissions of the same order
- Log every payment attempt with its transaction reference, outcome, and timestamp to a durable store — Worldpay requires transaction records for reconciliation and disputes
- Start all development and testing on Worldpay's sandbox environment (try.access.worldpay.com) — only switch to production credentials (access.worldpay.com) after thorough sandbox testing

## Use cases

### Server-to-Server Payment Authorization

Build a Replit backend that accepts tokenized card data from Worldpay's Access Checkout SDK (running in the browser), then sends a payment authorization request to the Worldpay Access API server-side. The server handles the full authorization, 3DS2 challenge routing, and returns the authorization response to the frontend without raw card numbers ever leaving the browser.

Prompt example:

```
Build a payment authorization endpoint that accepts a Worldpay session token and order details (amount, currency, merchant reference), sends an authorization request to the Worldpay Access API, handles the 3DS2 response (either frictionless approval or challenge redirect), and returns the authorization result with the transaction reference.
```

### Recurring Billing with Payment Tokens

After an initial card-present or card-not-present transaction, Worldpay can return a token representing the card. Use this token for recurring billing charges without requiring the customer to re-enter card details. Your Replit backend stores the token reference and uses it to initiate subsequent charges via the Worldpay Access API's repeat payment flow.

Prompt example:

```
Build a recurring billing system that stores customer payment tokens returned from initial Worldpay transactions, and creates a scheduled endpoint that submits repeat authorization requests using stored tokens for monthly subscription charges, logging each transaction result to a database.
```

### Payment Refund and Inquiry System

Build an admin API on Replit that lets your operations team look up transaction status and issue full or partial refunds through the Worldpay Access API. The server validates refund requests, checks the original transaction status, and submits the refund to Worldpay, returning the refund reference number and status.

Prompt example:

```
Build an admin refund endpoint that accepts a Worldpay transaction reference and a refund amount, validates that the refund amount does not exceed the original transaction amount, submits the refund to the Worldpay Access API, and returns the refund reference and status.
```

## Troubleshooting

### API returns 401 Unauthorized on every request

Cause: The WORLDPAY_SERVICE_KEY and WORLDPAY_CLIENT_KEY credentials are incorrect, not set in Replit Secrets, or the Base64 encoding of the username:password pair has an error.

Solution: Open the Replit Secrets panel (lock icon 🔒) and verify both WORLDPAY_SERVICE_KEY and WORLDPAY_CLIENT_KEY are set correctly with no extra whitespace. Verify the Base64 encoding is correct: Buffer.from('username:password').toString('base64') in Node.js or base64.b64encode(b'username:password').decode() in Python. Contact Worldpay support to confirm your API credentials are active for the environment you're using.

```
// Test your credentials directly
const testAuth = async () => {
  const auth = Buffer.from(`${process.env.WORLDPAY_SERVICE_KEY}:${process.env.WORLDPAY_CLIENT_KEY}`).toString('base64');
  console.log('Auth header:', `Basic ${auth.substring(0, 20)}...`);
};
testAuth();
```

### Payment authorization returns 'outcome: refused' with error code 5 (Do Not Honour)

Cause: In the sandbox environment, certain test card numbers always return specific decline codes. Code 5 (Do Not Honour) is the most common generic decline. In production, this means the issuing bank declined the transaction.

Solution: In sandbox testing, use Worldpay's published test card numbers for specific outcomes. For a successful authorization, use 4111111111111111 (Visa) or 5101180000000007 (Mastercard). Check the Worldpay sandbox documentation for test cards that trigger specific decline codes for testing your error handling logic.

### 3DS2 challenge response is not received or challenge redirect fails

Cause: The 3DS2 challenge flow requires the frontend to redirect the customer to the issuer's authentication URL, and then redirect back to a return URL you specify. If the return URL isn't accessible or HTTPS, or if it's a localhost URL, the redirect will fail.

Solution: Ensure your 3DS2 return URL is a publicly accessible HTTPS URL pointing to your Replit deployment (not the development preview). Test the full 3DS2 flow using Worldpay's sandbox 3DS2 test cards, which trigger the challenge path. The return URL must match what's configured in your Worldpay merchant profile's allowed redirect URLs.

### Server returns 500 errors and logs show 'ECONNRESET' or 'socket hang up'

Cause: Worldpay's API can be slow on some requests (up to 10-20 seconds for 3DS2 flows), and Replit's connection may time out before the API responds. The default axios timeout of 0 (no timeout) can also leave requests hanging indefinitely.

Solution: Increase your axios timeout to at least 30 seconds for payment authorization requests and 60 seconds for 3DS2 completion calls. Implement retry logic with exponential backoff for network errors (but NOT for payment authorizations — retry a payment only if you're certain it hasn't been processed to avoid double-charging).

```
const response = await axios.post(url, payload, {
  headers: worldpayHeaders,
  timeout: 30000, // 30 seconds for authorization
  validateStatus: (status) => status < 500 // Don't throw on 4xx responses
});
```

## Frequently asked questions

### How do I connect Replit to Worldpay?

Store your Worldpay API service key, client key, and merchant code in Replit Secrets (lock icon 🔒 in the sidebar). In your server code, create a Basic Auth header by Base64-encoding 'servicekey:clientkey' and include it as 'Authorization: Basic ...' on all requests to the Worldpay Access API at https://access.worldpay.com (production) or https://try.access.worldpay.com (sandbox).

### Does Replit work with Worldpay in sandbox mode?

Yes — Worldpay provides a full sandbox environment at try.access.worldpay.com with test card numbers for simulating different authorization outcomes. Set WORLDPAY_ENV to 'sandbox' in Replit Secrets and use the sandbox credentials from your Worldpay onboarding documentation to test without processing real payments.

### Is it safe to process Worldpay payments on Replit?

Yes, if you follow PCI DSS requirements. The critical rule is that raw card numbers must never reach your Replit server — use Worldpay's Access Checkout JavaScript SDK in the browser to collect card data, which returns a session token that your server uses instead of the real card number. Replit's environment itself is secure, but the architecture of your integration must keep card data out of your server code.

### How do I handle 3DS2 authentication with Worldpay on Replit?

When Worldpay returns an 'outcome: challenged' response from an authorization request, extract the challenge URL from the response and redirect the customer to it. After the customer completes authentication with their bank, they're redirected back to your configured return URL with authentication data. Pass this data to your Replit server, which then calls the Worldpay 3DS completion endpoint to finalize the payment.

### Should I use Replit Autoscale or Reserved VM for Worldpay payment processing?

Always use Reserved VM for production payment endpoints. Autoscale deployments can scale to zero and experience cold start delays of several seconds, which is unacceptable in a payment checkout flow where customers expect immediate responses. Reserved VM provides a constantly running server with no cold starts. The extra monthly cost is worth it for reliable payment processing.

### What currency format does the Worldpay Access API expect?

The Worldpay Access API expects monetary amounts in minor currency units — pence for GBP, cents for USD and EUR. You must convert decimal amounts to integers before sending: £29.99 becomes 2999, $100.00 becomes 10000. Always use Math.round(amount * 100) to handle floating-point rounding, never just multiply without rounding.

---

Source: https://www.rapidevelopers.com/replit-integration/worldpay
© RapidDev — https://www.rapidevelopers.com/replit-integration/worldpay
