Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

Sage Pay

Connect Bubble to Sage Pay (now Opayo) using the API Connector with a pre-computed Base64 Basic Auth header. Bubble cannot evaluate btoa() at runtime in the API Connector header field — you must pre-compute Base64(integration_key:integration_password) externally and paste the resulting string as a Private header value. Opayo's PI API is individual-transaction-centric with no bulk list endpoint, so you must store transaction IDs in your Bubble database at creation time.

What you'll learn

  • Why Bubble cannot compute Base64 dynamically in the API Connector, and how to pre-compute it externally
  • How to obtain your Opayo Integration Key and Integration Password from My Sage Pay portal
  • How to configure the API Connector with a pre-computed Private Authorization header
  • Why Opayo's PI API has no transaction list endpoint and how to work around this in Bubble
  • How to generate unique vendorTxCodes for every payment to avoid 5009 duplicate errors
  • How to build payment creation and refund workflows in Bubble
  • How to set up a Backend Workflow for Opayo payment notification POSTs on paid Bubble plans
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate19 min read2–3 hoursPaymentsLast updated July 2026RapidDev Engineering Team
TL;DR

Connect Bubble to Sage Pay (now Opayo) using the API Connector with a pre-computed Base64 Basic Auth header. Bubble cannot evaluate btoa() at runtime in the API Connector header field — you must pre-compute Base64(integration_key:integration_password) externally and paste the resulting string as a Private header value. Opayo's PI API is individual-transaction-centric with no bulk list endpoint, so you must store transaction IDs in your Bubble database at creation time.

Quick facts about this guide
FactValue
ToolSage Pay (Opayo)
CategoryPayments
MethodBubble API Connector
DifficultyIntermediate
Time required2–3 hours
Last updatedJuly 2026

Sage Pay / Opayo + Bubble: Pre-Computed Base64 Auth and Transaction ID Storage

Sage Pay was one of the UK's most widely used payment gateways for over two decades. It rebranded as Opayo after being acquired, and is now owned by Elavon. If your business has been using 'Sage Pay' for years, you are using what is now called Opayo — and the API credentials and portal are the same system, just with a new name. In this tutorial, we refer to it as 'Opayo' but show both names where relevant.

For Bubble builders, two Opayo-specific issues come up in almost every integration:

**Issue 1: Pre-computed Base64 authentication.** Opayo's Basic Auth header is `Authorization: Basic {Base64(integration_key:integration_password)}`. Bubble's API Connector does not evaluate JavaScript expressions at runtime — you cannot type `Basic {btoa(key:password)}` and have Bubble compute it. Instead, you compute the Base64 string once, externally (in your browser console), and paste the result as the static value of a Private header. This is secure and correct — the key never leaves your server — but it means you must recompute and update the header whenever you rotate credentials.

**Issue 2: No transaction list endpoint.** Opayo's PI API is designed around individual transaction ID lookups. There is no 'list all transactions by date range' endpoint. To display a transaction history in a Bubble Repeating Group, you must store Opayo transaction IDs in a Bubble database type (Things) when payments are created, then look up each transaction by ID.

Both of these patterns are straightforward once you understand them — they just differ significantly from modern REST APIs like Stripe or Braintree.

Integration method

Bubble API Connector

Use the built-in API Connector (by Bubble) with a pre-computed Base64 Authorization header stored as a Private shared header. Pre-compute `Base64(integration_key:integration_password)` externally using your browser console, then paste the result into the API Connector's `Authorization: Basic {value}` Private header. All payment operations go to `/transactions` with a `transactionType` field in the body.

Prerequisites

  • An active Opayo (Sage Pay) merchant account. Log into My Sage Pay portal to access your Integration Key and Integration Password.
  • Your Opayo sandbox Integration Key and Integration Password from My Sage Pay portal → Settings → Integration. These are different from your Opayo login credentials.
  • A browser console (Chrome/Firefox/Safari developer tools) to pre-compute Base64 strings — no special software needed.
  • A Bubble app on a paid plan (Starter or above) if you need to receive Opayo payment notification POSTs via Backend Workflows.
  • A Bubble database type for Transactions (or Orders) with fields: transaction_id (text), vendor_tx_code (text), amount (number), status (text), created_at (date). Opayo transaction IDs must be stored at payment time for all subsequent lookups and refunds.

Step-by-step guide

1

Step 1: Get Your Opayo Credentials and Pre-Compute the Base64 Authorization String

Log into My Sage Pay portal (MySagePay.sagepay.com or myopayo.opayo.co.uk — both take you to the same portal). Navigate to Settings → Integration. You will find two values: - **Integration Key** (also called Vendor Name in some legacy Sage Pay contexts): a string like `hJYxsw7HLbj40cB8udES8CDRFLhuUskc` - **Integration Password**: a separate secret string You need both to construct the Authorization header. Now, pre-compute the Base64 string. Open your browser's developer console (F12 in Chrome, Cmd+Option+J on Mac). Type: ```javascript btoa('YOUR_INTEGRATION_KEY:YOUR_INTEGRATION_PASSWORD') ``` Press Enter. The console returns a Base64 string like `aEpZeHN3N0hMYmo0MGNCO...`. Copy this entire string — this is the value you will paste into Bubble's Authorization header. Do this twice: 1. Once for the sandbox Integration Key and Password → label the result 'Sandbox Base64' 2. Once for the live Integration Key and Password → label the result 'Live Base64' Store both in a secure location (a password manager or your team's secrets vault). You will need to recompute these strings if you ever rotate your Integration Password.

opayo_base64_precompute.txt
1// Run this in your browser console (F12 → Console tab)
2// Replace with your actual Integration Key and Password
3
4// Sandbox:
5btoa('YOUR_SANDBOX_INTEGRATION_KEY:YOUR_SANDBOX_INTEGRATION_PASSWORD')
6// Returns: 'WU9VUl9TQU5EQk9YX0lOVEVHUkFUSU9OX0tFWTpZT1VSX1NBTkRCT1hfUEFTU1dPUkQ='
7
8// Live:
9btoa('YOUR_LIVE_INTEGRATION_KEY:YOUR_LIVE_INTEGRATION_PASSWORD')
10// Returns: 'WU9VUl9MSVZFX0lOVEVHUkFUSU9OX0tFWTpZT1VSX0xJVkVfUEFTU1dPUkQ='
11
12// The Authorization header value to use in Bubble:
13// Authorization: Basic WU9VUl9TQU5EQk9YX0lOVEVHUkFUSU9OX0tFWTpZT1VSX1NBTkRCT1hfUEFTU1dPUkQ=

Pro tip: Test your Base64 computation by decoding it: `atob('YOUR_COMPUTED_BASE64')` should return `integration_key:integration_password`. If the decoded string looks correct, the Authorization header will work. Store both sandbox and live Base64 strings in your password manager — you cannot recover them without recomputing.

Expected result: You have two Base64 strings — one for sandbox, one for live — computed from your Opayo Integration Key and Password. Both are stored securely and ready to paste into Bubble's API Connector.

2

Step 2: Install the API Connector and Configure the Pre-Computed Authorization Header

In Bubble, go to Plugins tab → Add plugins → search 'API Connector' (by Bubble) → Install. Click 'API Connector' in the plugin list. Click 'Add another API'. Name it 'Opayo Sandbox'. Set the base URL to `https://pi-test.sagepay.com/api/v1`. Do NOT use Bubble's built-in 'Basic auth' authentication option — it would expect username and password fields that Bubble tries to Base64-encode at runtime, but Bubble's API Connector does not reliably do this for custom gateway auth patterns. Instead, use the manual header approach: In the Shared Headers section, click 'Add shared header': - Key: `Authorization` - Value: `Basic YOUR_SANDBOX_BASE64_STRING` (replace with your actual pre-computed sandbox Base64 string from Step 1) - Check the **'Private' checkbox** — this ensures the header value (which contains your credentials) is never sent to the browser Add two more shared headers: - `Content-Type: application/json` - `Cache-Control: no-cache` Name this API group 'Opayo Sandbox' (not just 'Opayo') so when you add the live group later, naming prevents accidental cross-environment calls.

opayo_api_connector_config.json
1{
2 "api_name": "Opayo Sandbox",
3 "base_url": "https://pi-test.sagepay.com/api/v1",
4 "shared_headers": {
5 "Authorization": "<private: 'Basic ' + YOUR_SANDBOX_BASE64_STRING>",
6 "Content-Type": "application/json",
7 "Cache-Control": "no-cache"
8 }
9}
10
11// Example Authorization header value:
12// Basic aEpZeHN3N0hMYmo0MGNCO... (your pre-computed sandbox Base64)
13
14// Live configuration (separate API group):
15{
16 "api_name": "Opayo Live",
17 "base_url": "https://pi.sagepay.com/api/v1",
18 "shared_headers": {
19 "Authorization": "<private: 'Basic ' + YOUR_LIVE_BASE64_STRING>",
20 "Content-Type": "application/json"
21 }
22}

Pro tip: If you ever rotate your Opayo Integration Password, you MUST return to this API Connector config and update the Authorization header value with the newly computed Base64 string. Bubble does not automatically recompute it — the stored value becomes stale and all API calls will fail with 401 until updated.

Expected result: The 'Opayo Sandbox' API group shows a Private Authorization header with your pre-computed Base64 string, confirming it will be sent server-side and never exposed to the browser.

3

Step 3: Add and Initialize the Transaction Lookup Call

Inside the 'Opayo Sandbox' API group, click 'Add a call'. Name it 'Get Transaction'. Set the method to GET and the path to `/transactions/<dynamic: transactionId>`. To initialize, you need a real Opayo transaction ID from a previous test payment. If you do not have one yet, you can: 1. Create a test payment first (Step 4) and come back to initialize this call with the returned ID 2. Ask your Opayo integration contact for a sample sandbox transaction ID With a valid sandbox transaction ID, click 'Initialize call'. The successful response includes: `transactionId`, `transactionType`, `status` ('Ok', 'NotAuthed', 'Rejected', 'Error'), `statusCode`, `statusDetail`, `retrievalReference`, `paymentMethod` (with masked card details), `amount` (in pence), `currency`. Set 'Use as' to 'Data'. Remember: Opayo's PI API does NOT have a 'list all transactions' endpoint. You cannot call an endpoint to retrieve all transactions for a date range. The only way to display a transaction history in Bubble is to store transaction IDs in your Bubble database as they are created, then look them up one by one (or display the locally-stored data without fetching from Opayo on every page load).

opayo_get_transaction_response.json
1{
2 "method": "GET",
3 "path": "/transactions/<dynamic: transactionId>",
4 "example_response": {
5 "transactionId": "T1234567890ABCDEF1234567890",
6 "transactionType": "Payment",
7 "status": "Ok",
8 "statusCode": "0000",
9 "statusDetail": "The Authorisation was Successful.",
10 "retrievalReference": 19876543,
11 "paymentMethod": {
12 "card": {
13 "cardType": "Visa",
14 "lastFourDigits": "0006",
15 "expiryDate": "0322",
16 "cardIdentifier": "ABC123-MASKED"
17 }
18 },
19 "amount": {
20 "totalAmount": 1999,
21 "saleTxAmount": 1999,
22 "surchargeAmount": 0
23 },
24 "currency": "GBP",
25 "bankAuthorisationCode": "999777"
26 }
27}

Pro tip: If Initialize returns 'There was an issue setting up your call', the most likely causes are: the Authorization header value is wrong (check the Base64 string has no extra spaces), the sandbox base URL has a typo, or the transaction ID used for initialization does not exist in the sandbox.

Expected result: Bubble shows 'Get Transaction' with green 'Initialized' status and detected fields including transactionId, status, statusCode, and amount.totalAmount.

4

Step 4: Build the Payment Creation Workflow

Add a second call to 'Opayo Sandbox'. Name it 'Create Payment'. Method: POST. Path: `/transactions`. In the body section, enter the payment structure: ```json { "transactionType": "Payment", "paymentMethod": { "card": { "merchantSessionKey": "<dynamic: merchant_session_key>", "cardIdentifier": "<dynamic: card_identifier>" } }, "vendorTxCode": "<dynamic: vendor_tx_code>", "amount": 1999, "currency": "GBP", "description": "<dynamic: order_description>", "apply3DSecure": "UseMSPSetting", "customerFirstName": "<dynamic: first_name>", "customerLastName": "<dynamic: last_name>", "billingAddress": { "address1": "<dynamic: address1>", "postalCode": "<dynamic: postal_code>", "city": "<dynamic: city>", "country": "GB" } } ``` The `vendorTxCode` is the most critical field. It MUST be unique for every transaction — Opayo returns error `5009` ('Duplicate vendorTxCode') if you reuse any previous code. Generate it in your Bubble workflow using: `"BUBBLE-" + Current date/time's Unix timestamp`. This ensures uniqueness as long as no two transactions trigger simultaneously. The `amount` must be in pence (minor currency units): £19.99 = 1999. In Bubble's formula editor: `round down(Product's price * 100)`. Note: the `cardIdentifier` and `merchantSessionKey` come from Opayo's Sagepay JS (the client-side card tokenization library). You embed this JS in a Bubble HTML element to capture card details securely. The JS library tokenizes the card and returns a `cardIdentifier` and `merchantSessionKey` that your Bubble workflow collects (via Toolbox plugin's bubble_fn_ pattern) and passes to this API call. Set 'Use as' to 'Action'. After a successful Create Payment call, capture the returned `transactionId` and store it in your Bubble database immediately.

opayo_create_payment_body.json
1{
2 "transactionType": "Payment",
3 "paymentMethod": {
4 "card": {
5 "merchantSessionKey": "<dynamic: merchant_session_key>",
6 "cardIdentifier": "<dynamic: card_identifier>"
7 }
8 },
9 "vendorTxCode": "<dynamic: vendor_tx_code>",
10 "amount": "<dynamic: amount_in_pence>",
11 "currency": "GBP",
12 "description": "<dynamic: order_description>",
13 "apply3DSecure": "UseMSPSetting",
14 "customerFirstName": "<dynamic: first_name>",
15 "customerLastName": "<dynamic: last_name>",
16 "billingAddress": {
17 "address1": "<dynamic: billing_address1>",
18 "postalCode": "<dynamic: billing_postal_code>",
19 "city": "<dynamic: billing_city>",
20 "country": "GB"
21 }
22}
23
24// vendorTxCode generation in Bubble formula editor:
25// "BUBBLE-" + Current date/time's Unix timestamp
26// Example result: "BUBBLE-1737043200"
27
28// Amount conversion in Bubble formula editor:
29// round down(Current product's price * 100)
30// Example: £19.99 → 1999

Pro tip: Generate the vendorTxCode in the workflow step immediately before the Create Payment API call — not on page load. If a user stays on the checkout page for a long time and then submits, a stale timestamp-based code might not be sufficiently unique. Adding a random suffix (`Current date/time's Unix timestamp + unique random string`) provides extra safety.

Expected result: The Create Payment call initializes successfully against the Opayo sandbox, returns a transactionId and status 'Ok', and the Bubble workflow stores these values in the database Order record.

5

Step 5: Build the Refund Workflow

Add a third call to 'Opayo Sandbox'. Name it 'Create Refund'. Method: POST. Path: `/transactions` (same endpoint as payment creation — the `transactionType` field distinguishes the operation). In the body section: ```json { "transactionType": "Refund", "vendorTxCode": "<dynamic: refund_vendor_tx_code>", "amount": 1999, "currency": "GBP", "description": "Refund for order", "referenceTransactionId": "<dynamic: parent_transaction_id>" } ``` Key points: - `transactionType` is `"Refund"` (not `"Payment"`) — this field switches the operation - `vendorTxCode` must be a NEW unique code, not the original payment's vendorTxCode. Use: `"REFUND-" + Current date/time's Unix timestamp` - `referenceTransactionId` is the original payment's `transactionId` stored in your Bubble database - `amount` is in pence — same conversion as payment: `round down(Refund amount * 100)` In your Bubble page, add a Refund button to the Orders Repeating Group with two 'Only when' conditions: 1. `Current cell's Order's status is 'Ok'` — Opayo only refunds transactions with Ok status that have settled (typically 24–48 hours after authorization) 2. `Current date/time > Current cell's Order's created_at + 24 hours` — optional but recommended to prevent refund attempts on unsettled transactions Opayo returns error `3073` if you attempt a refund on a transaction that has not yet settled. Add workflow logic to display a friendly message when 3073 is encountered: 'This payment is still processing — refunds are available after settlement (typically 24–48 hours).' RapidDev's team has configured Opayo refund flows for dozens of UK-based Bubble apps. If you need help mapping Opayo's settlement timeline to your Bubble order management system, schedule a free scoping call at rapidevelopers.com/contact.

opayo_create_refund_body.json
1{
2 "transactionType": "Refund",
3 "vendorTxCode": "<dynamic: refund_vendor_tx_code>",
4 "amount": "<dynamic: refund_amount_in_pence>",
5 "currency": "GBP",
6 "description": "Refund for order",
7 "referenceTransactionId": "<dynamic: parent_transaction_id_from_bubble_db>"
8}
9
10// Refund vendorTxCode generation in Bubble formula editor:
11// "REFUND-" + Current date/time's Unix timestamp
12// Example: "REFUND-1737043800"
13
14// Opayo error 3073: Transaction not yet settled
15// Add 'Only when' condition: Current cell's Order's created_at < Current date/time - 1 day

Pro tip: The refund `vendorTxCode` must be unique and different from the original payment's vendorTxCode. Do not reuse or append '-REFUND' to the original code without adding a timestamp — if two refunds are attempted on the same order (e.g., user double-clicks), both would generate the same code and the second would fail with error 5009.

Expected result: The Create Refund call initializes against a test transaction in the Opayo sandbox, returns a status 'Ok' for the refund, and the Bubble workflow updates the Order record status to 'refunded' in the database.

6

Step 6: Set Up Payment Notifications and Go Live

Opayo can POST payment status notifications to your server when transaction status changes. Unlike Adyen, Opayo's notifications do not require HMAC verification — they are straightforward JSON POSTs (on the PI API, at least). To set up the Backend Workflow endpoint (requires paid Bubble plan — Starter or above): 1. Go to Settings → API → check 'This app exposes a Workflow API' → Save. 2. In Backend Workflows, create a new workflow named `opayo_notification`. 3. Check 'Detect request data'. Leave it waiting. 4. Contact your Opayo integration support or configure the notification URL in My Sage Pay portal → Settings → Notifications. Register: `https://yourapp.bubbleapps.io/api/1.1/wf/opayo_notification`. 5. Process a test payment. Opayo sends a POST with transaction status details. 6. After detection, add workflow steps: find the Order where `transaction_id = incoming notification's transactionId`, update status based on the notification's status field. To go live: 1. Pre-compute the live Base64 string from your live Integration Key and Integration Password. 2. In the API Connector, click 'Add another API' and name it 'Opayo Live'. Set the base URL to `https://pi.sagepay.com/api/v1`. Add the Authorization Private header with the live Base64 string. 3. Switch your Bubble workflows to use the 'Opayo Live' API calls. 4. Update the notification URL in My Sage Pay to your production Bubble app URL. Before your first live transaction, run a £1.00 test with a real card and verify the full flow: payment creation → Bubble database record → notification receipt → status update.

opayo_notification_payload.json
1// Opayo PI API notification payload (example)
2{
3 "transactionId": "T1234567890ABCDEF1234567890",
4 "transactionType": "Payment",
5 "status": "Ok",
6 "statusCode": "0000",
7 "statusDetail": "The Authorisation was Successful.",
8 "vendorTxCode": "BUBBLE-1737043200",
9 "retrievalReference": 19876543,
10 "amount": {
11 "totalAmount": 1999
12 },
13 "currency": "GBP"
14}
15
16// Sandbox base URL: https://pi-test.sagepay.com/api/v1
17// Live base URL: https://pi.sagepay.com/api/v1
18
19// Backend Workflow URL:
20// https://yourapp.bubbleapps.io/api/1.1/wf/opayo_notification
21// (NOT ending in '/initialize' — that suffix is only for Bubble's detection step)

Pro tip: If you rotate your Opayo Integration Password in the future, you MUST recompute both sandbox and live Base64 strings and update them in the respective API Connector Authorization header fields in Bubble. The stale pre-computed string will cause all API calls to fail with 401 until updated.

Expected result: Your Bubble app processes payments through Opayo using live credentials, receives payment notifications via Backend Workflow, and processes refunds. The pre-computed live Base64 Authorization header is set correctly in the 'Opayo Live' API group.

Common use cases

UK E-Commerce Payment Processing

Bubble apps for UK-based online shops can use Opayo to process card payments in GBP. Use `POST /transactions` with `transactionType: Payment` to charge cards, store the returned transactionId in the Bubble database, and retrieve individual transaction details with `GET /transactions/{transactionId}` when needed.

Bubble Prompt

Build a Bubble checkout workflow that calls POST /transactions with transactionType Payment, amount in pence, a unique vendorTxCode using the current timestamp, and the card details token. Store the returned transactionId and status in the Bubble Order record. If status is Ok, show a success confirmation and trigger order fulfillment.

Copy this prompt to try it in Bubble

Refund Processing for Settled Orders

Allow Bubble admin users to issue refunds on settled Opayo transactions. Because Opayo refunds reference the parentTransactionId, you must store the original transaction ID in the Bubble database at payment time. The refund workflow retrieves this ID, calls `POST /transactions` with `transactionType: Refund`, and updates the order status.

Bubble Prompt

When an admin clicks Refund on a settled order row, trigger a Bubble workflow that checks the order status is Ok (settled), generates a unique refund vendorTxCode using timestamp, and calls POST /transactions with transactionType Refund, the parentTransactionId from the Bubble database, and the refund amount in pence.

Copy this prompt to try it in Bubble

Transaction Status Dashboard

Display a searchable transaction history in Bubble by maintaining your own database of Opayo transaction IDs. When a payment is created, store transactionId, amount (pence), status, and timestamp in a Bubble Transaction type. Admins can then look up individual transaction details via GET /transactions/{transactionId} or view the locally-stored data in a Repeating Group without additional API calls.

Bubble Prompt

Build a Bubble admin page with a Repeating Group showing all stored Transaction records from the Bubble database. Include a Details button that calls GET /transactions/{transactionId} to refresh the status from Opayo and update the Bubble record if the status has changed since storage.

Copy this prompt to try it in Bubble

Troubleshooting

All API calls return 401 Unauthorized after setting up the Authorization header

Cause: The pre-computed Base64 string is incorrect (wrong Integration Key or Password was used), has trailing whitespace or newline characters from copy-paste, or the 'Basic ' prefix (with a space after 'Basic') is missing from the header value.

Solution: Open your browser console and re-run `btoa('integration_key:integration_password')`. Verify the result with `atob('your_base64_string')` — it should return `integration_key:integration_password` exactly. In Bubble's API Connector, confirm the Authorization header value is `Basic ` (with a space) followed immediately by the Base64 string, with no extra whitespace. Also check that the Private checkbox is checked.

typescript
1// Validate your Base64 string in browser console:
2// Step 1: Compute
3btoa('YOUR_INTEGRATION_KEY:YOUR_INTEGRATION_PASSWORD')
4// Step 2: Verify the result decodes correctly
5atob('RESULT_FROM_STEP_1')
6// Should output: 'YOUR_INTEGRATION_KEY:YOUR_INTEGRATION_PASSWORD'

Error 5009: Duplicate vendorTxCode — transaction rejected

Cause: The same vendorTxCode was submitted twice to Opayo. This happens when a user clicks the Pay button multiple times, or when a Bubble workflow is triggered twice (for example, by a double-click or a page refresh during payment). Opayo permanently rejects any vendorTxCode that has been used before on your account.

Solution: Add a 'Disable button after click' condition to your Pay button: set a custom page state `payment_in_progress` to true when the button is clicked, and add an 'Only when payment_in_progress is false' condition to the button. Also append a timestamp to the vendorTxCode: `"BUBBLE-" + Current date/time's Unix timestamp`. For high-traffic scenarios, add a random 6-character suffix from Bubble's unique ID generator.

Error 3073: Cannot refund — transaction not yet settled

Cause: The transaction was authorized recently (within the last 24–48 hours) and has not yet been settled by Opayo's batch processing. Opayo only allows refunds on transactions in 'Ok' status that have been settled.

Solution: Add an 'Only when' condition on the refund button: check that the transaction's `created_at` date is at least 24 hours ago, or that the stored status is explicitly 'Ok' and settled. Display a user-friendly message when this error is returned: 'Refunds are available after settlement, typically 24–48 hours after payment.' Log the 3073 error in a Bubble database field for operator review.

Initialize call returns 'There was an issue setting up your call'

Cause: The Initialize call needs a real successful 200 response from Opayo to detect field types. Common causes: Authorization header is wrong (Base64 error), the transactionId used for the GET /transactions/{id} call does not exist in the sandbox, or the sandbox base URL has a typo.

Solution: Open Bubble's Logs tab immediately after the failed initialize to see the raw HTTP status code from Opayo (401 = auth error, 404 = transaction not found). For the Get Transaction call, you need a real sandbox transaction ID — create a payment first (or ask Opayo support for a test transaction ID), then initialize the lookup call with that ID.

Backend Workflow does not receive Opayo payment notifications

Cause: The Bubble app is on the Free plan (Backend Workflows require paid plans), the Workflow API is not enabled in Settings, or the notification URL registered with Opayo includes '/initialize'.

Solution: Go to Settings → API and confirm 'This app exposes a Workflow API' is checked and your Bubble plan is Starter or above. Verify the registered notification URL ends with `/wf/opayo_notification` without any '/initialize' suffix. In My Sage Pay portal → Settings → Notifications, confirm the URL is saved correctly. Contact Opayo support to trigger a test notification to verify delivery.

Best practices

  • Pre-compute the Base64 Authorization string externally (`btoa('key:password')` in browser console) and store it as a Private shared header. Never attempt to compute it dynamically in Bubble's API Connector — the header field does not support runtime JavaScript expressions.
  • Always store Opayo transaction IDs in your Bubble database immediately after a successful Create Payment response. Opayo's PI API has no transaction list endpoint — without locally stored IDs, you cannot look up, display, or refund any previous transactions.
  • Generate a unique vendorTxCode for every payment and every refund. Use a timestamp-based pattern: `"BUBBLE-" + Current date/time's Unix timestamp`. Never reuse a vendorTxCode — Opayo permanently rejects duplicates with error 5009 and there is no recovery mechanism.
  • Store amounts in pence in your Bubble database (not GBP decimals). Use `round down(price * 100)` for all API submissions and `amount / 100` for display. This eliminates floating-point rounding errors in financial calculations.
  • Add Bubble Data tab → Privacy rules for your Transaction type. Without privacy rules, all financial records are readable by all users — a critical security issue for payment data.
  • When you rotate your Opayo Integration Password, immediately recompute both sandbox and live Base64 strings and update the Authorization header values in Bubble's API Connector. Stale pre-computed strings cause 401 errors until updated.
  • Display the vendorTxCode in your Bubble admin UI when showing transaction records. It is the most reliable way to cross-reference a Bubble order record with Opayo's transaction history in their portal or support tickets.
  • For refunds, always add both a status check ('Only when status is Ok') AND a settlement time check (created at least 24 hours ago) as 'Only when' conditions on the refund workflow trigger. Opayo error 3073 (unsettled transaction) is confusing for operators — prevent it at the workflow level.

Alternatives

Frequently asked questions

Why can't Bubble compute the Base64 Authorization string dynamically — why do I have to pre-compute it?

Bubble's API Connector header fields are evaluated as static strings at the time you configure them, not as JavaScript expressions at runtime. You cannot write `Basic {btoa('key:password')}` and have Bubble execute `btoa()` before each API call. The solution is to compute the Base64 string once externally (in your browser console using `btoa('key:password')`) and paste the resulting static string as the header value. Mark it Private so it is injected server-side.

What is a vendorTxCode and why does Opayo reject duplicates?

The vendorTxCode is your unique reference code for each transaction — it is your system's identifier for the payment. Opayo uses it to prevent accidental duplicate charges: once a vendorTxCode is used on your merchant account (even in test mode), it can never be reused. If a user double-clicks Pay and your workflow fires twice with the same code, the second attempt will fail with error 5009. Use a timestamp-based pattern like `'BUBBLE-' + Unix timestamp` to ensure uniqueness.

How do I show a transaction history in Bubble if Opayo has no list endpoint?

You maintain your own transaction history in a Bubble database type. When a payment is created, immediately store the Opayo transactionId, amount (in pence), status, vendorTxCode, and timestamp in a Bubble Thing. Build your Repeating Group against this Bubble database type rather than calling Opayo's API on every page load. Use the GET /transactions/{transactionId} call only when you need to refresh the status of a specific transaction from Opayo's system.

Can I use Opayo (Sage Pay) on a Free Bubble plan?

Yes, you can use the API Connector calls (create payments, get transaction details, process refunds) on the Free plan. However, receiving Opayo payment notification POSTs requires Backend Workflows, which are only available on paid Bubble plans (Starter+). On the Free plan, you can check payment status by calling GET /transactions/{transactionId} on your success/failure return page after the payment redirect.

My integration worked for months, and now all API calls are returning 401 — what happened?

You likely rotated your Opayo Integration Password in My Sage Pay portal and forgot to update the pre-computed Base64 string in Bubble's API Connector. When credentials change, the old Base64 string becomes invalid. Open your browser console, recompute `btoa('integration_key:new_integration_password')`, and paste the result into the Authorization header value in Bubble's API Connector. Do this for both sandbox and live groups if you rotated both.

What is the difference between Sage Pay and Opayo?

Opayo is the rebranded name for Sage Pay after Elavon (a CIAM company) acquired the payment business. The underlying technology, portal (My Sage Pay), and API endpoints are the same — only the brand name changed. When you see 'Opayo' in documentation, it means the same platform you knew as Sage Pay. The sandbox URL still contains 'sagepay.com' and the API paths reference '/api/v1' regardless of branding. Both names refer to the same integration target.

RapidDev

Talk to an Expert

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

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

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.