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:

Your server Finscale prov_eu_acq_01 POST /v1/payments — payment_method_id: pm_… authorize (routed) approved 201 — status: succeeded webhook payment.succeeded
POST /v1/payments — saved card
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"
  }'

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:

Customer Your server Finscale Card issuer pays on your checkout POST /v1/payments 201 requires_action + next_action.url redirect to next_action.url opens hosted 3-D Secure page challenge authenticated → authorize redirect to return_url webhook payment.succeeded order confirmation

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

  1. Create with an Idempotency-Key

    Derive the key from your order (idem_ord_9f21_0716) so retries can never double-charge. Store payment.id against the order immediately.

  2. Branch on the response

    status: "succeeded" — done. requires_action — redirect to next_action.url. processing — show "in progress" and wait for the webhook. failed — offer another method; failure_code says why.

  3. Fulfil from webhooks only

    Handle payment.succeeded and payment.failed on a verified webhook endpoint. Dedupe on event.id — delivery is at-least-once and unordered.

  4. 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:

POST /v1/payments/{id}/confirm
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

ScenarioAPI behaviorYour move
Validation failure (float amount, unknown method)400 invalid_request_error, code parameter_invalidFix the request — nothing was created.
Card declined by the issuer402 card_error, code card_declined — or payment.failed asyncShow a retry with another method. Never loop on a final decline.
Network error mid-requestUnknown outcome on your sideRetry with the same Idempotency-Key — you'll get the original result, whatever it was.
All providers down for the corridorpayment.failed after failover exhausts the chain; provider_attempts shows each hopRare by design. Surface a "try again shortly" state.
Risk review holdpayment.risk_review; status stays processingHold fulfilment. Resolution arrives as payment.succeeded or payment.failed.
Rate limited429 rate_limit_error + Retry-AfterBack off and retry idempotently. See Rate limits.
  • 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.