Skip to content
In this section

Test cards & sandbox

Test card numbers, APM sandbox links, how to control 3DS, and how the Fynex sandbox behaves.

Note

Whether a card payment reaches real card networks is decided by your account's operational mode, not by the host you call. A Demo account routes to the upstream card processor's sandbox — no real card networks, no money movement. A Live account does not: test PANs on a Live account can reach real cards, so confirm your operationalMode before sending one.

Whether a payment goes through a 3DS challenge is controlled by the skip3DS request flag, not by the card number (see below).

This page is your self-service reference for sandbox testing.


How the sandbox works

Sandbox or live is a property of your ACCOUNT, not of the URL you call.

Your seller account carries an operationalMode, and that mode — not the hostname — decides where a payment goes:

  • Demo routes to the upstream processor's sandbox. No real card networks are involved and no money moves, so test card numbers are exactly what you should be using.
  • Live routes to real card networks. Never send a test card number on a Live account — it will be declined or, worse, charged to a real card if the number happens to exist.
  • The Fynex-hosted checkout page, the server-to-server flow, and the checkout widget all route through the same upstream sandbox when the account is in Demo mode.
  • Alternative payment methods (APMs) use a separate local-payment sandbox flow. Bancontact has its own public test PANs, Multibanco / MB WAY use the provider-hosted mock page, Wero uses a UAT consent site, and Swish credentials are provisioned separately. See Alternative payment methods (APM) for the step-by-step APM testing table.

Caution

Never send a test PAN on a Live account. Confirm your mode first: query your seller account and check operationalMode — the same check described in Going live. If it reads Live, test cards are not safe. Use real cards, or ask us to provision a Demo account for integration work.

Which base URL do I use?

Mode is independent of environment. A Demo account on https://api.fynex.ai is a sandbox account, and that is not a misconfiguration.

So the host does not tell you your mode, and your mode does not tell you the host. Use the base URL you were issued alongside your token.

The two environments keep entirely separate credential stores, so a token only authenticates against the host it was issued for. Pointing it at the other one returns HTTP 401 with code invalid_api_key (invalid authorization token). That means wrong host for this token — not that your credentials are broken, and not that you should switch hosts to fix it.

Note

Fynex delivers outbound PaymentCompleted webhooks to the URLs configured on your seller account (and to any per-request webhookUrl). Your receiver must return HTTP 200 for a delivery to count as successful — any other status is retried (up to 3 attempts) and then marked failed. You can also verify payment outcomes by polling GET /payments-api/v1/payments/{payment_id} (the path value is your externalOrderRef) or subscribing to the SSE stream — see the Polling & SSE guide.


Controlling 3DS

On the server-to-server flow (POST /payments-api/v1/initialize-payment), whether a 3DS flow runs is selected by the skip3DS flag in the request body. When 3DS does run, the card number then determines the authentication outcome (threeDResult — see Card numbers):

skip3DS Behaviour
true No 3DS. The response has requiresAction: false and the payment proceeds straight to authorization (lifecycleStatus captured, or authorized for a manual-capture payment). Use this for the no-redirect test flow.
false / omitted 3DS is requested. The response has requiresAction: true and an actionUrl. Redirect the customer to actionUrl, let them complete the challenge on the processor's sandbox page, then call POST /finalize-payment. Use this for the 3DS redirect test flow. See the 3DS guide.

On the hosted checkout (POST /checkout) the hosted page manages 3DS internally — you only observe the final outcome when polling.


Card numbers

Important

On this sandbox the card number selects the 3DS authentication outcome (threeDResult). skip3DS only controls whether a 3DS flow runs at all — when 3DS runs (e.g. on the hosted checkout, which always runs it), the PAN determines whether authentication succeeds (Y), challenges (C), or is rejected/unable (R/U/N). A handle whose authentication is not Y/A is failed by the processor's risk rule and cannot be settled — the settle call comes back with a field-level error saying the payment handle is in a non-payable state. Use the right PAN.

Cards that authenticate successfully (threeDResult=Y, frictionless — no challenge)

Use these for a happy-path success on the hosted checkout — they authenticate without an OTP and reach lifecycleStatus captured:

Card number Brand
4000 0000 0000 2701 Visa — frictionless Y
5200 0000 0000 2235 Mastercard — frictionless Y

Cards that trigger a 3DS challenge (threeDResult=C)

These redirect to the processor's 3DS emulator, where you select the authentication status/reason to test (choose Y to complete successfully):

Card number Brand
4000 0000 0000 2503 / 4000 0000 0000 2370 / 4000 0000 0000 2420 Visa — challenge
5200 0000 0000 2490 / 5200 0000 0000 2151 / 5200 0000 0000 2664 Mastercard — challenge

Cards that fail authentication (for decline/UI testing)

Card number Outcome
4530 9100 0001 2345, 4500 0300 0000 0004, 4037 1122 3300 0001 threeDResult=U (unable to authenticate) → risk-rule rejected
4000 0000 0000 2925 threeDResult=N (not enrolled)

Unless noted otherwise, use:

  • Expiry month: any future month, 2 digits (e.g. 12)
  • Expiry year: any future year. 4-digit (2028) is recommended. A 2-digit year (28) is also accepted — Fynex widens it to 2028 before calling the processor.
  • CVV / CVC: any 3-digit number (e.g. 123); American Express CID: any 4-digit number
  • Cardholder name: Latin letters (A-Z), spaces, apostrophes, dots, or hyphens only

Note

The card numbers listed in this section are the set Fynex supports; the upstream sandbox can add or retire PANs without notice, so email support@fynex.ai if one stops behaving as documented. On a Live account a real issuer returns Y for a genuine card, so the U/N failure modes above are sandbox-only artifacts of the test PANs.


Forcing a specific failureCode

The card numbers above select a 3DS authentication outcome — they don't let you deterministically produce a specific numeric failureCode (e.g. testing your handling of 2003 ProviderTimeout versus 3002 AuthenticationRequired versus 2001 ProviderDeclined). For that, send the Simulator request header on a Demo-mode account:

Simulator: force_failure=2003

The payment is stamped failed with exactly that failureCode — same status, failureDescription, retry and failureCategory as a real failure, and the same PaymentCompleted webhook fires — but no call reaches the card processor at all. This makes failure-handling tests fully deterministic: no dependency on the upstream sandbox's own behavior, and no risk of the failure mode drifting if the processor's sandbox changes.

  • Any code in the failure code catalogue can be forced, except 0 (no failure) and 9999 (unclassified) — those describe the absence of a classified failure, not one to simulate. An unrecognised or malformed value returns 400.
  • Demo-mode only. On a Live account the Simulator header is never parsed or acted on, whatever it contains — it cannot affect a real payment.
  • Applies to POST /payments-api/v1/initialize-payment.
curl -sS -X POST "$FYNEX_API/payments-api/v1/initialize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Simulator: force_failure=2003" \
  -d '{
    "externalOrderRef": "TEST-SIMULATE-TIMEOUT-1",
    "amount": 19.99,
    "paymentType": "card",
    "paymentMethod": "card",
    "currencyCode": "GBP",
    "countryCode": "GB",
    "autoSettlement": true,
    "skip3DS": true,
    "cardData": {
      "cardNumber": "4111111111111111",
      "expMonth": 12,
      "expYear": 2028,
      "holderName": "Test User",
      "cvv": "123"
    },
    "returnLinks": [
      { "rel": "default", "href": "https://example.com/return", "method": "GET" }
    ]
  }'

Important

The initialize-payment response itself does NOT carry the simulated failureCode. A simulated failure follows the exact same path a real authorization-stage failure does, which returns 502 with a generic body — same as any other authorization-time failure on this call (validation errors return 400, an idempotency conflict 409, and so on; those aren't what a Simulator request produces, since it always reaches the authorization stage):

{ "error": "the request could not be processed - please try again or contact support" }

The requested failureCode, status: "failed", and matching failureDescription are visible on the payment row: poll GET /payments-api/v1/payments/{paymentId} (using your externalOrderRef), or read them off the PaymentCompleted webhook, which fires exactly as it would for a real failure. This is the same behavior every other authorization-stage failure already has — see the Errors guide — the simulator does not special-case it.

The exception is the two card refusals made before authorization: force_failure=1007 and force_failure=1008 return 422 with a machine-readable code (card_expired, card_brand_not_supported), exactly as a real expired card or unsupported brand does — see Common 422 responses in the Errors guide.


Common test scenarios

Happy path — no 3DS

curl -sS -X POST "$FYNEX_API/payments-api/v1/initialize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "externalOrderRef": "TEST-HAPPY-1",
    "amount": 19.99,
    "paymentType": "card",
    "paymentMethod": "card",
    "currencyCode": "GBP",
    "countryCode": "GB",
    "autoSettlement": true,
    "skip3DS": true,
    "cardData": {
      "cardNumber": "4111111111111111",
      "expMonth": 12,
      "expYear": 2028,
      "holderName": "Test User",
      "cvv": "123"
    },
    "returnLinks": [
      { "rel": "default", "href": "https://example.com/return", "method": "GET" }
    ]
  }'

The response has requiresAction: false and the payment's lifecycleStatus becomes captured. Poll GET /payments-api/v1/payments/{payment_id} to confirm before fulfilling the order.

Note

countryCode is required and, if your terminal pins a country, must match it. returnLinks (note: not returnUrls) is required on the server-to-server flow — each link needs rel ∈ default | on_completed | on_failed | on_cancelled, an http/https href, and method: "GET". The hosted checkout flow uses returnUrls at session creation instead.

3DS challenge

Send the same request without skip3DS (or with "skip3DS": false). The response is:

{
  "paymentId": "TEST-3DS-1",
  "status": "provider_pending",
  "lifecycleStatus": "pending",
  "legacyStatus": "provider_pending",
  "requiresAction": true,
  "actionUrl": "https://3ds.example.com/.../authentication/..."
}

Redirect the customer's browser to actionUrl, let them complete the sandbox challenge, then call POST /finalize-payment with the same paymentId. See the 3DS guide for the full localStorage bridge pattern.

Decline / failure UI testing

A payment that the processor declines (or a 3DS challenge the customer fails) transitions to failed; a customer who abandons the challenge returns cancelled. When you poll for status, the response includes:

{
  "paymentId": "TEST-DECLINE-1",
  "status": "failed",
  "lifecycleStatus": "failed",
  "legacyStatus": "failed",
  "failureCode": 2001,
  "failureDescription": "Card declined by issuer"
}
Field Purpose
lifecycleStatus "failed" for hard declines; "cancelled" if the customer cancelled
failureCode Numeric code — see the Errors guide for the full list
failureDescription Human-readable reason — do not display verbatim to customers

Caution

Distinguish "cancelled" from "failed". A customer who clicks "Back" or abandons the form returns a cancelled status — show a neutral "payment not completed" message rather than an error.

Refund testing

  1. Pay and wait for the payment to become refundable — lifecycleStatus captured and settlementStatus not in_flight (see Captures & Refunds).
  2. Issue the refund using the public endpoint with your externalOrderRef as the path param:
curl -sS -X POST "$FYNEX_API/payments-api/v1/payments/TEST-REFUND-1/refund" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "amount": 19.99 }'

Caution

Only payments whose lifecycleStatus is captured can be refunded — a pre-capture refund attempt returns 409 "payment action is not allowed for the current payment status". Omit amount to refund the remaining refundable amount, or pass a smaller value for a partial refund.

Partial capture

Create a manual-capture payment (autoSettlement: false), then capture less than the authorized amount:

# Authorize £50
curl -sS -X POST "$FYNEX_API/payments-api/v1/initialize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "externalOrderRef": "TEST-PARTIAL-1",
    "amount": 50.00,
    "paymentType": "card",
    "paymentMethod": "card",
    "currencyCode": "GBP",
    "countryCode": "GB",
    "autoSettlement": false,
    "captureMode": "manual",
    "skip3DS": true,
    "cardData": {
      "cardNumber": "4111111111111111",
      "expMonth": 12,
      "expYear": 2028,
      "holderName": "Test User",
      "cvv": "123"
    },
    "returnLinks": [
      { "rel": "default", "href": "https://example.com/return", "method": "GET" }
    ]
  }'

# Capture only £35
curl -sS -X POST "$FYNEX_API/payments-api/v1/finalize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "paymentId": "TEST-PARTIAL-1", "amount": 35.00 }'

The response capturedAmount will be 35.00. The remaining £15 is released back to the customer's card.


Test customer data

The sandbox does not validate billing data against real services. Use any well-formed values:

Field Suggested test value Notes
Email test@example.com Any *@example.com address works; no emails are sent from sandbox
Billing name Test User Any non-empty string
Billing address 1 Test Street, London, EC1A 1BB, GB Staging does not run AVS checks against real addresses
Phone +44 7700 900000 Any well-formed +44 number; not validated
Postcode / ZIP EC1A 1BB Not validated by AVS in sandbox

Paying a Demo checkout without a browser

A hosted checkout session is paid by its buyer, on its checkoutUrl. When there is no buyer — a test suite, a CI job, an AI agent — a Demo account can pay its own session with one call. Fynex submits the session's checkout page with the frictionless Visa above (4000 0000 0000 2701) and the test customer data, runs 3DS and completes the payment:

curl -sS -X POST "$FYNEX_API/test/checkout/6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b/complete" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
{ "status": "completed", "paymentId": "ORDER-100106", "lifecycleStatus": "captured" }

The path takes the sessionId POST /checkout returned. No card details are sent to or returned by the call, and the payment is the session's own: GET /payments/{paymentId}, webhooks and events see an ordinary card payment. A session created with manual capture ends authorized; capture it before settling. Repeating the call is safe — a session that was already paid answers already_completed. It is refused on a Live account (403, code demo_only), for another account's session (404, checkout_session_not_found), once the session has expired (410, checkout_expired), and with the checkout page's own refusal otherwise.

A Demo account takes payments in one currency, its own — GET /account shows it. A sandbox from POST /sandbox/accounts gets it from the country you send: GBP for GB (the default), USD for US, EUR for a euro-area country such as DE. A session in any other currency is refused with 422 currency_not_supported.


Settling a Demo payment

The sandbox acquirer never sends a settlement report, so Fynex settles Demo payments itself: a completed Demo payment in GBP, USD or EUR of at most 500 (so never more than £500) settles on its own within about a minute — its split runs and the wallets are credited. A bigger Demo payment is never settled automatically: settle it yourself once it has completed (for manual capture, once it is captured). The call also settles a smaller one at once instead of waiting:

curl -sS -X POST "$FYNEX_API/test/payments/ORDER-100106/settle" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
{ "status": "settled", "paymentId": "ORDER-100106" }

The call takes a few seconds; the split runs right after it returns, so GET /payments/{paymentId}/split-decisions, wallet balances and payouts then behave as they will in production. Repeating the call is safe — a payment that was already settled, by an earlier call or by the automatic settlement, answers already_settled. It is refused on a Live account (403, code demo_only) and before the payment has completed (409, payment_not_completed).


What does NOT work in sandbox

Feature Status Notes
Real customer email receipts Sent on a Demo account There is no operational-mode gate on the email path, so a Demo account on production sends real mail to whatever address you supply — use addresses you control when testing
KYB / identity checks Separate sandbox Contact your Fynex representative for a KYB sandbox link; it is independent of the payments sandbox
Apple Pay merchant validation Real, when enabled The hosted checkout page validates through /api/v1/checkout/apple-pay-validate-merchant (session-bound; active only where Apple Pay is enabled for the deployment and a merchant certificate is configured). Your own Apple Pay button must run merchant validation from your own server in every environment
Google Pay in production mode Not available The dashboard uses environment: 'TEST'; switch to environment: 'PRODUCTION' only when going live

Sandbox limits

Limit Value Notes
Rate limiting Enforced The same mechanism as production, with its own budget — read X-RateLimit-Remaining; tell Fynex before high-volume load testing
Maximum test amount £10,000 (recommended) No hard cap is enforced by Fynex, but use reasonable amounts; very large amounts may be rejected by the upstream sandbox's own rules
Minimum test amount £0.01
Session expiry Set server-side Check expiresAt in the checkout session response

See also

  • Quickstart — Take your first test payment in 5 minutes.
  • Hosted checkout — Full hosted checkout reference — optional fields, return URL handling, and more.
  • Server-to-server — POST /initialize-payment and POST /finalize-payment, including 3DS handling.
  • 3DS Authentication — The redirect lifecycle, localStorage bridge, and finalize call.
  • Alternative payment methods (APM) — Sandbox steps for Bancontact, Multibanco, MB WAY, Wero, and Swish.
  • Errors — HTTP status codes, failure codes, and error response shapes.