Hosted checkout
The fastest integration: your server creates a Payment, you redirect the customer to the Finscale-hosted page, and a webhook tells you the outcome. Finscale renders the method UI, collects card data, and runs 3-D Secure — none of it touches your servers.
How it works
Two ideas carry the whole flow: the redirect is presentation — the webhook is truth. The customer coming back to return_url proves nothing about the money; payment.succeeded does.
Create the payment
One server-side call. Pass the method your customer picked — or let the hosted page offer every method enabled on your account for the currency by omitting payment_method. return_url is required for redirect flows:
curl https://api.finscale.dev/v1/payments \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_ord_9f21_0716" \
-H "Content-Type: application/json" \
-d '{
"amount": 4900,
"currency": "EUR",
"payment_method": "ideal",
"reference": "ord_9f21_0716",
"customer": { "email": "anna@example.com" },
"return_url": "https://shop.example.com/checkout/return"
}'
{
"id": "pay_8Q2mX4nT1cVb",
"object": "payment",
"amount": 4900,
"currency": "EUR",
"status": "requires_action",
"payment_method": "ideal",
"reference": "ord_9f21_0716",
"provider": "prov_eu_acq_01",
"next_action": {
"type": "redirect",
"url": "https://pay.finscale.dev/r/8Q2mX4nT"
},
"created_at": "2026-07-16T09:24:31Z",
"livemode": false
}
Redirect the customer to next_action.url — a plain HTTP 302, a link, or a client-side navigation all work. The URL is single-use and tied to this payment.
pk_test_51FinscaleDemo…) has one job: initializing checkout in the browser. It can't read payments, refund, or list anything — see Authentication.
What the hosted page handles
Everything between the redirect and the return is Finscale's problem:
| Concern | On the hosted page |
|---|---|
| Method UX | Bank pickers for redirects, QR codes for real-time rails, wallet sheets, card fields — each method's native flow, in the customer's language. |
| Card data | PAN, expiry, and CVC are collected on pay.finscale.dev, built to PCI DSS Level 1 standards. Your servers stay out of scope for card data entirely — the SAQ-A story in Security & PCI. |
| 3-D Secure | Challenges run inside the hosted flow when the issuer requires one. No extra integration — see Cards & 3DS. |
| Risk screening | Card payments are scored before routing; blocked payments never reach a provider. See Risk engine. |
| Routing & failover | The same smart routing as every other integration — hosted checkout adds nothing and removes nothing. |
Handle the return
When the customer lands back on return_url, fetch the payment and branch on status — it may still be settling the last hop:
status at return | Show the customer | Then |
|---|---|---|
succeeded | Order confirmation | Fulfil when payment.succeeded arrives (it may already have). |
processing | "Payment in progress" | Wait for the webhook — never poll in a loop. Common for bank rails. |
requires_action | Checkout again, politely | The customer abandoned the hosted page. Offer to retry with a fresh redirect. |
failed | A retry with another method | failure_code says why — don't echo it verbatim to the customer. |
Webhook events
The hosted flow emits the standard payment events — register an endpoint in Webhooks:
payment.requires_action— the payment is waiting on the customer (fires at create for redirect methods).payment.processing— customer action complete, transaction submitted to a provider.payment.succeeded— funds captured. Fulfil the order on this event, exactly once (key onevent.id).payment.failed— no provider approved it. The payment'sfailure_codehas the normalized reason.payment.risk_review— held for manual review; the payment staysprocessinguntil resolved.
Error scenarios
| What happens | What you see |
|---|---|
Invalid create request (bad currency, float amount, missing return_url for a redirect method) | 400 invalid_request_error — nothing was created; fix and retry. |
| Customer abandons the hosted page | The payment idles in requires_action, then expires to canceled. No webhook until it does. |
| Customer's bank or card declines | payment.failed with a normalized failure_code (e.g. insufficient_funds). The hosted page shows the customer a retry. |
| Same Idempotency-Key, different payload | 409 idempotency_error — see Idempotency. |
| Risk screening blocks the payment | payment.failed, failure_code: payment_blocked. Test it with "amount": 4992. |
Related
- API integration — when you want your own payment UI instead.
- Payment lifecycle — every status the payment moves through.
- Payment methods — what the hosted page can offer per currency.
- Security & PCI — why hosted checkout keeps you in SAQ-A territory.