Skip to main content
RapidDev - Software Development Agency
v0-integrationsNext.js API Route

How to Integrate Sage Pay with V0

Integrate Opayo (formerly Sage Pay) with your V0-generated Next.js app by creating a secure server-to-server API route that handles UK/EU payment requests. Store your Opayo integration key and password in Vercel environment variables, then call the Opayo REST API from a Next.js route handler to tokenize cards and process transactions without exposing credentials to the browser.

What you'll learn

  • How to create a Next.js API route that communicates with the Opayo REST API
  • How to securely store Opayo integration keys in Vercel environment variables
  • How to build a V0-generated payment form that submits to your API route
  • How to handle Opayo transaction responses and redirect users on success or failure
  • How to test payments in the Opayo sandbox environment before going live
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read45 minutesPaymentApril 2026RapidDev Engineering Team
TL;DR

Integrate Opayo (formerly Sage Pay) with your V0-generated Next.js app by creating a secure server-to-server API route that handles UK/EU payment requests. Store your Opayo integration key and password in Vercel environment variables, then call the Opayo REST API from a Next.js route handler to tokenize cards and process transactions without exposing credentials to the browser.

Accepting UK Payments with Opayo (Sage Pay) in V0

Opayo, formerly known as Sage Pay and now operated by Elavon, is one of the most trusted payment gateways for UK and EU merchants. If you are building a V0 app that needs to accept card payments from British customers, Opayo offers competitive rates, direct card acquirer relationships, and a REST API designed for server-to-server integrations. Many UK businesses specifically require Opayo support because of its strong brand recognition and compliance with UK card scheme rules.

V0 generates the React payment form components and the Next.js project scaffolding. You then add an API route that acts as a secure proxy between your frontend and the Opayo REST API. Because Opayo uses Basic Auth with an integration key and password, those credentials must never reach the browser — the API route keeps them server-side only. The Opayo Pi REST API (their modern integration) lets you create merchant sessions, tokenize card data in the browser via their JavaScript library, and then authorise the token server-side.

This tutorial walks through the complete flow: generating a payment UI with V0, setting up the Opayo Pi server integration, storing credentials in Vercel, and handling transaction responses. You will use the Opayo sandbox environment throughout development so no real money is processed during testing.

Integration method

Next.js API Route

Opayo does not have an official npm SDK, so integration happens through direct REST API calls from a Next.js server-side route handler. The API route receives payment data from the V0 frontend, authenticates using Basic Auth with your Opayo integration credentials stored in Vercel environment variables, and submits a server-to-server payment request to the Opayo REST API. This keeps all sensitive credentials away from the browser.

Prerequisites

  • A V0 account at v0.dev with a Next.js project created
  • An Opayo (Opayo Pi) merchant account — sign up at opayo.co.uk for a test account
  • Your Opayo Integration Key and Integration Password from the Opayo MySagePay portal
  • A Vercel account connected to your V0 project for deployment
  • Basic familiarity with Next.js API routes and environment variables

Step-by-step guide

1

Generate a Payment Form UI with V0

Start in the V0 chat interface and describe the payment form you need. V0 will generate a React component with the appropriate form fields for collecting payment information. For Opayo Pi integration, the recommended approach is to render a standard form first and then layer in Opayo's card tokenization JavaScript. Ask V0 to create a clean checkout form with fields for cardholder name, card number, expiry date, and CVV, along with billing address fields required for UK AVS/CV2 checks. V0 will use Tailwind CSS to style the form and create a submit handler that posts the data to your Next.js API route. Make sure the form uses controlled React state so you can clear sensitive fields after submission. The component should also handle loading and error states — showing a spinner while the payment processes and displaying friendly error messages if the transaction is declined. Once V0 generates the component, review it in the preview to confirm the layout looks right before moving on to the API route.

V0 Prompt

Create a UK payment checkout form component with fields for full name, email, card number (with a lock icon), expiry date (MM/YY), CVV, billing postcode, and a 'Pay Now' button in dark blue. Add loading state with a spinner and an error message area below the button. The form should POST to /api/payment and show a green success card with transaction reference when complete. Use Tailwind for styling.

Paste this in V0 chat

Pro tip: Opayo Pi recommends using their Sage Pay Form drop-in JavaScript library for PCI DSS compliance. For simplicity in this tutorial, we use server-to-server mode which requires your server to handle card data — ensure your Vercel deployment is PCI compliant or use Opayo's hosted payment page for full PCI offload.

Expected result: A styled payment form component appears in the V0 preview with card fields, billing postcode, and a Pay Now button.

2

Create the Opayo API Route

Create a Next.js App Router route handler at app/api/payment/route.ts. This file will receive the payment data from your frontend, construct an Opayo Pi merchant session, and then submit the payment authorisation. The Opayo Pi REST API uses Basic Auth where the username is your Integration Key and the password is your Integration Password. You first call the /merchant-session-keys endpoint to get a short-lived session key, which the browser uses to tokenize the card. For a server-to-server integration (where you handle card data on your server), you call the /transactions endpoint directly with the card details. The API route must POST to https://pi-test.sagepay.com/api/v1/transactions for sandbox or https://pi-live.sagepay.com/api/v1/transactions for production. Always read credentials from process.env — never hardcode them. Return a structured JSON response to the frontend with the transaction status, reference, and any error details so the UI can display appropriate feedback. Add proper error handling for network failures and Opayo API errors.

app/api/payment/route.ts
1// app/api/payment/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const OPAYO_BASE_URL = process.env.OPAYO_ENV === 'production'
5 ? 'https://pi-live.sagepay.com/api/v1'
6 : 'https://pi-test.sagepay.com/api/v1';
7
8function getBasicAuthHeader() {
9 const credentials = `${process.env.OPAYO_INTEGRATION_KEY}:${process.env.OPAYO_INTEGRATION_PASSWORD}`;
10 return `Basic ${Buffer.from(credentials).toString('base64')}`;
11}
12
13export async function POST(request: NextRequest) {
14 try {
15 const body = await request.json();
16 const { amount, currency = 'GBP', description, cardDetails, billingAddress } = body;
17
18 if (!amount || !cardDetails) {
19 return NextResponse.json({ error: 'Missing required payment fields' }, { status: 400 });
20 }
21
22 // Create a transaction via Opayo Pi REST API
23 const transactionPayload = {
24 transactionType: 'Payment',
25 paymentMethod: {
26 card: {
27 merchantSessionKey: process.env.OPAYO_VENDOR_NAME,
28 cardIdentifier: cardDetails.cardIdentifier,
29 },
30 },
31 vendorTxCode: `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
32 amount: Math.round(amount * 100), // Opayo expects pence
33 currency,
34 description,
35 apply3DSecure: 'UseMSPSetting',
36 billingAddress: {
37 address1: billingAddress?.address1 || '',
38 city: billingAddress?.city || '',
39 postalCode: billingAddress?.postalCode || '',
40 country: billingAddress?.country || 'GB',
41 },
42 };
43
44 const response = await fetch(`${OPAYO_BASE_URL}/transactions`, {
45 method: 'POST',
46 headers: {
47 'Authorization': getBasicAuthHeader(),
48 'Content-Type': 'application/json',
49 },
50 body: JSON.stringify(transactionPayload),
51 });
52
53 const result = await response.json();
54
55 if (!response.ok) {
56 return NextResponse.json(
57 { error: result.description || 'Payment failed', code: result.code },
58 { status: response.status }
59 );
60 }
61
62 return NextResponse.json({
63 success: true,
64 transactionId: result.transactionId,
65 status: result.status,
66 statusCode: result.statusCode,
67 statusDetail: result.statusDetail,
68 });
69 } catch (error) {
70 console.error('Opayo payment error:', error);
71 return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
72 }
73}

Pro tip: The vendorTxCode must be unique per transaction. Using a timestamp combined with a random string prevents collisions even under high load.

Expected result: The API route file is created and the payment logic compiles without TypeScript errors.

3

Get a Merchant Session Key Endpoint

Opayo Pi's recommended integration flow requires obtaining a short-lived Merchant Session Key (MSK) before the browser can tokenize the card. Create a separate API route at app/api/payment/session/route.ts that calls the Opayo merchant-session-keys endpoint. The frontend calls this first when the payment form loads, receives the MSK, and then uses the Opayo.js drop-in library to convert the raw card number into a safe card identifier. This card identifier (not the raw PAN) is then sent to your main payment API route. This two-step flow is the recommended PCI-compliant pattern for Opayo Pi. The MSK is valid for 400 seconds so it is fine to fetch one per page load. Your frontend component should call /api/payment/session on mount, store the returned merchantSessionKey and merchantId in component state, and pass them to the Opayo.js library. This setup reduces your PCI DSS scope significantly because raw card data is handled by Opayo's JavaScript, not your server.

V0 Prompt

Update the payment form component to fetch a merchant session key from /api/payment/session when the component mounts. Store the session key in state and show a loading skeleton on the card fields until the session key arrives. If the fetch fails, show an error banner saying 'Payment system unavailable, please try again.'

Paste this in V0 chat

app/api/payment/session/route.ts
1// app/api/payment/session/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET() {
5 try {
6 const credentials = `${process.env.OPAYO_INTEGRATION_KEY}:${process.env.OPAYO_INTEGRATION_PASSWORD}`;
7 const authHeader = `Basic ${Buffer.from(credentials).toString('base64')}`;
8
9 const response = await fetch(
10 'https://pi-test.sagepay.com/api/v1/merchant-session-keys',
11 {
12 method: 'POST',
13 headers: {
14 'Authorization': authHeader,
15 'Content-Type': 'application/json',
16 },
17 body: JSON.stringify({
18 vendorName: process.env.OPAYO_VENDOR_NAME,
19 }),
20 }
21 );
22
23 const data = await response.json();
24
25 if (!response.ok) {
26 return NextResponse.json(
27 { error: 'Failed to create merchant session' },
28 { status: 500 }
29 );
30 }
31
32 return NextResponse.json({
33 merchantSessionKey: data.merchantSessionKey,
34 expiry: data.expiry,
35 });
36 } catch (error) {
37 console.error('Opayo session error:', error);
38 return NextResponse.json({ error: 'Session creation failed' }, { status: 500 });
39 }
40}

Pro tip: Use the sandbox URL (pi-test.sagepay.com) for development and switch to pi-live.sagepay.com by changing the OPAYO_ENV environment variable to 'production'.

Expected result: Navigating to /api/payment/session in the browser returns a JSON object with merchantSessionKey and expiry fields.

4

Add Environment Variables in Vercel

Your Opayo credentials must never be committed to your repository or exposed to the browser. In the Vercel Dashboard, go to your project, then navigate to Settings → Environment Variables. Add the following variables. OPAYO_INTEGRATION_KEY: found in the MySagePay portal under Settings → Manage Integrations → Opayo Pi. OPAYO_INTEGRATION_PASSWORD: the corresponding password shown alongside the integration key. OPAYO_VENDOR_NAME: your Sage Pay vendor name (your merchant account name used when you registered). OPAYO_ENV: set to 'sandbox' for development and 'production' when you are ready to go live. Set these for the Production, Preview, and Development environments. For the Development environment, use your sandbox credentials. After adding the variables, redeploy your Vercel project so the new environment variables take effect — Vercel inlines them at build time for Next.js, but server-side process.env reads happen at runtime in serverless functions. You can verify the variables are available by adding a temporary log in your API route during development, but remove any console.log statements that might expose credentials before going to production.

Pro tip: Never prefix Opayo credentials with NEXT_PUBLIC_ — they would be exposed in the browser JavaScript bundle and visible to anyone who views source.

Expected result: All four environment variables appear in the Vercel Dashboard under Settings → Environment Variables for each environment scope.

5

Connect the UI to the API Route and Test

With the API route and environment variables in place, update the payment form component so the submit handler calls your /api/payment endpoint. The fetch call should POST a JSON body containing the transaction amount, currency (GBP), description, card identifier from Opayo.js, and billing address fields. Handle the response by checking for the success property: if true, display a confirmation panel with the transactionId; if false, show the error message from the API response. Add client-side validation to check that all required fields are filled before submitting, and disable the Pay Now button after first click to prevent duplicate submissions. To test in the Opayo sandbox, use the test card numbers provided in the Opayo developer documentation — for example, 4929000000006 for a successful Visa transaction and 4000000000000002 for a declined card. Check the Opayo MySagePay reporting portal to confirm test transactions appear. Once everything works in the sandbox, change OPAYO_ENV to 'production' in your Vercel environment variables and update the API route URLs from pi-test.sagepay.com to pi-live.sagepay.com to process real payments.

V0 Prompt

Update the checkout form submit handler to POST the payment data to /api/payment with the amount, currency 'GBP', cardholder name, card identifier, and billing postcode. On success, replace the form with a green confirmation panel showing 'Payment successful' and the transaction reference. On error, show the error message in a red alert below the submit button and re-enable the button.

Paste this in V0 chat

components/PaymentForm.tsx
1// components/PaymentForm.tsx (relevant submit handler)
2async function handleSubmit(e: React.FormEvent) {
3 e.preventDefault();
4 setIsLoading(true);
5 setError('');
6
7 try {
8 const response = await fetch('/api/payment', {
9 method: 'POST',
10 headers: { 'Content-Type': 'application/json' },
11 body: JSON.stringify({
12 amount: orderTotal,
13 currency: 'GBP',
14 description: orderDescription,
15 cardDetails: {
16 cardIdentifier: cardIdentifier, // from Opayo.js
17 },
18 billingAddress: {
19 address1: formData.address1,
20 city: formData.city,
21 postalCode: formData.postcode,
22 country: 'GB',
23 },
24 }),
25 });
26
27 const result = await response.json();
28
29 if (result.success) {
30 setTransactionId(result.transactionId);
31 setPaymentComplete(true);
32 } else {
33 setError(result.error || 'Payment was declined. Please try again.');
34 }
35 } catch (err) {
36 setError('Network error. Please check your connection and try again.');
37 } finally {
38 setIsLoading(false);
39 }
40}

Pro tip: For complex Opayo setups including 3D Secure authentication flows, RapidDev's team can help configure the full challenge/response cycle required for strong customer authentication (SCA) under PSD2.

Expected result: Submitting the form with test card 4929000000006 returns a successful transaction ID and the confirmation panel appears.

Common use cases

E-commerce Checkout for UK Shoppers

A UK-based online retailer uses V0 to build a product page with an add-to-cart and checkout flow. The Opayo API route processes card payments and returns a transaction reference displayed in the order confirmation page.

V0 Prompt

Create a checkout page with a billing address form, card details section with fields for card number, expiry, and CVV, an order summary panel on the right showing item name and total price in GBP, and a 'Pay Now' button that posts to /api/payment. Show a success message with transaction ID when payment completes.

Copy this prompt to try it in V0

Subscription Payment Form

A SaaS startup in London accepts monthly subscription payments through their V0-built marketing site. Opayo tokenizes the card so recurring charges can be made without storing PAN data.

V0 Prompt

Build a subscription sign-up form with plan selection (monthly £9.99 or annual £99), full name, email, and card fields. On submit it calls /api/payment/subscribe and displays a confirmation card with the plan name and next billing date.

Copy this prompt to try it in V0

Invoice Payment Portal

A UK accounting firm's clients pay invoices through a V0-built portal. The invoice ID is passed to the Opayo API route, which creates a payment linked to that invoice reference for reconciliation.

V0 Prompt

Create an invoice payment page that reads an invoice ID from a URL parameter, displays the invoice amount and description, and has a card payment form that submits to /api/payment/invoice. Show a receipt table after successful payment.

Copy this prompt to try it in V0

Troubleshooting

401 Unauthorized response from the Opayo API

Cause: The Basic Auth credentials are incorrect or the environment variables are not loaded. This often happens when OPAYO_INTEGRATION_KEY or OPAYO_INTEGRATION_PASSWORD have trailing spaces or were copied with extra whitespace.

Solution: Double-check the environment variable values in the Vercel Dashboard. Copy the Integration Key and Password directly from the MySagePay portal. Redeploy after changing any variable. Verify with a temporary console.log in the API route (development only) that the credentials are being read correctly.

CORS error when calling Opayo API from the browser

Cause: You are calling the Opayo API directly from client-side code instead of through the Next.js API route. Opayo does not permit cross-origin requests from browsers to their REST API.

Solution: All Opayo REST API calls must go through your server-side API route at app/api/payment/route.ts. The browser should only call your own /api/payment endpoint, never https://pi-test.sagepay.com directly.

Transaction returns statusCode '4000' with message 'Card declined'

Cause: In the sandbox, certain test card numbers simulate decline scenarios. In production, this is a genuine card decline from the issuing bank.

Solution: In sandbox, use the Opayo test card number 4929000000006 for a successful transaction. For production declines, display a user-friendly message and suggest the customer retry with a different card or contact their bank. Never expose the raw Opayo error code to end users.

vendorTxCode already used error on retry

Cause: The vendorTxCode must be unique for every transaction attempt. If your UI allows retries, the same code gets sent twice, causing Opayo to reject the second attempt.

Solution: Generate a new vendorTxCode on every API route call rather than passing it from the frontend. Use Date.now() combined with a random string as shown in the API route code above.

typescript
1const vendorTxCode = `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

Best practices

  • Always use server-side API routes to call the Opayo REST API — never call it directly from client-side code
  • Store OPAYO_INTEGRATION_KEY and OPAYO_INTEGRATION_PASSWORD in Vercel environment variables without the NEXT_PUBLIC_ prefix
  • Use separate sandbox and production credentials by setting OPAYO_ENV to 'sandbox' in Preview environments and 'production' in Production
  • Implement idempotent vendorTxCode generation server-side so each transaction attempt has a guaranteed unique code
  • Log transaction IDs and statuses in your database immediately after receiving the Opayo response to enable reconciliation
  • Display only user-friendly error messages on the frontend — never expose raw Opayo error codes or technical details
  • Test all decline scenarios using Opayo's published sandbox card numbers before going live

Alternatives

Frequently asked questions

Is Opayo the same as Sage Pay?

Yes. Sage Pay rebranded to Opayo in May 2020 after being acquired by Elavon. The underlying payment gateway technology and API are the same. You may see both names used interchangeably in developer documentation.

Do I need a PCI DSS certification to use Opayo with V0?

Your PCI DSS requirements depend on how you handle card data. Using the Opayo Pi merchant session key flow with their Opayo.js library offloads card data handling to Opayo, reducing your scope to SAQ A. If you process raw card numbers server-side, your scope increases. Consult Opayo's PCI guidance for your specific setup.

Can I test Opayo without a merchant account?

You can request a sandbox test account from Opayo by registering on the opayo.co.uk website. Sandbox accounts provide test integration keys and a set of test card numbers that simulate success and decline scenarios without any real money movement.

How do I handle 3D Secure authentication with Opayo?

Opayo Pi supports 3D Secure 2 for Strong Customer Authentication (SCA) required under PSD2. The transaction response will include a 3D Secure status and may return a challenge URL for the cardholder. Implementing the full 3D Secure flow requires additional frontend redirect logic and a callback route to handle the authentication result.

Why is my Opayo sandbox returning 422 errors?

A 422 response from Opayo usually means a validation error in your request body — for example, a missing required field, an amount value of zero, or a malformed postal code. Check the Opayo API error response body for specific field-level validation messages and compare your payload against the Opayo Pi API documentation.

Can I use Opayo for recurring subscription payments?

Yes, Opayo supports repeat payments using stored card tokens. After the first transaction, you receive a card identifier that can be used for subsequent charges without the cardholder re-entering their details. This is governed by your Continuous Payment Authority agreement with the cardholder.

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.