33 — Developer

Errors

Every provider speaks its own dialect of failure. Finscale normalizes all of them into one envelope, one set of types, and one decline-code vocabulary — whichever provider ran the transaction.

The error envelope

Any non-2xx response carries a single error object:

402 Payment Required
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "message": "The card was declined by the issuer.",
    "doc_url": "https://docs.finscale.dev/errors/#card_declined",
    "request_id": "req_7Hf3kQd2"
  }
}
FieldAlways presentMeaning
typeYesWhich class of failure — drives your handling branch. See error types.
codeUsually — null when no specific code appliesMachine-readable specific cause, stable across API versions.
messageYesHuman-readable explanation. For logs and dashboards — don't show it verbatim to customers, and don't parse it.
paramValidation errorsThe request field that failed, e.g. "param": "currency". Only on invalid_request_error.
doc_urlMost errorsDeep link to the relevant section of this page.
request_idYesQuote this id (req_7Hf3kQd2) in support requests — it indexes the full request trace.

A validation failure looks like this — note param:

400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "amount must be a positive integer in minor units (4900 = €49.00).",
    "param": "amount",
    "request_id": "req_7Hf3kQd2"
  }
}

Error types

TypeHTTP statusWhat it meansRetry?
invalid_request_error 400 / 404 Malformed request or unknown resource. param names the offending field. No — fix the request.
authentication_error 401 Missing, invalid, expired, or under-scoped API key. See Authentication. No — fix the key.
card_error 402 The payment was declined. code carries the decline code — the normal, expected failure class of a payments API. Only with a new instrument or per the decline code.
idempotency_error 409 An Idempotency-Key was reused with a different payload. See Idempotency. No — use a fresh key.
rate_limit_error 429 Too many requests. Honor the Retry-After response header. Yes, with backoff.
api_error 5xx Something failed on Finscale's side. Rare by design. Yes — safely, with the same idempotency key.

Decline codes

When type is card_error (or a local-method equivalent), code tells you why — already normalized across providers. Hard declines are final for that instrument; soft declines are circumstantial and eligible for failover or a later retry.

CodeKindMeaningWhat to do
card_declinedhardGeneric issuer decline with no further detail given.Ask the customer for another payment method.
do_not_honorhardThe issuer refused without a reason code. Common and opaque.Don't retry the same card; offer an alternative method.
insufficient_fundshardNot enough balance or credit at authorization time.Retry later or offer another method. Test with 4000 0000 0000 9995.
expired_cardhardThe card's expiry date has passed.Ask for updated card details.
incorrect_cvchardSecurity code check failed.Ask the customer to re-enter their details.
incorrect_numberhardThe card number fails validation (Luhn or length).Ask the customer to re-enter the number.
lost_cardhardReported lost by the cardholder.Never retry. Request a different instrument.
stolen_cardhardReported stolen. The generic message deliberately doesn't say so to the customer.Never retry. Request a different instrument.
fraud_suspectedhardDeclined by issuer or provider risk screening.Don't auto-retry; route the order to manual review.
authentication_requiredsoftThe issuer demands 3-D Secure before approving.Re-create the payment; Finscale returns requires_action with the challenge redirect.
processing_errorsoftA transient error occurred while processing at the provider.Nothing — failover retries this automatically.
provider_unavailablesoftThe routed provider was down or timed out.Nothing — failover retries on the next provider automatically.
limit_exceededsoftThe transaction exceeds a velocity or amount limit at the issuer or method (common with Blik and bank transfers).Retry later or with a smaller amount.
currency_not_supportedhardNo connected provider can process this currency/method pair.Charge in a supported currency or enable another method.
payment_canceledn/aThe customer abandoned or canceled the redirect flow (iDEAL bank page, 3DS challenge, …).Invite the customer to try again; the payment is terminal.
Codes are the contract; messages are not

Branch on error.code. The message string can be reworded at any time without an API version bump.

How failover interacts with declines

Declines come in two kinds, and smart routing treats them very differently:

  • Soft declinesprovider_unavailable, processing_error, timeouts — say something about the provider, not the customer. Finscale retries the transaction on the next-best provider automatically, inside the same API call. You only ever see an error if every eligible provider fails.
  • Hard declinesinsufficient_funds, stolen_card, expired_card, fraud_suspected — say something about the instrument. Re-sending the same card to a different acquirer would produce the same answer (and hurt your fraud metrics), so Finscale surfaces the decline immediately and never re-routes it.

The provider_attempts trail on the payment object shows exactly what happened:

GET /v1/payments/pay_8Q2mX4nT1cVb — after a failover
{
  "id": "pay_8Q2mX4nT1cVb",
  "status": "succeeded",
  "provider": "prov_eu_acq_02",
  "provider_attempts": [
    { "provider": "prov_eu_acq_01", "outcome": "timeout", "decline_code": null },
    { "provider": "prov_eu_acq_02", "outcome": "approved" }
  ]
}

Simulate this yourself with the magic amount 4999 — see Testing.

A minimal handling recipe

Branch on HTTP status first, then on error.type:

  1. 2xx — inspect status on the payment object; requires_action means redirect the customer, not success.
  2. 402 card_error — show a customer-friendly message keyed on error.code; offer another payment method.
  3. 409 idempotency_error — a bug on your side; log request_id and generate keys per logical operation, as described in Idempotency.
  4. 429 — back off for Retry-After seconds, then retry with the same idempotency key.
  5. 5xx or network timeout — retry with the same idempotency key; the replay guarantee makes this safe even if the original request actually went through.