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
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
Get Opayo Sandbox Credentials
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.
Store Credentials in Replit Secrets
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.
Create a Merchant Session Key and Process Payment (Python)
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.
1import os2import base643import requests4from flask import Flask, request, jsonify, redirect56app = Flask(__name__)78INTEGRATION_KEY = os.environ["OPAYO_INTEGRATION_KEY"]9INTEGRATION_PASSWORD = os.environ["OPAYO_INTEGRATION_PASSWORD"]10ENVIRONMENT = os.environ.get("OPAYO_ENVIRONMENT", "sandbox")1112BASE_URL = (13 "https://pi-test.sagepay.com/api/v1"14 if ENVIRONMENT == "sandbox"15 else "https://pi.sagepay.com/api/v1"16)171819def 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}"242526HEADERS = {27 "Authorization": get_auth_header(),28 "Content-Type": "application/json",29 "Cache-Control": "no-cache"30}313233@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=HEADERS40 )41 resp.raise_for_status()42 return jsonify(resp.json())434445@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.0051 order_ref = data.get("orderRef", "ORDER001")5253 transaction_payload = {54 "transactionType": "Payment",55 "paymentMethod": {56 "card": {57 "merchantSessionKey": data.get("merchantSessionKey"),58 "cardIdentifier": card_identifier59 }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 }7980 resp = requests.post(81 f"{BASE_URL}/transactions",82 json=transaction_payload,83 headers=HEADERS84 )8586 result = resp.json()8788 # Check if 3DS challenge is required89 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 })9899 return jsonify(result)100101102@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")107108 resp = requests.post(109 f"{BASE_URL}/transactions/{transaction_id}/3d-secure-challenge",110 json={"cRes": cres},111 headers=HEADERS112 )113 result = resp.json()114 status = result.get("status", "")115116 if status == "Ok":117 return redirect("/payment-success")118 else:119 return redirect(f"/payment-failed?status={status}")120121122@app.route("/payment-success")123def payment_success():124 return jsonify({"message": "Payment completed successfully"})125126127@app.route("/payment-failed")128def payment_failed():129 status = request.args.get("status", "Failed")130 return jsonify({"message": f"Payment failed: {status}"}), 400131132133if __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).
Build a Node.js Opayo Payment Server
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.
1const express = require('express');2const axios = require('axios');34const app = express();5app.use(express.json());6app.use(express.urlencoded({ extended: true })); // for 3DS form POST78const 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';1213const BASE_URL = ENVIRONMENT === 'sandbox'14 ? 'https://pi-test.sagepay.com/api/v1'15 : 'https://pi.sagepay.com/api/v1';1617const 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};2324// Create merchant session key25app.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});3738// Create payment transaction39app.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;4546 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 country63 },64 strongCustomerAuthentication: {65 website: req.body.returnUrl || '',66 notificationURL: `${req.protocol}://${req.get('host')}/3ds-callback`67 }68 };6970 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});7778// Handle 3D Secure callback79app.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});9596app.get('/payment-success', (req, res) => {97 res.json({ message: 'Payment completed successfully' });98});99100app.get('/payment-failed', (req, res) => {101 res.status(400).json({ message: `Payment failed: ${req.query.status || 'Unknown'}` });102});103104app.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.
Deploy on Reserved VM for Payment Reliability
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.
1[[ports]]2internalPort = 30003externalPort = 8045[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.
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.
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.
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.
1import base642key = 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 charsTransaction 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.
1# Use the public deployment URL, not the preview URL2notification_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.
1import uuid2vendor_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
Worldpay is an enterprise-grade global payment processor with multi-currency and multi-acquirer support, better suited to large UK retailers processing high transaction volumes internationally.
Braintree (PayPal) includes native PayPal and Venmo payment methods alongside card processing, making it a better fit if your UK customers frequently pay with PayPal.
Stripe has more comprehensive developer documentation and a simpler integration path, making it a better choice for developers unfamiliar with the UK payment ecosystem.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation