Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Sage Pay

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.

What you'll learn

  • How to register for Opayo developer sandbox credentials and understand the test environment
  • How to store Opayo integration key and password in Replit Secrets
  • How to create a merchant session key and card identifier for PCI-compliant payment tokenization
  • How to submit a card payment transaction with 3D Secure 2.0 authentication from Python and Node.js
  • How to handle 3D Secure challenge flows and process payment callbacks in your Replit backend
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read60 minutesPaymentMarch 2026RapidDev Engineering Team
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.

Integration method

Standard API Integration

You connect Replit to Opayo by registering a developer account at developer.elavon.com or opayo.co.uk, obtaining an integration key and integration password, and storing them in Replit Secrets. Your server-side Python or Node.js code authenticates using HTTP Basic auth (base64-encoded key:password) and calls the Opayo Pi REST API to create payment sessions, process card transactions, and handle 3D Secure authentication flows. The 3DS challenge redirect and callback must be handled server-side, making a backend in Replit essential.

Prerequisites

  • 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.

Pro tip: Opayo sandbox credentials are free. Apply for a sandbox account before your live merchant account so you can test the full payment flow including 3D Secure before charging real customers.

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.

Pro tip: Add OPAYO_ENVIRONMENT as a separate secret so you can toggle between sandbox and live without changing any code β€” just update the secret value.

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.

app.py
1import os
2import base64
3import requests
4from flask import Flask, request, jsonify, redirect
5
6app = Flask(__name__)
7
8INTEGRATION_KEY = os.environ["OPAYO_INTEGRATION_KEY"]
9INTEGRATION_PASSWORD = os.environ["OPAYO_INTEGRATION_PASSWORD"]
10ENVIRONMENT = os.environ.get("OPAYO_ENVIRONMENT", "sandbox")
11
12BASE_URL = (
13 "https://pi-test.sagepay.com/api/v1"
14 if ENVIRONMENT == "sandbox"
15 else "https://pi.sagepay.com/api/v1"
16)
17
18
19def get_auth_header() -> str:
20 """Create Basic auth header from integration key and password."""
21 credentials = f"{INTEGRATION_KEY}:{INTEGRATION_PASSWORD}"
22 encoded = base64.b64encode(credentials.encode()).decode()
23 return f"Basic {encoded}"
24
25
26HEADERS = {
27 "Authorization": get_auth_header(),
28 "Content-Type": "application/json",
29 "Cache-Control": "no-cache"
30}
31
32
33@app.route("/session-key", methods=["POST"])
34def create_session_key():
35 """Create a merchant session key for frontend card tokenisation."""
36 resp = requests.post(
37 f"{BASE_URL}/merchant-session-keys",
38 json={"vendorName": os.environ.get("OPAYO_VENDOR_NAME", "sandbox")},
39 headers=HEADERS
40 )
41 resp.raise_for_status()
42 return jsonify(resp.json())
43
44
45@app.route("/transaction", methods=["POST"])
46def create_transaction():
47 """Submit a card payment using a card identifier from the frontend."""
48 data = request.get_json()
49 card_identifier = data.get("cardIdentifier")
50 amount = data.get("amount") # in pence, e.g. 1000 = Β£10.00
51 order_ref = data.get("orderRef", "ORDER001")
52
53 transaction_payload = {
54 "transactionType": "Payment",
55 "paymentMethod": {
56 "card": {
57 "merchantSessionKey": data.get("merchantSessionKey"),
58 "cardIdentifier": card_identifier
59 }
60 },
61 "vendorTxCode": order_ref,
62 "amount": amount,
63 "currency": "GBP",
64 "description": data.get("description", "Online payment"),
65 "apply3DSecure": "UseMSPSetting",
66 "customerFirstName": data.get("firstName", ""),
67 "customerLastName": data.get("lastName", ""),
68 "billingAddress": {
69 "address1": data.get("address1", ""),
70 "city": data.get("city", ""),
71 "postalCode": data.get("postalCode", ""),
72 "country": data.get("country", "GB")
73 },
74 "strongCustomerAuthentication": {
75 "website": data.get("returnUrl", ""),
76 "notificationURL": f"{request.host_url}3ds-callback"
77 }
78 }
79
80 resp = requests.post(
81 f"{BASE_URL}/transactions",
82 json=transaction_payload,
83 headers=HEADERS
84 )
85
86 result = resp.json()
87
88 # Check if 3DS challenge is required
89 if result.get("status") == "3DAuth":
90 return jsonify({
91 "status": "3DAuth",
92 "acsUrl": result.get("acsUrl"),
93 "acsTransId": result.get("acsTransId"),
94 "dsTransId": result.get("dsTransId"),
95 "cReq": result.get("cReq"),
96 "transactionId": result.get("transactionId")
97 })
98
99 return jsonify(result)
100
101
102@app.route("/3ds-callback", methods=["POST"])
103def handle_3ds_callback():
104 """Handle 3D Secure authentication callback from Opayo."""
105 cres = request.form.get("cres")
106 transaction_id = request.form.get("transactionId")
107
108 resp = requests.post(
109 f"{BASE_URL}/transactions/{transaction_id}/3d-secure-challenge",
110 json={"cRes": cres},
111 headers=HEADERS
112 )
113 result = resp.json()
114 status = result.get("status", "")
115
116 if status == "Ok":
117 return redirect("/payment-success")
118 else:
119 return redirect(f"/payment-failed?status={status}")
120
121
122@app.route("/payment-success")
123def payment_success():
124 return jsonify({"message": "Payment completed successfully"})
125
126
127@app.route("/payment-failed")
128def payment_failed():
129 status = request.args.get("status", "Failed")
130 return jsonify({"message": f"Payment failed: {status}"}), 400
131
132
133if __name__ == "__main__":
134 app.run(host="0.0.0.0", port=3000, debug=False)

Pro tip: Amounts in Opayo are always in the smallest currency unit β€” pence for GBP. Β£10.00 = 1000. Always validate and sanitize the amount on the server side before submitting to Opayo.

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.

server.js
1const express = require('express');
2const axios = require('axios');
3
4const app = express();
5app.use(express.json());
6app.use(express.urlencoded({ extended: true })); // for 3DS form POST
7
8const INTEGRATION_KEY = process.env.OPAYO_INTEGRATION_KEY;
9const INTEGRATION_PASSWORD = process.env.OPAYO_INTEGRATION_PASSWORD;
10const ENVIRONMENT = process.env.OPAYO_ENVIRONMENT || 'sandbox';
11const VENDOR_NAME = process.env.OPAYO_VENDOR_NAME || 'sandbox';
12
13const BASE_URL = ENVIRONMENT === 'sandbox'
14 ? 'https://pi-test.sagepay.com/api/v1'
15 : 'https://pi.sagepay.com/api/v1';
16
17const AUTH = Buffer.from(`${INTEGRATION_KEY}:${INTEGRATION_PASSWORD}`).toString('base64');
18const HEADERS = {
19 Authorization: `Basic ${AUTH}`,
20 'Content-Type': 'application/json',
21 'Cache-Control': 'no-cache'
22};
23
24// Create merchant session key
25app.post('/session-key', async (req, res) => {
26 try {
27 const { data } = await axios.post(
28 `${BASE_URL}/merchant-session-keys`,
29 { vendorName: VENDOR_NAME },
30 { headers: HEADERS }
31 );
32 res.json(data);
33 } catch (err) {
34 res.status(err.response?.status || 500).json(err.response?.data || { error: err.message });
35 }
36});
37
38// Create payment transaction
39app.post('/transaction', async (req, res) => {
40 const {
41 cardIdentifier, merchantSessionKey, amount, orderRef,
42 description, firstName, lastName,
43 address1, city, postalCode, country = 'GB'
44 } = req.body;
45
46 const payload = {
47 transactionType: 'Payment',
48 paymentMethod: {
49 card: { merchantSessionKey, cardIdentifier }
50 },
51 vendorTxCode: orderRef || `ORDER-${Date.now()}`,
52 amount: Number(amount),
53 currency: 'GBP',
54 description: description || 'Online payment',
55 apply3DSecure: 'UseMSPSetting',
56 customerFirstName: firstName || '',
57 customerLastName: lastName || '',
58 billingAddress: {
59 address1: address1 || '',
60 city: city || '',
61 postalCode: postalCode || '',
62 country
63 },
64 strongCustomerAuthentication: {
65 website: req.body.returnUrl || '',
66 notificationURL: `${req.protocol}://${req.get('host')}/3ds-callback`
67 }
68 };
69
70 try {
71 const { data } = await axios.post(`${BASE_URL}/transactions`, payload, { headers: HEADERS });
72 res.json(data);
73 } catch (err) {
74 res.status(err.response?.status || 500).json(err.response?.data || { error: err.message });
75 }
76});
77
78// Handle 3D Secure callback
79app.post('/3ds-callback', async (req, res) => {
80 const { cres, transactionId } = req.body;
81 try {
82 const { data } = await axios.post(
83 `${BASE_URL}/transactions/${transactionId}/3d-secure-challenge`,
84 { cRes: cres },
85 { headers: HEADERS }
86 );
87 if (data.status === 'Ok') {
88 return res.redirect('/payment-success');
89 }
90 res.redirect(`/payment-failed?status=${data.status}`);
91 } catch (err) {
92 res.redirect(`/payment-failed?status=error`);
93 }
94});
95
96app.get('/payment-success', (req, res) => {
97 res.json({ message: 'Payment completed successfully' });
98});
99
100app.get('/payment-failed', (req, res) => {
101 res.status(400).json({ message: `Payment failed: ${req.query.status || 'Unknown'}` });
102});
103
104app.listen(3000, '0.0.0.0', () => {
105 console.log('Opayo payment server running on port 3000');
106});

Pro tip: The 3DS callback is a POST from Opayo's servers to your Replit URL β€” ensure your deployment URL is reachable from the internet. Use a Reserved VM deployment rather than Autoscale if you need the callback URL to be guaranteed-available 24/7.

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.

.replit
1[[ports]]
2internalPort = 3000
3externalPort = 80
4
5[deployment]
6run = ["node", "server.js"]
7deploymentTarget = "cloudrun"

Pro tip: Always test your 3DS callback URL before going live. Send a test transaction from the Opayo sandbox using a challenge-triggering test card and verify your /3ds-callback route processes the callback correctly.

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.

Common 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.

Replit Prompt

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.

Copy this prompt to try it in Replit

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.

Replit Prompt

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.

Copy this prompt to try it in Replit

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.

Replit Prompt

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.

Copy this prompt to try it in Replit

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.

typescript
1import base64
2key = os.environ['OPAYO_INTEGRATION_KEY']
3pwd = os.environ['OPAYO_INTEGRATION_PASSWORD']
4auth = base64.b64encode(f'{key}:{pwd}'.encode()).decode()
5print(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.

typescript
1# Use the public deployment URL, not the preview URL
2notification_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.

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

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.

Alternatives

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation β€” no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.