# How to Integrate Replit with Sage Pay

- Tool: Replit
- Difficulty: Intermediate
- Time required: 60 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Sage Pay (Opayo), create an Opayo developer account, obtain your integration key and password for the REST API, store them in Replit Secrets (lock icon 🔒), and use Python or Node.js to process UK and EU card payments with 3D Secure authentication. Deploy on Autoscale for PCI-compliant payment processing.

## Why Connect Replit to Opayo (Sage Pay)?

Opayo (formerly Sage Pay) is the most widely used payment gateway among UK small and medium businesses. It processes billions of pounds annually and is deeply integrated with UK banking infrastructure, making it the default choice for UK-based e-commerce and SaaS products. The Opayo Pi REST API is the modern REST interface replacing the older Sage Pay Form and Server integrations — it supports 3D Secure 2.0, tokenised card storage, and recurring billing.

The Pi API uses a two-step tokenisation model designed for PCI DSS compliance. First, your backend creates a temporary merchant session key (MSK) by calling the sessions endpoint. Your frontend JavaScript uses this MSK to tokenise the card number directly with Opayo's servers, returning a card identifier. Your backend then uses the card identifier (never the raw card number) to create a transaction. This architecture means raw card numbers never touch your Replit server.

3D Secure is mandatory for most UK card payments under Strong Customer Authentication (SCA) regulations. The Opayo Pi API integrates 3DS2 natively. Replit's always-reachable deployment URL is required for the 3DS callback to work in production — use a Reserved VM deployment to ensure your callback endpoint is always available.

## Before you start

- A Replit account with a Python or Node.js project created
- An Opayo developer account at https://www.opayo.co.uk/support/12/36/testing-your-sandbox (sandbox) or a live merchant account
- Your Opayo integration key and integration password from the MySagePay or Opayo portal
- Understanding of PCI DSS tokenisation concepts (raw card numbers should never reach your server)
- A deployed Replit URL (Autoscale or Reserved VM) for 3D Secure callback handling — localhost will not work for 3DS in production

## Step-by-step guide

### 1. Get Opayo Sandbox Credentials

Go to https://www.opayo.co.uk and create a developer account to access the sandbox environment. The Opayo sandbox is a free testing environment where you can process test card payments without real money being moved. Once your account is created, log into the MySagePay portal and navigate to Settings > Integrations to find your Integration Key and Integration Password.

Opayo provides separate credentials for sandbox and live environments. The sandbox base URL is https://pi-test.sagepay.com/api/v1 and the live URL is https://pi.sagepay.com/api/v1. Always develop and test against the sandbox before switching to live credentials.

Opayo also provides test card numbers for sandbox testing. The standard test Visa card number is 4929000000006 (3DS enrolled), and a Mastercard test number is 5404000000000001. These card numbers are only valid in the sandbox environment. For 3D Secure testing, Opayo provides specific test cards that trigger different 3DS outcomes (challenge, frictionless pass, frictionless fail) — check the Opayo developer documentation for the full list.

Store both your integration key and password securely — these are your API authentication credentials and must never be committed to source code.

**Expected result:** You have an Opayo integration key and integration password for the sandbox environment.

### 2. Store Credentials in Replit Secrets

Click the lock icon 🔒 in the Replit sidebar to open the Secrets pane. Add three secrets:

- Key: OPAYO_INTEGRATION_KEY — Value: your integration key
- Key: OPAYO_INTEGRATION_PASSWORD — Value: your integration password
- Key: OPAYO_ENVIRONMENT — Value: sandbox (change to live when ready)

The integration key and password are combined and base64-encoded to create the Basic auth header for every API request: base64(integrationKey:integrationPassword). Your Python or Node.js code handles this encoding automatically — you never need to encode manually.

Opayo API credentials are particularly sensitive because they grant the ability to create real payment transactions on your merchant account. Never log these values, never include them in error messages, and never expose them in API responses. Replit's Secret Scanner monitors your code for accidentally committed credentials.

**Expected result:** OPAYO_INTEGRATION_KEY, OPAYO_INTEGRATION_PASSWORD, and OPAYO_ENVIRONMENT appear in the Replit Secrets pane.

### 3. Create a Merchant Session Key and Process Payment (Python)

The Opayo Pi payment flow has three steps: (1) create a merchant session key (MSK) using your backend credentials, (2) use the MSK in frontend JavaScript to tokenise the card number into a card identifier, and (3) submit a transaction using the card identifier. This server demonstrates steps 1 and 3 — step 2 happens in the browser using Opayo's Drop-In UI or a custom card form with their JS library.

The Python server below creates a Flask backend with three routes: /session-key (creates MSK for frontend), /transaction (submits the payment), and /3ds-callback (handles the 3D Secure redirect). The transaction endpoint accepts the card identifier from the frontend and creates a Payment, Deferred, or Authenticate transaction.

For SCA compliance, include the customer's billing address and, if available, the 3DS2 browser data (screen resolution, timezone, color depth) in the transaction request. Opayo uses this data for frictionless 3DS2 authentication, which avoids the challenge redirect for low-risk transactions.

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

app = Flask(__name__)

INTEGRATION_KEY = os.environ["OPAYO_INTEGRATION_KEY"]
INTEGRATION_PASSWORD = os.environ["OPAYO_INTEGRATION_PASSWORD"]
ENVIRONMENT = os.environ.get("OPAYO_ENVIRONMENT", "sandbox")

BASE_URL = (
    "https://pi-test.sagepay.com/api/v1"
    if ENVIRONMENT == "sandbox"
    else "https://pi.sagepay.com/api/v1"
)


def get_auth_header() -> str:
    """Create Basic auth header from integration key and password."""
    credentials = f"{INTEGRATION_KEY}:{INTEGRATION_PASSWORD}"
    encoded = base64.b64encode(credentials.encode()).decode()
    return f"Basic {encoded}"


HEADERS = {
    "Authorization": get_auth_header(),
    "Content-Type": "application/json",
    "Cache-Control": "no-cache"
}


@app.route("/session-key", methods=["POST"])
def create_session_key():
    """Create a merchant session key for frontend card tokenisation."""
    resp = requests.post(
        f"{BASE_URL}/merchant-session-keys",
        json={"vendorName": os.environ.get("OPAYO_VENDOR_NAME", "sandbox")},
        headers=HEADERS
    )
    resp.raise_for_status()
    return jsonify(resp.json())


@app.route("/transaction", methods=["POST"])
def create_transaction():
    """Submit a card payment using a card identifier from the frontend."""
    data = request.get_json()
    card_identifier = data.get("cardIdentifier")
    amount = data.get("amount")  # in pence, e.g. 1000 = £10.00
    order_ref = data.get("orderRef", "ORDER001")

    transaction_payload = {
        "transactionType": "Payment",
        "paymentMethod": {
            "card": {
                "merchantSessionKey": data.get("merchantSessionKey"),
                "cardIdentifier": card_identifier
            }
        },
        "vendorTxCode": order_ref,
        "amount": amount,
        "currency": "GBP",
        "description": data.get("description", "Online payment"),
        "apply3DSecure": "UseMSPSetting",
        "customerFirstName": data.get("firstName", ""),
        "customerLastName": data.get("lastName", ""),
        "billingAddress": {
            "address1": data.get("address1", ""),
            "city": data.get("city", ""),
            "postalCode": data.get("postalCode", ""),
            "country": data.get("country", "GB")
        },
        "strongCustomerAuthentication": {
            "website": data.get("returnUrl", ""),
            "notificationURL": f"{request.host_url}3ds-callback"
        }
    }

    resp = requests.post(
        f"{BASE_URL}/transactions",
        json=transaction_payload,
        headers=HEADERS
    )

    result = resp.json()

    # Check if 3DS challenge is required
    if result.get("status") == "3DAuth":
        return jsonify({
            "status": "3DAuth",
            "acsUrl": result.get("acsUrl"),
            "acsTransId": result.get("acsTransId"),
            "dsTransId": result.get("dsTransId"),
            "cReq": result.get("cReq"),
            "transactionId": result.get("transactionId")
        })

    return jsonify(result)


@app.route("/3ds-callback", methods=["POST"])
def handle_3ds_callback():
    """Handle 3D Secure authentication callback from Opayo."""
    cres = request.form.get("cres")
    transaction_id = request.form.get("transactionId")

    resp = requests.post(
        f"{BASE_URL}/transactions/{transaction_id}/3d-secure-challenge",
        json={"cRes": cres},
        headers=HEADERS
    )
    result = resp.json()
    status = result.get("status", "")

    if status == "Ok":
        return redirect("/payment-success")
    else:
        return redirect(f"/payment-failed?status={status}")


@app.route("/payment-success")
def payment_success():
    return jsonify({"message": "Payment completed successfully"})


@app.route("/payment-failed")
def payment_failed():
    status = request.args.get("status", "Failed")
    return jsonify({"message": f"Payment failed: {status}"}), 400


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

**Expected result:** POST /session-key returns a merchant session key and POST /transaction creates a payment, returning either a status of 'Ok' (approved) or '3DAuth' (3DS challenge required).

### 4. Build a Node.js Opayo Payment Server

Install dependencies with 'npm install express axios'. The Node.js implementation mirrors the Python version. The key difference is that Node.js uses Buffer.from() for base64 encoding instead of Python's base64 module.

The server below implements the same three-route pattern: session key creation, transaction submission, and 3DS callback handling. Error responses from Opayo include a 'status' field and 'statusDetail' description — always log these details for debugging failed transactions.

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

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true })); // for 3DS form POST

const INTEGRATION_KEY = process.env.OPAYO_INTEGRATION_KEY;
const INTEGRATION_PASSWORD = process.env.OPAYO_INTEGRATION_PASSWORD;
const ENVIRONMENT = process.env.OPAYO_ENVIRONMENT || 'sandbox';
const VENDOR_NAME = process.env.OPAYO_VENDOR_NAME || 'sandbox';

const BASE_URL = ENVIRONMENT === 'sandbox'
  ? 'https://pi-test.sagepay.com/api/v1'
  : 'https://pi.sagepay.com/api/v1';

const AUTH = Buffer.from(`${INTEGRATION_KEY}:${INTEGRATION_PASSWORD}`).toString('base64');
const HEADERS = {
  Authorization: `Basic ${AUTH}`,
  'Content-Type': 'application/json',
  'Cache-Control': 'no-cache'
};

// Create merchant session key
app.post('/session-key', async (req, res) => {
  try {
    const { data } = await axios.post(
      `${BASE_URL}/merchant-session-keys`,
      { vendorName: VENDOR_NAME },
      { headers: HEADERS }
    );
    res.json(data);
  } catch (err) {
    res.status(err.response?.status || 500).json(err.response?.data || { error: err.message });
  }
});

// Create payment transaction
app.post('/transaction', async (req, res) => {
  const {
    cardIdentifier, merchantSessionKey, amount, orderRef,
    description, firstName, lastName,
    address1, city, postalCode, country = 'GB'
  } = req.body;

  const payload = {
    transactionType: 'Payment',
    paymentMethod: {
      card: { merchantSessionKey, cardIdentifier }
    },
    vendorTxCode: orderRef || `ORDER-${Date.now()}`,
    amount: Number(amount),
    currency: 'GBP',
    description: description || 'Online payment',
    apply3DSecure: 'UseMSPSetting',
    customerFirstName: firstName || '',
    customerLastName: lastName || '',
    billingAddress: {
      address1: address1 || '',
      city: city || '',
      postalCode: postalCode || '',
      country
    },
    strongCustomerAuthentication: {
      website: req.body.returnUrl || '',
      notificationURL: `${req.protocol}://${req.get('host')}/3ds-callback`
    }
  };

  try {
    const { data } = await axios.post(`${BASE_URL}/transactions`, payload, { headers: HEADERS });
    res.json(data);
  } catch (err) {
    res.status(err.response?.status || 500).json(err.response?.data || { error: err.message });
  }
});

// Handle 3D Secure callback
app.post('/3ds-callback', async (req, res) => {
  const { cres, transactionId } = req.body;
  try {
    const { data } = await axios.post(
      `${BASE_URL}/transactions/${transactionId}/3d-secure-challenge`,
      { cRes: cres },
      { headers: HEADERS }
    );
    if (data.status === 'Ok') {
      return res.redirect('/payment-success');
    }
    res.redirect(`/payment-failed?status=${data.status}`);
  } catch (err) {
    res.redirect(`/payment-failed?status=error`);
  }
});

app.get('/payment-success', (req, res) => {
  res.json({ message: 'Payment completed successfully' });
});

app.get('/payment-failed', (req, res) => {
  res.status(400).json({ message: `Payment failed: ${req.query.status || 'Unknown'}` });
});

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

**Expected result:** POST /session-key returns a merchantSessionKey and expiry, and POST /transaction creates a payment transaction with Opayo.

### 5. Deploy on Reserved VM for Payment Reliability

For payment processing, deploy on a Reserved VM rather than Autoscale. Autoscale can scale to zero during idle periods, which means the first request after a cold start takes several seconds to respond. During a 3D Secure challenge flow, this delay could cause the authentication callback to time out, resulting in a failed payment for the customer.

Click the Deploy button in Replit and select Reserved VM. This keeps your server always running with a consistent response time. Update the .replit file to use the correct run command.

After deploying, update your Opayo merchant portal with your production deployment URL as the notification URL for 3D Secure callbacks. Also switch OPAYO_ENVIRONMENT from 'sandbox' to 'live' in Replit Secrets and replace the sandbox credentials with your live integration key and password.

Before going live, test the full payment flow end-to-end in the sandbox with multiple test card numbers including 3DS-challenge cards, declined cards, and 3DS2 frictionless cards to ensure all paths are handled correctly.

```
[[ports]]
internalPort = 3000
externalPort = 80

[deployment]
run = ["node", "server.js"]
deploymentTarget = "cloudrun"
```

**Expected result:** Your Replit payment server is deployed on a Reserved VM with a stable URL, successfully processing sandbox test card payments including 3D Secure challenges.

## Best practices

- Store OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD in Replit Secrets — never hardcode them in source files or include them in API responses.
- Use a Reserved VM deployment for payment servers — Autoscale cold starts can delay 3D Secure callback processing and cause transaction timeouts.
- Always validate and sanitize the payment amount on the server before submitting to Opayo — never trust amount values sent from the frontend.
- Generate unique vendorTxCode values for every transaction using UUID or timestamp to prevent duplicate transaction errors.
- Implement idempotent payment handling — check the Opayo transaction status before retrying to avoid double-charging customers.
- Test all 3DS paths in the sandbox (frictionless pass, frictionless fail, challenge, declined) before switching to live credentials.
- Log transaction IDs and statuses from Opayo responses for dispute resolution — do not log card numbers, CVV values, or raw card identifiers.
- Handle the 3DAuth status explicitly in your transaction response — send the acsUrl and cReq fields to the frontend for the 3DS redirect, never assume all transactions will be approved without a challenge.

## Use cases

### UK E-commerce Checkout

A Replit-backed online shop uses the Opayo Pi API to accept card payments from UK customers. The backend creates a merchant session key, passes it to the frontend for card tokenisation, then submits the transaction with 3D Secure. The Replit server handles the 3DS callback and redirects the customer to an order confirmation page.

Prompt example:

```
Build a Flask payment server that creates Opayo merchant session keys, submits card transactions with 3D Secure, handles the 3DS callback, and returns payment success or failure status to a frontend checkout page.
```

### Subscription Billing with Card Tokens

A SaaS product built on Replit uses Opayo to charge UK customers on a recurring monthly basis. On initial signup, the card is tokenised and a reusable card token is saved in the database. The Replit backend runs a scheduled job to charge the stored token each month without requiring the customer to re-enter card details.

Prompt example:

```
Create a Node.js script that saves a reusable Opayo card token on first payment and charges it automatically for monthly subscription renewals, handling declined card responses with retry logic.
```

### Payment Status Dashboard

A Replit admin tool queries the Opayo Pi API to retrieve and display recent transaction statuses, identify failed payments, and trigger refunds from an internal dashboard. The backend uses the Opayo reporting endpoints to build a real-time view of payment health without logging into the Opayo merchant portal.

Prompt example:

```
Write a Flask dashboard that fetches recent Opayo transactions, displays success and failure rates, and provides a button to issue a refund for a selected transaction ID.
```

## Troubleshooting

### HTTP 401 Unauthorized — 'Invalid Credentials' error from Opayo API

Cause: The base64 encoding of the integration key:password pair is incorrect, or the credentials in Replit Secrets do not match the environment (sandbox vs live).

Solution: Verify the OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD values in Replit Secrets exactly match what is shown in your Opayo/MySagePay portal. Check that OPAYO_ENVIRONMENT is set to 'sandbox' when using sandbox credentials. The colon separator in the encoding must be between key and password with no extra spaces.

```
import base64
key = os.environ['OPAYO_INTEGRATION_KEY']
pwd = os.environ['OPAYO_INTEGRATION_PASSWORD']
auth = base64.b64encode(f'{key}:{pwd}'.encode()).decode()
print(f'Auth header: Basic {auth[:20]}...')  # debug first 20 chars
```

### Transaction returns status 'NotAuthed' — payment declined by the bank

Cause: In the sandbox, specific test cards are required to get approved transactions. Using a real card number or the wrong test card number results in a NotAuthed status.

Solution: Use Opayo's official sandbox test card numbers. For a basic approved Visa payment, use 4929000000006 with any future expiry date and CVV 123. Check the Opayo developer documentation for the full list of test cards and their expected outcomes (approved, declined, 3DS challenge, insufficient funds).

### 3D Secure callback never fires — payment hangs after 3DS challenge

Cause: The notificationURL sent to Opayo points to localhost or a Replit preview URL that is not publicly accessible from Opayo's servers.

Solution: The 3DS notification URL must be a publicly accessible HTTPS URL. Use your Replit deployment URL (e.g., https://your-app.yourusername.repl.co) rather than the editor preview URL. Deploy your app first, then use the deployment URL in the notificationURL field.

```
# Use the public deployment URL, not the preview URL
notification_url = "https://your-app.yourusername.repl.co/3ds-callback"
```

### vendorTxCode error — 'Transaction already exists with this code'

Cause: The vendorTxCode must be unique across all transactions on your merchant account. Reusing the same order reference causes this error.

Solution: Generate a unique vendorTxCode for each transaction by including a timestamp or UUID. The code can be up to 40 characters and must be unique within your merchant account.

```
import uuid
vendor_tx_code = f"ORDER-{uuid.uuid4().hex[:16].upper()}"
```

## Frequently asked questions

### How do I store Opayo credentials in Replit?

Click the lock icon 🔒 in the Replit sidebar to open Secrets. Add OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD with your credentials from the Opayo portal. Access them in Python with os.environ['OPAYO_INTEGRATION_KEY'] and in Node.js with process.env.OPAYO_INTEGRATION_KEY. Restart the Repl after adding secrets.

### Is 3D Secure mandatory for Opayo payments?

Yes, for most UK and EU card payments. Strong Customer Authentication (SCA) regulations require 3DS for the majority of online card transactions. Setting apply3DSecure to 'UseMSPSetting' uses your merchant account configuration. Transactions without 3DS on regulated cards will be declined by the issuing bank. Ensure your Replit server has a publicly accessible callback URL for 3DS authentication.

### What is the difference between sandbox and live Opayo credentials?

Sandbox credentials use the endpoint https://pi-test.sagepay.com/api/v1 and do not process real money. Live credentials use https://pi.sagepay.com/api/v1 and charge real cards. The credentials (integration key and password) are different for each environment. Store the environment name in Replit Secrets as OPAYO_ENVIRONMENT and switch its value from 'sandbox' to 'live' when you are ready to go live.

### Can I use Opayo for recurring payments in Replit?

Yes. Opayo supports reusable card tokens (stored against a customer) and continuous authority transactions for subscriptions. On the initial payment, include saveCard=true in the transaction request to receive a card token. Use this token for subsequent charges with transactionType set to 'Repeat'. Token-based charging does not require 3DS for MIT (merchant-initiated transactions) in most cases.

### Why does my Opayo integration work in sandbox but fail in production?

The most common cause is using sandbox credentials with the live endpoint URL or vice versa. Verify OPAYO_ENVIRONMENT is set to 'live' and that OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD have been updated to your live credentials. Also check that your live merchant account is fully activated — Opayo requires completing merchant onboarding before live transactions are permitted.

---

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