API integration
Full control: your UI, your flow, the Finscale API server-to-server. Two sequences cover almost every integration — the direct charge that resolves inside one request, and the redirect round-trip when the customer has to authenticate. Both end the same way: a signed webhook.
Direct API vs. hosted checkout
Choose hosted checkout when you want the fastest path and Finscale's payment UI. Choose the direct API when you own the checkout experience — saved cards, one-click reorders, subscriptions, in-app flows. The objects, statuses, routing, and webhooks are identical; only who renders the UI changes. One constraint is not negotiable: raw card numbers never transit your servers — direct card charges use saved payment methods (pm_…) or network tokens, per Security & PCI.
Sequence 1 — direct charge, no customer action
A saved card, no 3-D Secure required. The whole lifecycle fits inside your request — routing included:
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": "card",
"payment_method_id": "pm_7Wq2xN9dT4Ls",
"customer": "cus_4T8nQb2Wp1Zr",
"reference": "ord_9f21_0716"
}'
{
"id": "pay_8Q2mX4nT1cVb",
"object": "payment",
"amount": 4900,
"amount_captured": 4900,
"currency": "EUR",
"status": "succeeded",
"payment_method": "card",
"payment_method_id": "pm_7Wq2xN9dT4Ls",
"reference": "ord_9f21_0716",
"provider": "prov_eu_acq_01",
"risk": {
"score": 17,
"decision": "approved",
"checks": { "avs_result": "pass", "cvv_result": "pass" }
},
"next_action": null,
"created_at": "2026-07-16T09:24:31Z",
"livemode": false
}
Even here, treat the synchronous response as a convenience and the webhook as the record. If your connection drops after Finscale processed the charge, the webhook still arrives — and your Idempotency-Key makes the retry safe.
Sequence 2 — the 3-D Secure redirect round-trip
When the issuer demands authentication (or the method is a redirect rail like iDEAL), the payment pauses in requires_action and you send the customer to next_action.url. The round-trip:
The same sequence serves every requires_action method — swap "3-D Secure challenge" for a bank picker (iDEAL), an app approval (PIX, UPI), or a wallet sheet. Your code doesn't branch per method: if next_action is present, redirect; if it's null, you're done waiting on the customer.
The integration, step by step
-
Create with an Idempotency-Key
Derive the key from your order (
idem_ord_9f21_0716) so retries can never double-charge. Storepayment.idagainst the order immediately. -
Branch on the response
status: "succeeded"— done.requires_action— redirect tonext_action.url.processing— show "in progress" and wait for the webhook.failed— offer another method;failure_codesays why. -
Fulfil from webhooks only
Handle
payment.succeededandpayment.failedon a verified webhook endpoint. Dedupe onevent.id— delivery is at-least-once and unordered. -
Reconcile the edges
A daily sweep of payments stuck in non-terminal states (
GET /v1/payments?status=requires_action) catches abandoned checkouts and lets you expire orders cleanly.
Deferred confirmation
By default, creating a payment confirms it immediately (confirm: true). Pass confirm: false to build the payment early — at cart time — and route it only when the customer commits. The payment waits in requires_confirmation, and you can switch the instrument at confirm time:
curl https://api.finscale.dev/v1/payments/pay_8Q2mX4nT1cVb/confirm \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Content-Type: application/json" \
-d '{ "payment_method_id": "pm_7Wq2xN9dT4Ls" }'
# → the payment leaves requires_confirmation and enters routing
Confirming a payment in any other state returns a 400 invalid_request_error. Unconfirmed payments can be canceled with POST /v1/payments/{id}/cancel at no cost — nothing was ever routed.
Error scenarios
| Scenario | API behavior | Your move |
|---|---|---|
| Validation failure (float amount, unknown method) | 400 invalid_request_error, code parameter_invalid | Fix the request — nothing was created. |
| Card declined by the issuer | 402 card_error, code card_declined — or payment.failed async | Show a retry with another method. Never loop on a final decline. |
| Network error mid-request | Unknown outcome on your side | Retry with the same Idempotency-Key — you'll get the original result, whatever it was. |
| All providers down for the corridor | payment.failed after failover exhausts the chain; provider_attempts shows each hop | Rare by design. Surface a "try again shortly" state. |
| Risk review hold | payment.risk_review; status stays processing | Hold fulfilment. Resolution arrives as payment.succeeded or payment.failed. |
| Rate limited | 429 rate_limit_error + Retry-After | Back off and retry idempotently. See Rate limits. |
Related
- Payment lifecycle — the full state machine these sequences move through.
- Authorization flows — pre-auth, incremental authorization, partial capture.
- Cards & 3DS — when challenges fire and what exemptions exist.
- Webhooks — signatures, retries, and duplicate handling.
- SDKs — curl-first, plus generating a typed client from
/openapi.json.