Hosted checkout
Accept payments by redirecting customers to a Fynex-hosted page — the lowest-PCI path to going live.
Hosted checkout is the simplest, lowest-friction way to accept a payment with Fynex. Card details are entered on a Fynex-hosted page, so your servers and frontend never touch raw PAN/CVV — your PCI obligations stay at the SAQ A level.
When to use it
- You want to ship fast.
- You don't have an existing card-collection UI.
- You're OK redirecting customers to a Fynex domain to complete the payment.
If you need to keep the customer on your own domain or already manage card data securely, use server-to-server instead.
Lifecycle
Note
Fynex delivers a
PaymentCompletedwebhook to the webhook URL(s) configured on your seller account (your receiver must return HTTP 200). To verify the final payment state — or as a backstop — pollGET /payments-api/v1/payments/{payment_id}(bearer token; the path value is yourexternalOrderRef) from your server, or subscribe to SSE events in the browser. See the Polling & SSE guide.
Step-by-step
- Create a checkout session from your backend
Send POST /payments-api/v1/checkout with a Bearer token and a unique Idempotency-Key header.
curl -sS -X POST "$FYNEX_API/payments-api/v1/checkout" \
-H "Authorization: Bearer $FYNEX_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"externalOrderRef": "ORDER-1042",
"amount": 49.00,
"currencyCode": "GBP",
"countryCode": "GB",
"returnUrls": {
"success": "https://example.com/orders/1042/success",
"failure": "https://example.com/orders/1042/failure"
},
"description": "Order #1042"
}'
You receive a 201 Created response:
{
"sessionId": "6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b",
"checkoutUrl": "https://pay.fynex.ai/checkout/6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b",
"expiresAt": "2026-04-29T13:30:00Z"
}
The TTL is set server-side; expiresAt tells you when the session will become invalid.
- Redirect the customer
Send a 302 to checkoutUrl, or render it as a button. Do not embed it in an iframe — browsers may block cross-origin iframe navigation.
res.redirect(302, session.checkoutUrl);
- Customer completes the payment on the hosted page
3DS challenges, if required, are handled internally by the hosted page. Your integration does not need to manage any 3DS redirect.
- Customer is redirected back
On completion, Fynex redirects the customer to returnUrls.success (payment completed) or returnUrls.failure (payment failed or abandoned). The redirect itself carries no authoritative payment state — do not grant fulfilment based on it.
- Verify the payment status
Poll GET /payments-api/v1/payments/{payment_id} (bearer token; the path value is your externalOrderRef) or use SSE to confirm the final state before fulfilling the order.
See Polling & SSE for code samples.
Request fields reference
Required fields
| Field | Type | Description |
|---|---|---|
externalOrderRef |
string | Your order reference. Stored on both the checkout session and the underlying payment. |
amount |
float | Amount in major units (e.g., 49.00 for £49.00). |
currencyCode |
string (3-letter) | ISO 4217 code, e.g. GBP. Normalized to upper-case server-side. Note: /checkout does not enforce a currency allowlist — an unsupported currency will be rejected at payment time, not at session creation. |
countryCode |
string (2-letter) | ISO 3166-1 alpha-2. Required despite the omitempty JSON tag. |
returnUrls.success |
string | URL the customer is redirected to on successful payment. |
returnUrls.failure |
string | URL the customer is redirected to on failure. Note the field name is failure, not cancel. |
Optional fields
| Field | Type | Description |
|---|---|---|
autoSettlement |
bool | When true, the payment auto-settles immediately; false leaves it for manual POST /capture. Omitted means the seller account's configured default. |
orderData.payeeDistribution |
array | Split funds across payees. Each element is { "payeeId": <int64>, "amountMinor": <int64> }, the share in integer minor units (8500 for £85.00). The deprecated { "payeeId": <int64>, "amount": <float> } in major units is still accepted; every element must use the same one. The shares must add up to the payment amount. See Split payments. |
sellerMerchantName |
string | Business name displayed on the hosted checkout page. |
logoUrl |
string | Logo URL displayed on the hosted checkout page. |
locale |
string | Language code (e.g., en). Falls back to the seller account locale, then en. |
description |
string | Order description shown to the customer (e.g., "Order #1042"). |
allowPromotions |
bool | When true, the customer may apply a coupon or Fynex wallet credit on the hosted page, and the amount charged is then lower than amount by the promotion. Defaults to false: the customer pays exactly amount. Promotions are offered only once Fynex has switched coupon checkout on. |
Caution
Fields that do NOT exist on this DTO:
successUrl,cancelUrl,expiresInMinutes,metadata,customerEmail,payeeId(top-level),splitRules(top-level). Do not send these — the server will ignore them silently or reject the request.
Split payments
To distribute the captured amount across multiple payees, include orderData.payeeDistribution. Retrieve payee IDs from GET /payments-api/v1/payees.
Note
payeeDistributiononly takes effect alongside an active Split Rule on your seller account — with no active rule, it's silently ignored and nothing is distributed. See Split Rules § Precedence when both are present for exactly how the two interact.
{
"externalOrderRef": "ORDER-1042",
"amount": 100.00,
"currencyCode": "GBP",
"countryCode": "GB",
"returnUrls": {
"success": "https://example.com/success",
"failure": "https://example.com/failure"
},
"orderData": {
"totalAmount": 100.00,
"payeeDistribution": [
{ "payeeId": 101, "amountMinor": 8500 },
{ "payeeId": 102, "amountMinor": 1500 }
]
}
}
amount and orderData.totalAmount are in major units. Each share's amountMinor is in integer minor units, so 8500 is £85.00. The shares must add up to amount in minor units, which is 10000 for the £100.00 above (or to orderData.totalAmount if you provide it). A distribution sent in the wrong unit cannot add up, so it is refused with 400. The deprecated major-units amount on a share is still accepted, but every share in one distribution must use the same unit — see Split Rules. What happens to the money afterwards (fees, rounding, holds, refunds) is described in Split Rules, section How Fynex splits money.
3DS handling
The hosted checkout page manages the entire 3DS flow internally. If the issuer requires a challenge, the customer completes it on the hosted page without ever leaving the Fynex-controlled flow. Your backend only sees the final outcome via polling/SSE — there is no requiresAction or redirectUrl on this path.
If you need direct control over the 3DS redirect (e.g., your own checkout UI), use server-to-server instead.
Common pitfalls
Warning
Do not fulfil the order based on the redirect alone. A user can navigate directly to
returnUrls.successwithout paying. Always verify the payment state server-side before dispatching goods or services.
Caution
401 seller auth is required
/checkoutanswers this when the request carries no authenticated seller — a missing, malformed or revoked Bearer token (/initialize-paymentphrases the same condition asmerchantId is missing in auth context). It is not anIdempotency-Keyproblem: a missing or non-UUID key answers400withIdempotency-Key header is required/Idempotency-Key must be a valid UUID, and that check runs only after the token has passed. Fix the token for a 401, the header for those 400s.
Idempotency-Keyreuse with a different body returns409 Conflict—allowPromotionsincluded. Use a fresh UUID per order.returnUrls.failure— notcancelUrl. The field is namedfailure. SendingcancelUrlhas no effect.currencyCodeis not validated at session creation. An unrecognized currency may be silently accepted at/checkoutand only rejected when the payment is processed. Test end-to-end in staging.- Amount is locked at session creation. To change the amount, create a new session.
Test cards
The hosted checkout page manages 3DS internally and always runs it — unlike the
server-to-server flow, the hosted page does not expose the skip3DS flag. Because 3DS always
runs, the PAN you use decides the authentication outcome, and a handle that does not
authenticate (threeDResult other than Y/A) cannot be settled.
For a happy-path success in staging use one of the frictionless-Y PANs, with any future
expiry date and any 3-digit CVV:
| Card number | Brand |
|---|---|
4000 0000 0000 2701 |
Visa |
5200 0000 0000 2235 |
Mastercard |
Important
Do not use
4111 1111 1111 1111here. It is not one of the authenticating PANs, so on the hosted checkout it always ends in a decline. It only works on flows that skip 3DS.
To exercise a decline or a failed 3DS challenge, use one of the challenge/failure PANs from the Test cards & sandbox guide, which lists the full set.
With no buyer at hand — a test suite, a CI job, an AI agent — a Demo account can pay its own
session with POST /test/checkout/{sessionId}/complete: Fynex submits this page with the
frictionless Visa above, so no card details pass through your code. See
Test cards & sandbox.
See also
- Checkout widget — Want to embed Fynex
- Polling & SSE — Verify payment outcomes without webhooks.
- Captures & refunds — Capture, partially capture, or refund a payment after the fact.
- Server-to-server payments — Take full control of the payment flow and 3DS redirect.