Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with Sage Pay (Opayo)

Integrating Sage Pay (now Opayo) with Lovable uses Edge Functions to create payment sessions and verify 3-D Secure transactions. Store your Integration Key and Integration Password in Cloud Secrets, create an Edge Function to generate merchant session keys, use Opayo's Drop-in UI for card tokenization, then process the payment server-side. Setup takes 35 minutes.

What you'll learn

  • How to create merchant session keys via the Opayo API for Drop-in UI initialization
  • How to embed Opayo's Drop-in card collection UI in a React component
  • How to process a card transaction using a card identifier from the Drop-in
  • How to handle 3-D Secure authentication redirects in Lovable
  • How to use Opayo's sandbox environment with test cards
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read35 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

Integrating Sage Pay (now Opayo) with Lovable uses Edge Functions to create payment sessions and verify 3-D Secure transactions. Store your Integration Key and Integration Password in Cloud Secrets, create an Edge Function to generate merchant session keys, use Opayo's Drop-in UI for card tokenization, then process the payment server-side. Setup takes 35 minutes.

Why integrate Opayo (Sage Pay) with Lovable?

Opayo, formerly known as Sage Pay, is the UK's most widely used payment gateway among established British businesses. With over 50,000 UK merchants and tight integration with UK high-street banks like Barclays, HSBC, and NatWest, Opayo is often the required payment gateway when building apps for UK-based clients with existing merchant agreements. Many UK companies have had their Sage Pay/Opayo accounts for years and prefer not to migrate to Stripe, even when building new digital products.

For Lovable developers, Opayo matters most when your client is a UK business that already has an Opayo merchant account, when your client's UK bank requires a specific gateway for their merchant facility, or when you need to accept UK-specific payment methods and comply with the UK's payment regulations. Opayo has been FCA-regulated and handles UK-specific requirements like the Strong Customer Authentication (SCA) mandate that came into force across Europe in 2021, which requires 3-D Secure for online payments.

The integration follows a three-step pattern: generate a merchant session key (a short-lived token for the frontend), use Opayo's Drop-in UI to collect and tokenize card data into a card identifier, then call the Opayo Transaction API server-side with the card identifier to process the payment. This keeps card data off your server while handling the 3DS2 challenge flow that UK regulations require.

Integration method

Edge Function Integration

Opayo (formerly Sage Pay) has no native Lovable connector. Integration requires a Supabase Edge Function to generate merchant session keys, which are passed to Opayo's Drop-in UI for PCI-compliant card data collection. A second Edge Function then processes the payment transaction using the card identifier returned by the Drop-in. Integration Key and Password are stored in Cloud Secrets.

Prerequisites

  • A Lovable project with Cloud enabled
  • An Opayo test account — register at www.opayo.co.uk/support/12/36/test-card-details-for-your-test-transactions
  • Your Opayo Integration Key and Integration Password from MySagePay portal → Settings → Integration Keys
  • Your Opayo Vendor Name (shown in the MySagePay dashboard header)
  • Basic understanding of 3-D Secure authentication and what a merchant session key is

Step-by-step guide

1

Get Opayo credentials from the MySagePay portal

Access the Opayo test environment by registering at test.opayo.eu.elavon.com. Your test credentials are separate from production — the test environment uses a specific set of test card numbers and simulates payment outcomes based on amount. Once logged in, navigate to Settings → Integration Keys (in the left sidebar under 'Manage'). You'll see your Integration Key (a long UUID-style string) and Integration Password. These two credentials authenticate your server-side API calls. Also note your Vendor Name — this is the merchant identifier for your Opayo account, typically a lowercase string like 'yourcompanytest' in the test environment. In your Lovable project, open the Cloud tab → Secrets and add four secrets: OPAYO_INTEGRATION_KEY (your Integration Key), OPAYO_INTEGRATION_PASSWORD (your Integration Password), OPAYO_VENDOR_NAME (your vendor name), and OPAYO_ENVIRONMENT set to either 'test' or 'live'. The API base URL changes based on environment: test uses sandbox.opayo.eu.elavon.com, live uses live.opayo.eu.elavon.com.

Pro tip: Opayo test and live environments have completely separate Integration Keys. Do not use your live keys for testing — keep both sets in Cloud Secrets with environment-specific naming if you need both.

Expected result: OPAYO_INTEGRATION_KEY, OPAYO_INTEGRATION_PASSWORD, OPAYO_VENDOR_NAME, and OPAYO_ENVIRONMENT stored in Cloud Secrets.

2

Create an Edge Function to generate merchant session keys

The Opayo Drop-in UI requires a short-lived merchant session key (MSK) to initialize. This key identifies your merchant account to the Opayo tokenization service and expires after 400 seconds. You must generate a new MSK for each checkout session — they cannot be reused. In Lovable's Code panel, create supabase/functions/opayo-session-key/index.ts. This function calls POST https://pi-test.sagepay.com/api/v1/merchant-session-keys (test) or POST https://pi.sagepay.com/api/v1/merchant-session-keys (live) with HTTP Basic Auth using your Integration Key as the username and Integration Password as the password. The request body is a JSON object with your vendorName. The response contains a merchantSessionKey and an expiry timestamp. The MSK is safe to return to the browser — it's designed to be a public-facing token for the Drop-in UI. It cannot be used to process transactions on its own. Your frontend calls this Edge Function when the checkout page loads to get a fresh MSK, then passes it to the Opayo Drop-in initialization. Generate a new one if the user is on the checkout page for more than 6 minutes.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/opayo-session-key/index.ts. Call the Opayo merchant session keys endpoint using Basic Auth with OPAYO_INTEGRATION_KEY as username and OPAYO_INTEGRATION_PASSWORD as password from Deno.env.get(). Use the test URL https://pi-test.sagepay.com/api/v1/merchant-session-keys. Send the vendorName from OPAYO_VENDOR_NAME in the request body. Return the merchantSessionKey and expiry from the response. Include CORS headers.

Paste this in Lovable chat

supabase/functions/opayo-session-key/index.ts
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
2
3const INTEGRATION_KEY = Deno.env.get("OPAYO_INTEGRATION_KEY") ?? "";
4const INTEGRATION_PASSWORD = Deno.env.get("OPAYO_INTEGRATION_PASSWORD") ?? "";
5const VENDOR_NAME = Deno.env.get("OPAYO_VENDOR_NAME") ?? "";
6const ENVIRONMENT = Deno.env.get("OPAYO_ENVIRONMENT") ?? "test";
7
8const BASE_URL = ENVIRONMENT === "live"
9 ? "https://pi.sagepay.com/api/v1"
10 : "https://pi-test.sagepay.com/api/v1";
11
12const corsHeaders = {
13 "Access-Control-Allow-Origin": "*",
14 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
15};
16
17serve(async (req) => {
18 if (req.method === "OPTIONS") {
19 return new Response("ok", { headers: corsHeaders });
20 }
21
22 try {
23 const authString = btoa(`${INTEGRATION_KEY}:${INTEGRATION_PASSWORD}`);
24
25 const res = await fetch(`${BASE_URL}/merchant-session-keys`, {
26 method: "POST",
27 headers: {
28 "Authorization": `Basic ${authString}`,
29 "Content-Type": "application/json",
30 },
31 body: JSON.stringify({ vendorName: VENDOR_NAME }),
32 });
33
34 if (!res.ok) {
35 const err = await res.text();
36 throw new Error(`Opayo error: ${res.status} ${err}`);
37 }
38
39 const data = await res.json();
40
41 return new Response(
42 JSON.stringify({
43 merchantSessionKey: data.merchantSessionKey,
44 expiry: data.expiry,
45 }),
46 { headers: { ...corsHeaders, "Content-Type": "application/json" } }
47 );
48 } catch (e) {
49 return new Response(
50 JSON.stringify({ error: (e as Error).message }),
51 { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
52 );
53 }
54});

Pro tip: Merchant session keys expire after 400 seconds. Generate a new key when the checkout page loads. If the user spends more than 6 minutes on the payment page, call this function again to refresh the key before the Drop-in submits.

Expected result: Edge Function returning a merchantSessionKey UUID and expiry timestamp when called.

3

Embed the Opayo Drop-in UI and process the payment

Opayo's Drop-in UI (formerly called the Pi.js library) collects card details and tokenizes them into a card identifier. Load the Opayo JavaScript library from their CDN in your index.html. For test: https://pi-test.sagepay.com/api/v1/js/sagepay.js. For live: https://pi.sagepay.com/api/v1/js/sagepay.js. In your React component, call your opayo-session-key Edge Function on mount to get a fresh MSK, then initialize the Opayo Drop-in with sagepayCheckout({ merchantSessionKey: msk, onTokenise: handleTokenise }). The onTokenise callback fires with a cardIdentifier when the customer enters their card. Pass this cardIdentifier to your payment processing Edge Function. The payment processing Edge Function calls POST https://pi-test.sagepay.com/api/v1/transactions with Basic Auth. The request body includes vendorName, vendorTxCode (your unique order ID), amount in pennies (£10.00 = 1000), currency (GBP), transactionType (Payment), cardDetails with the cardIdentifier and merchantSessionKey, and billingAddress. The response includes a status field — 'Ok' means authorized, '3DAuth' means 3DS challenge is required (you must redirect the user to the 3DSUrl). Handle both outcomes in your frontend.

Lovable Prompt

Create an Opayo payment component. Load the Opayo test JS from CDN in index.html. In the component: 1) on mount, call opayo-session-key Edge Function to get a merchantSessionKey, 2) initialize window.sagepayCheckout with the key and an onTokenise callback, 3) render a div with id 'sp-container' where the Drop-in will mount, 4) in onTokenise, send the cardIdentifier and merchantSessionKey to an opayo-process-payment Edge Function with the order amount in pennies and a unique vendorTxCode, 5) handle status 'Ok' (show success), '3DAuth' (redirect to the 3DSUrl), and error states. Create the opayo-process-payment Edge Function that calls the Opayo Transactions API with Basic Auth.

Paste this in Lovable chat

supabase/functions/opayo-process-payment/index.ts
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
2import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
3
4const INTEGRATION_KEY = Deno.env.get("OPAYO_INTEGRATION_KEY") ?? "";
5const INTEGRATION_PASSWORD = Deno.env.get("OPAYO_INTEGRATION_PASSWORD") ?? "";
6const VENDOR_NAME = Deno.env.get("OPAYO_VENDOR_NAME") ?? "";
7const ENVIRONMENT = Deno.env.get("OPAYO_ENVIRONMENT") ?? "test";
8const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
9const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
10
11const BASE_URL = ENVIRONMENT === "live"
12 ? "https://pi.sagepay.com/api/v1"
13 : "https://pi-test.sagepay.com/api/v1";
14
15const corsHeaders = {
16 "Access-Control-Allow-Origin": "*",
17 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
18};
19
20serve(async (req) => {
21 if (req.method === "OPTIONS") {
22 return new Response("ok", { headers: corsHeaders });
23 }
24
25 try {
26 const { cardIdentifier, merchantSessionKey, amountInPennies, vendorTxCode, billingAddress } = await req.json();
27 const authString = btoa(`${INTEGRATION_KEY}:${INTEGRATION_PASSWORD}`);
28
29 const payload = {
30 transactionType: "Payment",
31 paymentMethod: {
32 card: {
33 merchantSessionKey,
34 cardIdentifier,
35 save: false,
36 },
37 },
38 vendorTxCode,
39 amount: amountInPennies,
40 currency: "GBP",
41 description: "Order payment",
42 apply3DSecure: "UseMSPSetting",
43 customerFirstName: billingAddress?.firstName ?? "Customer",
44 customerLastName: billingAddress?.lastName ?? "Name",
45 billingAddress: billingAddress ?? {
46 address1: "88",
47 city: "London",
48 postalCode: "EC1A1BB",
49 country: "GB",
50 },
51 vendorName: VENDOR_NAME,
52 };
53
54 const res = await fetch(`${BASE_URL}/transactions`, {
55 method: "POST",
56 headers: { "Authorization": `Basic ${authString}`, "Content-Type": "application/json" },
57 body: JSON.stringify(payload),
58 });
59
60 const data = await res.json();
61
62 if (data.status === "Ok") {
63 const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
64 await supabase.from("orders").update({ status: "paid", opayo_tx_id: data.transactionId }).eq("id", vendorTxCode);
65 return new Response(JSON.stringify({ success: true, transactionId: data.transactionId }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
66 } else if (data.status === "3DAuth") {
67 return new Response(JSON.stringify({ requires3DS: true, acsUrl: data.acsUrl, paReq: data.paReq, transactionId: data.transactionId }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
68 } else {
69 return new Response(JSON.stringify({ success: false, error: data.statusDetail ?? "Payment declined" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } });
70 }
71 } catch (e) {
72 return new Response(JSON.stringify({ error: (e as Error).message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
73 }
74});

Pro tip: Amount must be in pennies for GBP — £19.99 = 1999, not 19.99. The currency field must be 'GBP' for UK payments. Opayo also supports EUR and USD but GBP is the default for UK merchant accounts.

Expected result: Opayo Drop-in UI renders on the checkout page. Test payment with Opayo test card 4929 0000 0000 6 (Visa, use expiry 0325 and CVV 123) processes successfully and order updates in Supabase.

4

Handle 3-D Secure authentication responses

UK's Strong Customer Authentication (SCA) regulation requires most card-present-equivalent online transactions to go through 3-D Secure (3DS). When Opayo returns a status of '3DAuth' instead of 'Ok', you must redirect the customer to their bank's authentication page to approve the payment. After authentication, the bank redirects back to your app with a PARes (Payer Authentication Response). When your process-payment Edge Function returns requires3DS: true, redirect the customer to the acsUrl returned in the response. You need to POST the paReq and a TermUrl (where the bank should redirect back to) to that acsUrl. This is typically done by rendering a hidden HTML form and submitting it automatically with JavaScript. After the customer authenticates, the bank sends a POST to your TermUrl with a PARes parameter. Your TermUrl should be a route in your Lovable app (e.g., /payment/3ds-complete) that reads the PARes from the POST body. Create an Edge Function that completes the 3DS transaction by calling the Opayo 3DS endpoint with the original transactionId and the PARes. If the 3DS authentication is successful, Opayo confirms the transaction. This two-step process is required for compliance — skipping it will result in liability shift for fraudulent transactions.

Pro tip: For testing 3DS, Opayo provides specific test card numbers that always trigger 3DS challenges: 4929 0000 0000 6 with amount above £30 often triggers 3DS in the test environment. Use the Opayo test documentation for current 3DS test scenarios.

Expected result: 3-D Secure flow working end-to-end: 3DS-required test payments redirect to the Opayo test 3DS page, authentication completes, and the transaction is confirmed.

Common use cases

UK e-commerce checkout for an existing Opayo merchant

A UK retailer with an existing Opayo merchant account launches a new Lovable-built online store. The checkout uses Opayo's Drop-in UI so customers can pay by Visa, Mastercard, or Amex. 3-D Secure is handled automatically via the Drop-in's built-in 3DS2 flow. The Edge Function processes the transaction and stores the Opayo transaction ID for order confirmation and potential refunds.

Lovable Prompt

Add an Opayo payment checkout to the UK store. Create an Edge Function to generate a merchant session key from the Opayo API using the Integration Key and Password. Pass that session key to the Opayo Drop-in UI embedded on the checkout page. When the customer completes card entry, the Drop-in returns a cardIdentifier. Send that to a second Edge Function that calls the Opayo Transactions API to create a Payment transaction with the amount in pennies, GBP currency, and the cardIdentifier. Handle the 3DS2 redirect if required. On success, mark the order as paid in Supabase.

Copy this prompt to try it in Lovable

Recurring billing for UK subscription services

A UK SaaS platform needs to bill subscribers monthly using stored card details. During initial checkout, the Opayo transaction response includes a token for the card. The Edge Function stores this token in Supabase and uses it for subsequent monthly charges via the Opayo Repeat Payments API, avoiding the need for customers to re-enter their card each month.

Lovable Prompt

Implement monthly subscription billing using Opayo. During the first payment, set the tokenise flag in the Opayo transaction to get back a cardToken. Store this token in the Supabase subscriptions table alongside the Opayo vendorTxCode. Create a scheduled Edge Function that runs monthly to charge each active subscriber using the stored cardToken via the Opayo Repeat Payments endpoint. On successful charge, update the next_billing_date in Supabase. Send a payment receipt email via Edge Function.

Copy this prompt to try it in Lovable

Phone order payment processing

A UK call center app lets agents take card payments over the phone by entering card details on behalf of the customer. The agent enters the card details into a form, the Edge Function calls the Opayo API with the card data directly (MOTO transaction type), and the payment is processed without requiring the customer to enter details themselves. MOTO transactions bypass 3DS as the customer isn't present at a screen.

Lovable Prompt

Build a phone order payment screen for call center agents. Create a form where agents enter the customer's card number, expiry, and CVV along with the order amount and a unique order reference. Submit this to an Edge Function that calls the Opayo Transaction API with transactionType 'Payment', entryMethod 'TelephoneOrder', and the card details directly in the cardDetails object. Return the VPS transaction ID on success. Log the transaction in Supabase with agentId, orderId, and transaction status.

Copy this prompt to try it in Lovable

Troubleshooting

Merchant session key request returns 401 Unauthorized

Cause: Basic Auth credentials are encoded incorrectly, or the Integration Key and Password are from the wrong environment (test vs live). Opayo test and live environments have completely separate credentials.

Solution: Verify that OPAYO_ENVIRONMENT in Cloud Secrets matches the credentials you're using. Test credentials only work against pi-test.sagepay.com. Live credentials only work against pi.sagepay.com. In your Edge Function, confirm btoa() is correctly encoding the 'key:password' string. Log the first few characters of the auth string to verify format.

typescript
1// Verify auth string format:
2const authString = btoa(`${INTEGRATION_KEY}:${INTEGRATION_PASSWORD}`);
3console.log('Auth prefix:', authString.substring(0, 20)); // Should start with base64 of your key

Drop-in UI container is blank or window.sagepayCheckout is undefined

Cause: The Opayo sagepay.js script has not loaded, the sp-container div doesn't exist in the DOM when sagepayCheckout is called, or the merchantSessionKey has expired (keys expire after 400 seconds).

Solution: Add the Opayo test JS CDN to index.html head section: <script src='https://pi-test.sagepay.com/api/v1/js/sagepay.js'></script>. In your React component, use a useEffect with a dependency on the merchantSessionKey so initialization only runs after the key is fetched. Generate a fresh key if the user has been on the page for more than 6 minutes.

Transaction returns status 'Rejected' with statusDetail 'Authentication Unsuccessful'

Cause: 3-D Secure authentication failed — the customer's bank declined the 3DS challenge, or the test card requires a specific 3DS test password that wasn't entered correctly.

Solution: In Opayo's test environment, 3DS test cards use the password '3DSecurePassw0rd!' (with a zero). If testing manually, enter this password when prompted. In production, this error means the cardholder failed authentication at their bank. Show the customer a message to contact their bank or try a different card.

API returns 422 'Unprocessable Entity' on transaction creation

Cause: The request body is missing required fields, or the amount is invalid. Common causes: amount is a decimal (Opayo requires whole pennies), currency is missing, or billingAddress fields are invalid.

Solution: Check that amount is an integer number of pennies (not a float). Verify billingAddress has the required fields: address1, city, postalCode (UK postcode format), and country (ISO 2-letter code 'GB'). The vendorTxCode must be unique — if you're retrying a failed transaction, generate a new vendorTxCode rather than reusing the original.

typescript
1// Ensure amount is integer pennies:
2const amountInPennies = Math.round(orderAmountInPounds * 100);
3// e.g., 19.99 becomes 1999

Best practices

  • Always generate a fresh merchant session key when the checkout page loads — never cache or reuse session keys, as they expire after 400 seconds and reuse causes 'already used' errors
  • Store OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD in Cloud Secrets and never expose them in frontend code — the MSK returned by your Edge Function is the only credential that touches the browser
  • Implement idempotency in your payment Edge Function by generating a unique vendorTxCode per order (e.g., include timestamp and order ID) — Opayo returns an error if you submit two transactions with the same vendorTxCode
  • Always handle the '3DAuth' response from Opayo — with SCA regulations, skipping 3DS for UK payments means your business bears 100% liability for fraudulent chargebacks
  • Convert amounts to pennies (multiply by 100) before sending to Opayo — the API rejects decimal amounts and errors are not always descriptive about the cause
  • Keep test and live Opayo credentials completely separate in Cloud Secrets — use OPAYO_ENVIRONMENT variable to switch between endpoints, making it easy to deploy to production without changing code
  • For complex UK payment configurations involving subscription billing, MOTO payments, or multi-currency support, RapidDev's team can help navigate Opayo's requirements

Alternatives

Frequently asked questions

Is Sage Pay and Opayo the same company?

Yes. Sage Pay was the original brand name for the payment gateway founded in 2001. In 2019, the company rebranded to Opayo. In 2023, Elavon (owned by US Bancorp) acquired Opayo. The API and developer documentation still uses both names interchangeably. Existing Sage Pay merchant accounts continued operating as Opayo accounts. The technical integration is the same regardless of which name your client uses.

What test cards work with Opayo's sandbox?

Opayo provides several test card numbers. For a successful payment: 4929 0000 0000 6 (Visa, use expiry 0325, CVV 123). For a declined payment: 4929 0000 0000 7. For a 3-D Secure challenge: amounts over certain thresholds on these test cards can trigger 3DS. When prompted during 3DS, enter the password '3DSecurePassw0rd!' (with a zero instead of the letter o). Full test card details are in Opayo's developer documentation.

Do I need to handle Strong Customer Authentication for all UK payments?

For most online UK card payments, yes — SCA (Strong Customer Authentication) has been mandatory in the UK since March 2022. Opayo handles this via 3-D Secure 2 (3DS2). Exemptions exist for transactions under £30, recurring transactions after the first payment, and merchant-initiated transactions. When your Opayo transaction response returns '3DAuth', always redirect to the 3DS challenge unless you have a specific exemption claim.

Can I process refunds through the Opayo API?

Yes. Use the Transactions API with transactionType 'Refund' and include the referenceTransactionId pointing to the original transaction's transactionId. The refund amount can be partial (less than original) or full. Refunds are processed as separate transactions and appear as new transaction records in your MySagePay dashboard. Store the original Opayo transactionId in Supabase when charging so you can reference it for refunds.

How do I switch from Opayo test to production?

Update OPAYO_ENVIRONMENT in Cloud Secrets from 'test' to 'live'. Update OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD with your live credentials from the production MySagePay portal. Update your OPAYO_VENDOR_NAME to your production vendor name (often different from the test name). Change the Opayo JS CDN in index.html from pi-test.sagepay.com to pi.sagepay.com. Your Edge Function already handles the URL switching via the OPAYO_ENVIRONMENT variable.

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.