Skip to content
In this section

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

Your backendFynexCustomer browserPOSTresponseredirectsubmitredirectpoll statusCreate sessionPOST /checkoutSession createdsessionId, checkoutUrlRedirect customerto checkoutUrlHosted payment pageCard form and 3DSProcess paymentAuthorize and captureReturn to your sitereturnUrls.success or .failureVerify and fulfilWebhook or poll status

Note

Fynex delivers a PaymentCompleted webhook 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 — poll GET /payments-api/v1/payments/{payment_id} (bearer token; the path value is your externalOrderRef) from your server, or subscribe to SSE events in the browser. See the Polling & SSE guide.

Step-by-step

  1. 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.

  1. 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);
  1. 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.

  1. 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.

  1. 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

payeeDistribution only 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.success without paying. Always verify the payment state server-side before dispatching goods or services.

Caution

401 seller auth is required

/checkout answers this when the request carries no authenticated seller — a missing, malformed or revoked Bearer token (/initialize-payment phrases the same condition as merchantId is missing in auth context). It is not an Idempotency-Key problem: a missing or non-UUID key answers 400 with Idempotency-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-Key reuse with a different body returns 409 Conflict — allowPromotions included. Use a fresh UUID per order.
  • returnUrls.failure — not cancelUrl. The field is named failure. Sending cancelUrl has no effect.
  • currencyCode is not validated at session creation. An unrecognized currency may be silently accepted at /checkout and 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 1111 here. 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