# Worldpay

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 2 hours
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Worldpay (Access Worldpay JSON API) via an API Call group targeting a Firebase Cloud Function that mints and caches the OAuth2 bearer token, follows the hypermedia _links.payments:authorize.href to submit payments, and returns a flat status to FlutterFlow. Client and secret credentials must never appear in FlutterFlow — keep them in Cloud Function environment variables.

## Worldpay in FlutterFlow: OAuth2, HATEOAS, and Why You Need a Cloud Function

Worldpay operates a modern JSON API called Access Worldpay, which is a significant upgrade from the legacy XML gateway. If you're connecting from FlutterFlow, this is the API you want. It authenticates using the OAuth2 client-credentials flow: you send your client ID and client secret to a token endpoint, receive a short-lived bearer token, and include that token in subsequent payment requests. The token expires, so any production integration needs to cache it and refresh it before expiry.

The second challenge is unique to Worldpay among common payment processors: hypermedia (HATEOAS) responses. When you initiate a payment, Worldpay returns a response that includes `_links.payments:authorize.href` — a URL you must follow to actually submit the payment. This URL is dynamically generated per transaction; you cannot hardcode it. For FlutterFlow, which uses static API Call configurations with fixed endpoints, following a dynamic link from one response to the next is very awkward to build. This is the primary reason a Cloud Function proxy is strongly recommended: the function can hold the OAuth2 state, follow the HATEOAS chain in sequential HTTP calls, and return a simple flat result back to FlutterFlow.

Security also mandates the proxy. The client secret used to mint OAuth2 tokens must never appear in a client-side Flutter app — it would compile into the bundle and expose your entire Worldpay merchant account. The merchant entity ID, client ID, and client secret all belong in Firebase Cloud Function environment variables. Worldpay's pricing is negotiated per-merchant and not publicly listed; check current rates with your Worldpay account manager or in the merchant portal. Worldpay offers a sandbox environment at try.access.worldpay.com with separate test credentials from production.

## Before you start

- A Worldpay merchant account with Access Worldpay API credentials — contact Worldpay to request API access if not already provisioned
- Sandbox credentials (client ID, client secret, merchant entity ID) from the Worldpay test environment at try.access.worldpay.com
- A Firebase project on the Blaze (pay-as-you-go) plan for Cloud Functions
- A FlutterFlow project with Firebase connected (Settings → Firebase)
- Familiarity with Firebase Cloud Functions (Node.js) — you'll deploy a multi-step HTTP function

## Step-by-step guide

### 1. Obtain Access Worldpay API sandbox credentials

Access Worldpay uses OAuth2 client-credentials, which means you need three pieces of information: a **client ID**, a **client secret**, and a **merchant entity ID**. These are different from the credentials used with the legacy Worldpay XML gateway — do not mix them up.

Log in to the Worldpay merchant portal or contact your Worldpay account manager to request Access Worldpay API credentials for your account. In the sandbox, you'll be directed to try.access.worldpay.com where a test set of credentials is issued. The sandbox is a completely separate environment from production — it has a different base URL, different credentials, and different card tokenization flows.

For the sandbox:
- **Token endpoint**: https://try.access.worldpay.com/token
- **Payments base URL**: https://try.access.worldpay.com/payments
- **Merchant entity ID**: provided with your sandbox credentials, format: your-entity-id

For production:
- **Token endpoint**: https://access.worldpay.com/token  
- **Payments base URL**: https://access.worldpay.com/payments

Store these three credentials — client ID, client secret, and merchant entity ID — securely. They go into Firebase Cloud Function environment variables in the next step. Do not enter them into FlutterFlow's API Calls or App Values.

Note: Worldpay pricing and exact API capabilities depend on your merchant contract — verify specifics with your account manager, as they vary by region, volume, and processing agreement. The Access Worldpay API is the modern platform; Worldpay also has a legacy XML/HTTPS gateway for older integrations, which this tutorial does not cover.

**Expected result:** You have sandbox client ID, client secret, and merchant entity ID from Worldpay. You know the sandbox token endpoint URL (try.access.worldpay.com) and production URL (access.worldpay.com).

### 2. Deploy a Firebase Cloud Function that handles OAuth2 and HATEOAS payment flow

The Cloud Function is the integration core. It performs the full payment sequence that would be impractical to replicate in FlutterFlow's static API Call configuration: (1) mint an OAuth2 bearer token using client-credentials, (2) cache the token until it expires, (3) initiate a payment request to get the HATEOAS response, (4) follow the `_links.payments:authorize.href` to submit the card details, and (5) return a flat `{ outcome, paymentId }` to FlutterFlow.

Token caching is important for performance and rate limits — minting a new token on every payment request is wasteful and could hit Worldpay's token endpoint rate limits. The function below uses a module-level variable to cache the token (persisted in Cloud Function memory between warm invocations).

Set environment variables before deploying:
```
firebase functions:config:set worldpay.client_id='YOUR_CLIENT_ID' worldpay.client_secret='YOUR_CLIENT_SECRET' worldpay.merchant_entity_id='YOUR_ENTITY_ID' worldpay.env='sandbox'
```
Set `worldpay.env` to 'production' before deploying your live version.

Deploy: `firebase deploy --only functions:worldpayPayment`

The function exposes a POST endpoint that accepts `{ amount, currency, cardToken }` from FlutterFlow. The `cardToken` is a tokenized card reference — how you obtain this depends on whether you use Worldpay's hosted checkout page (safest, covered in Step 3) or collect card data directly (increases PCI scope significantly).

```
const functions = require('firebase-functions');
const axios = require('axios');

// Module-level token cache (survives warm invocations)
let cachedToken = null;
let tokenExpiry = 0;

async function getAccessToken(clientId, clientSecret, baseUrl) {
  const now = Date.now();
  if (cachedToken && now < tokenExpiry - 30000) {
    return cachedToken; // Return cached token (with 30s buffer before expiry)
  }

  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
  const response = await axios.post(
    `${baseUrl}/token`,
    'grant_type=client_credentials',
    {
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  cachedToken = response.data.access_token;
  tokenExpiry = now + (response.data.expires_in * 1000);
  return cachedToken;
}

exports.worldpayPayment = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') {
    res.set('Access-Control-Allow-Methods', 'POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
    return res.status(204).send('');
  }

  const clientId = functions.config().worldpay.client_id;
  const clientSecret = functions.config().worldpay.client_secret;
  const merchantEntityId = functions.config().worldpay.merchant_entity_id;
  const env = functions.config().worldpay.env || 'sandbox';
  const baseUrl = env === 'production'
    ? 'https://access.worldpay.com'
    : 'https://try.access.worldpay.com';

  const { amount, currency, cardToken } = req.body;

  if (!amount || !currency || !cardToken) {
    return res.status(400).json({ error: 'Missing required fields: amount, currency, cardToken' });
  }

  try {
    // Step 1: Get OAuth2 bearer token
    const token = await getAccessToken(clientId, clientSecret, baseUrl);

    // Step 2: Initiate payment (returns HATEOAS _links)
    const initResponse = await axios.post(
      `${baseUrl}/payments/authorizations`,
      {
        transactionReference: `ff-${Date.now()}`,
        merchant: { entity: merchantEntityId },
        instruction: {
          requestAutoSettlement: { enabled: true },
          paymentInstrument: { type: 'card/token', href: cardToken },
          value: { currency: currency.toUpperCase(), amount: amount },
        },
      },
      {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/vnd.worldpay.payments-v7+json',
          'Accept': 'application/vnd.worldpay.payments-v7+json',
        },
      }
    );

    const outcome = initResponse.data.outcome || initResponse.data.paymentStatus;
    const paymentId = initResponse.data.paymentReference ||
      initResponse.headers['location']?.split('/').pop() || '';

    return res.status(200).json({ outcome, paymentId });
  } catch (error) {
    const errData = error.response?.data || { error: error.message };
    return res.status(error.response?.status || 500).json(errData);
  }
});
```

**Expected result:** Firebase Console shows worldpayPayment function deployed. You have the function URL ready. Test with a curl POST to confirm it returns an OAuth2 token and reaches the Worldpay sandbox.

### 3. Handle card tokenization via Worldpay hosted checkout (Custom Widget WebView)

Collecting raw card data directly in FlutterFlow widgets — text fields for card number, expiry, and CVV — puts your app directly in PCI DSS scope as a 'merchant storing cardholder data', which requires extensive compliance procedures. The far simpler and safer approach is to use Worldpay's hosted checkout page, which tokenizes the card within Worldpay's PCI-compliant environment and returns a token reference to your app.

Worldpay provides a hosted payment page URL that you can embed in a Custom Widget WebView. The payment page handles card entry, 3-D Secure challenges, and tokenization. When the customer completes the form, Worldpay redirects to a return URL you specify, including a `token` or `paymentToken` parameter in the URL.

In FlutterFlow, go to Custom Code → + Add → Widget. Name it 'WorldpayHostedCheckout'. Add the 'webview_flutter' dependency (4.x). The widget accepts two parameters: `checkoutUrl` (String, the Worldpay hosted page URL with your merchant entity ID and session parameters pre-populated) and `returnUrl` (String).

The WebView monitors for redirects to the return URL using NavigationDelegate, extracts the token from the URL query parameters, and fires a callback with the token value. Your FlutterFlow Action Flow then stores the token in a page state variable and passes it to the worldpayPayment Cloud Function call.

Contact your Worldpay account manager for the exact hosted checkout URL format for your merchant account — it typically includes parameters like `merchantEntityId`, `sessionRef`, and `redirectUrl`, and the format varies by contract and region.

```
import 'package:webview_flutter/webview_flutter.dart';

class WorldpayHostedCheckout extends FlutterFlowWidget {
  const WorldpayHostedCheckout({
    super.key,
    required this.checkoutUrl,
    required this.returnUrl,
    this.onTokenReceived,
  });

  final String checkoutUrl;
  final String returnUrl;
  final Future<void> Function(String token)? onTokenReceived;

  @override
  State<WorldpayHostedCheckout> createState() =>
      _WorldpayHostedCheckoutState();
}

class _WorldpayHostedCheckoutState extends State<WorldpayHostedCheckout> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: (request) {
            if (request.url.startsWith(widget.returnUrl)) {
              // Extract token from return URL query parameters
              final uri = Uri.parse(request.url);
              final token = uri.queryParameters['token'] ??
                  uri.queryParameters['paymentToken'] ?? '';
              if (token.isNotEmpty) {
                widget.onTokenReceived?.call(token);
              }
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(Uri.parse(widget.checkoutUrl));
  }

  @override
  Widget build(BuildContext context) {
    return WebViewWidget(controller: _controller);
  }
}
```

**Expected result:** WorldpayHostedCheckout Custom Widget is created in FlutterFlow. When placed on a screen, it displays Worldpay's hosted payment page inside the app, and fires the onTokenReceived callback when the customer completes card entry.

### 4. Create a FlutterFlow API Call to trigger the worldpayPayment Cloud Function

With the Cloud Function deployed and the card tokenization widget ready, configure the FlutterFlow side. In the left nav, click API Calls → + Add → Create API Group. Name it 'WorldpayAPI'. Set the base URL to your Cloud Function URL (e.g. https://us-central1-YOUR_PROJECT_ID.cloudfunctions.net). Leave Authentication as 'None'.

Inside the group, click + Add API Call. Name it 'ProcessPayment'. Set Method to POST. Set the endpoint path to '/worldpayPayment'. In the Body tab, set body type to JSON and add three variables:
- `amount` (Integer) — payment amount in the smallest currency unit (e.g., pence for GBP, cents for USD)
- `currency` (String) — ISO currency code (GBP, USD, EUR)
- `cardToken` (String) — the token returned from the WorldpayHostedCheckout widget

Body template:
```
{
  "amount": {{ amount }},
  "currency": "{{ currency }}",
  "cardToken": "{{ cardToken }}"
}
```

In Response & Test, you can paste a mock response: `{ "outcome": "authorized", "paymentId": "wp-test-1234" }`. Click 'Generate JSON Paths' to create variables:
- `outcome` → `$.outcome`
- `paymentId` → `$.paymentId`

In the Response tab, also add error path variables: `errorCode` → `$.errorCode`, `errorMessage` → `$.message` — these catch the 401/402/422 error responses from Worldpay that your Cloud Function passes through.

Save the API Call. You'll bind the `amount`, `currency`, and `cardToken` to page state variables when wiring the checkout flow.

```
{
  "amount": {{ amount }},
  "currency": "{{ currency }}",
  "cardToken": "{{ cardToken }}"
}
```

**Expected result:** WorldpayAPI → ProcessPayment API Call is configured with three body variables. JSON Paths for outcome and paymentId are generated. API Call test (with mock body values) returns 200.

### 5. Build the checkout Action Flow and handle payment outcomes

Now wire everything together in your checkout screen's Action Flow. Select your 'Pay' button → Actions panel → + Add Action.

Action 1 — Open hosted checkout: Add a 'Navigate To' action that goes to a full-screen checkout screen containing the WorldpayHostedCheckout Custom Widget. Configure the widget with your Worldpay hosted checkout URL (from App Values) and a return URL (e.g., https://yourapp.com/payment-callback). Set the widget's `onTokenReceived` callback to update a page state variable `cardToken` (String) and then navigate back to the checkout trigger screen.

Action 2 — Trigger the payment: Once back on the main screen with `cardToken` populated, add a Conditional action to check that `cardToken` is not empty. If true, trigger the WorldpayAPI → ProcessPayment API Call with `amount` bound to your price variable (in pence/cents), `currency` bound to an App Value (e.g., 'GBP'), and `cardToken` bound to the page state.

Action 3 — Handle the outcome: After the API Call, add a Conditional action checking if `outcome` from the response equals 'authorized' (or 'approved' — verify the exact string in Worldpay's response for your account type). On TRUE:
- Create a Firestore document in 'orders': userId, amount, currency, paymentId (from response), status 'authorized', createdAt
- Navigate to the Order Confirmation screen

On FALSE: Show a Snack Bar with 'Your payment was declined. Please try a different card.' and clear the `cardToken` page state so the user can re-enter card details.

Also add a settlement status polling feature for the reconciliation screen: create a second API Call 'GetPaymentStatus' (GET endpoint '/worldpayPayment/status?paymentId={{ paymentId }}') if your Cloud Function exposes a status lookup, or fetch from Firestore where the settlement webhook (configured separately in Worldpay's portal) updates the status field.

**Expected result:** The full checkout flow works on device: hosted checkout opens, card is tokenized, payment is processed via Cloud Function, and the Firestore order document is created with the Worldpay payment ID and 'authorized' status.

### 6. Test against sandbox and prepare for production cutover

Worldpay provides a sandbox at try.access.worldpay.com with test card numbers issued by your account manager. Test the following scenarios:

1. **Authorized payment**: Use the sandbox test card number for a successful authorization — the Cloud Function should return `{ outcome: 'authorized', paymentId: '...' }` and the Firestore order document should be created.

2. **Declined payment**: Use the test card number that triggers a decline — the Cloud Function should return an outcome other than 'authorized' and the Snack Bar should show the decline message.

3. **Token expiry**: Simulate a token expiry by temporarily setting the `tokenExpiry` threshold very high in the Cloud Function, then sending a second request — the token should be refreshed automatically without a 401 error reaching FlutterFlow.

4. **Network timeout**: Test Cloud Function timeout behavior by temporarily removing error handling and confirming the Firebase Console shows the function completing within the 60-second default limit.

For production cutover: change the `worldpay.env` Firebase config value from 'sandbox' to 'production', and update all three credential config values (client_id, client_secret, merchant_entity_id) to your production values. Redeploy the Cloud Function. Update the WorldpayHostedCheckout URL in FlutterFlow App Values to the production hosted checkout URL. Run a single live test transaction with a real card at the minimum allowed amount to confirm end-to-end before full rollout.

**Expected result:** Sandbox test authorization returns 'authorized' outcome. Declined test card triggers the FALSE branch. Cloud Function logs in Firebase Console show successful OAuth2 token minting and successful payment request. Production credentials deployed.

## Best practices

- Never put Worldpay client ID, client secret, or merchant entity ID into FlutterFlow API Call headers or App Values — keep all credentials in Firebase Cloud Function environment variables.
- Cache the OAuth2 bearer token in Firestore (not a module-level variable) so all Cloud Function instances share one token and avoid unnecessary token endpoint requests.
- Always follow Worldpay's HATEOAS _links dynamically — never hardcode the payment authorization URL, as Worldpay can change internal paths without notice.
- Use Worldpay's hosted checkout page embedded in a Custom Widget WebView to tokenize card data, keeping raw card numbers outside FlutterFlow's widget tree and reducing PCI DSS scope.
- Test token expiry and refresh behavior explicitly before production launch — a mid-session token expiry causing a payment failure is a critical UX failure.
- Store all payment amounts as integers in the smallest currency unit (pence, cents) from the start — converting from decimals at the point of API call introduces rounding bugs.
- Register Worldpay settlement webhooks in a separate Firebase Cloud Function to update Firestore order status from 'authorized' to 'settled' — settlement typically occurs T+1.
- Build a settlement/status screen in FlutterFlow by reading the `status` field from the Firestore order document, which is updated server-side by the webhook handler — never rely solely on the API Call response for final order state.

## Use cases

### UK retail or hospitality app with card-not-present payments

A FlutterFlow app for a UK restaurant or hotel chain lets customers pre-authorize a card for a booking or pay for a room service order. The app collects a tokenized card reference (via Worldpay's hosted checkout) and triggers a charge through the Cloud Function proxy. Worldpay's UK acquiring relationships give favorable interchange rates for UK Visa/Mastercard transactions.

Prompt example:

```
A hotel booking app where guests pre-authorize their card on arrival — the app triggers a Worldpay authorization via the Cloud Function, and charges are captured at checkout with the stored payment reference.
```

### Multi-currency enterprise checkout in a B2B app

A FlutterFlow B2B platform serving European clients in multiple currencies uses Worldpay's multi-currency processing. The Cloud Function handles currency selection and passes the transaction amount and currency code to Worldpay's payment endpoint. Settlement status is polled from Worldpay's data endpoints and displayed on a reconciliation screen.

Prompt example:

```
A B2B invoice payment app where clients select their currency (EUR, GBP, USD), enter card details via Worldpay hosted fields, and submit payment — the app shows real-time authorization status from Worldpay.
```

### Subscription billing dashboard for enterprise SaaS

A FlutterFlow admin dashboard for a SaaS company shows recurring payment status fetched from Worldpay's reporting endpoints. Account managers can view successful and declined charges, retry failed payments by triggering a new Cloud Function call, and export transaction data displayed in a DataTable widget.

Prompt example:

```
An internal payments dashboard showing this month's Worldpay transactions, with filters by currency and status, and a 'Retry' button for declined charges that calls the Cloud Function proxy.
```

## Troubleshooting

### 401 Unauthorized returned by the Cloud Function when calling the Worldpay token endpoint

Cause: The client ID or client secret is incorrect, or the sandbox credentials are being used against the production URL (or vice versa). The worldpay.env Firebase config may not match the credentials stored in worldpay.client_id and worldpay.client_secret.

Solution: Run 'firebase functions:config:get' in the terminal to see all stored config values. Confirm that the env, client_id, client_secret, and merchant_entity_id all match the same Worldpay environment (sandbox or production). Re-deploy the function after any config change with 'firebase deploy --only functions:worldpayPayment'.

### OAuth2 token expiry causes 401 mid-session — payment fails after the app has been open for a while

Cause: The cached bearer token expired and the cache refresh logic was not triggered before the expiry window. This can happen if the token was cached by one Cloud Function instance and a different instance (or a cold start) doesn't have the cached value.

Solution: For production, store the cached token in Firestore with an expiry timestamp field instead of a module-level variable. All function instances will read and update the same Firestore token document. Check the expiry before every payment call and refresh if within 60 seconds of expiry.

```
// Store in Firestore: { token: '...', expiresAt: Timestamp }
// Check: if (!tokenDoc.exists || tokenDoc.data().expiresAt.toMillis() < Date.now() + 60000) refresh();
```

### HATEOAS links break — payment endpoint changes and existing Cloud Function calls fail

Cause: The Cloud Function hardcoded a payment endpoint URL instead of following the _links.payments:authorize.href from Worldpay's response. Worldpay can change these internal URLs without notice.

Solution: Always follow the _links URL from the Worldpay API response dynamically. In the Cloud Function, read initResponse.data._links['payments:authorize']?.href and use that URL for the authorization POST. Never hardcode Worldpay's internal path structure.

```
const authorizeUrl = initResponse.data._links?.['payments:authorize']?.href;
```

### 400 Bad Request — 'amount must be a positive integer'

Cause: The amount was passed as a decimal number (e.g., 9.99) instead of an integer in the smallest currency unit (e.g., 999 pence for GBP).

Solution: In FlutterFlow, multiply the display price by 100 and round to an integer before passing it to the API Call. Use a Custom Function: `int toSmallestUnit(double amount) => (amount * 100).round();` and bind its output to the amount variable in the ProcessPayment API Call.

```
int toSmallestUnit(double amount) => (amount * 100).round();
```

## Frequently asked questions

### What is the difference between the legacy Worldpay gateway and Access Worldpay?

Legacy Worldpay used XML/HTTPS-based request formats typical of older payment gateways from the early 2000s. Access Worldpay is the modern JSON REST API introduced as Worldpay's next-generation platform. If you're starting a new integration, always use Access Worldpay — the legacy gateway is deprecated for new merchants and less well-documented. This tutorial covers Access Worldpay exclusively.

### Why can't I just make direct API Calls from FlutterFlow to Worldpay without a Cloud Function?

Two reasons. First, your client secret (needed to mint OAuth2 tokens) would compile into the Flutter app bundle where it can be extracted from the APK or IPA by anyone. Second, Worldpay's HATEOAS responses require following dynamic _links URLs from one API response to the next — FlutterFlow's static API Call configuration can't natively follow dynamic links. The Cloud Function handles both problems: it keeps secrets server-side and executes the multi-step HATEOAS chain before returning a flat result.

### Can FlutterFlow apps collect raw card numbers and send them to Worldpay?

Technically possible, but strongly inadvisable. Collecting raw card numbers (PAN, CVV, expiry) in FlutterFlow widget text fields puts your application in PCI DSS SAQ D scope — the most complex compliance path, requiring annual audits, penetration testing, and infrastructure certification. Use Worldpay's hosted checkout page or a tokenization SDK instead. The hosted page collects and tokenizes card data within Worldpay's PCI-certified environment, and you only ever see a safe token reference.

### What is Worldpay's pricing?

Worldpay pricing is negotiated per-merchant and is not publicly listed. Rates depend on your transaction volume, business type, industry risk level, and geographic market (particularly UK bank acquiring). Contact Worldpay's sales team or your account manager for current rates. Unlike Stripe's published 2.9% + 30¢ rate, Worldpay's negotiated pricing can be more competitive at scale.

### How do I test 3-D Secure (3DS) flows with Worldpay in FlutterFlow?

Worldpay's hosted checkout page handles 3-D Secure 2 (3DS2) authentication within the hosted page itself — the challenge (biometric or OTP) happens in the hosted environment, and your FlutterFlow app sees only the final tokenized result. If you're using card tokenization server-side and need to handle the 3DS ACS redirect, the Cloud Function must return the challenge URL to FlutterFlow, which opens it in a WebView, and after completion, the Cloud Function receives the callback to complete the payment authorization. The full 3DS server-side flow is complex — test thoroughly in sandbox before production.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/worldpay
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/worldpay
