Guides

Errors and failure codes

NATIO separates two different things. An error means the API refused the request. A failure means the request was accepted and the money movement did not happen. They have different shapes, and they need different handling.

The error envelope

Every error response, at every status code, has the same shape:

422 Unprocessable Entity
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_failed",
    "message": "body.currency: unsupported currency",
    "param": "currency",
    "details": [
      { "path": "currency", "message": "unsupported currency", "code": "custom" }
    ]
  },
  "request_id": "req_0uwP4ioIZ8q8dgBW"
}
FieldAlways presentMeaning
error.typeyesThe broad class of error. Maps to the HTTP status.
error.codeyesThe stable machine-readable identifier. Branch on this and nothing else.
error.messageyesA human sentence for logs and for your own operators. Wording can change; never parse it.
error.paramnoThe request field that caused the error, when a single field is responsible.
error.detailsnoStructured detail. For validation errors, one entry per failing path.
request_idyesMatches the x-request-id response header. Quote it in support requests.

Error types and HTTP status

error.typeHTTP statusMeaning
invalid_request_error400 / 413 / 415 / 422The request itself is wrong: malformed JSON, an unknown field, a failed validation rule or a body that is too large.
authentication_error401No API key was presented, or the key is unknown or revoked.
permission_error403The key is valid but not allowed to do this: missing permission, or the caller IP is outside the project allow-list.
not_found_error404No object with that id exists inside the merchant, project and mode of the key — or the route does not exist.
idempotency_error409 / 422An Idempotency-Key is in flight (409) or was reused with a different body (422).
rate_limit_error429Too many requests in the current window. Retry after the window resets.
state_error409The object cannot move to the requested state, for example capturing a payment that is not authorized.
provider_error503No eligible provider was available for the request.
risk_errorReserved for risk-layer rejections surfaced as an API error rather than a failed payment.
internal_error500An unexpected error. Quote the request_id when you contact support.

Common error codes

codetypeStatusMessage
validation_failedinvalid_request_error422One or more fields failed validation. details lists each path.
invalid_jsoninvalid_request_error400Request body is not valid JSON
unsupported_media_typeinvalid_request_error415Use application/json
payload_too_largeinvalid_request_error413Request body is too large
invalid_idempotency_keyinvalid_request_error400Idempotency-Key must be at most 255 characters
test_scenario_not_allowedinvalid_request_error400test_scenario is only accepted with test API keys
unauthorizedauthentication_error401Authentication required
invalid_api_keyauthentication_error401Invalid or revoked API key
forbiddenpermission_error403You do not have permission to perform this action
ip_not_allowedpermission_error403Request IP is not in the project allow-list
resource_not_foundnot_found_error404<Entity> <id> was not found
route_not_foundnot_found_error404No route for <METHOD> <path>
idempotency_in_progressidempotency_error409A request with this Idempotency-Key is still being processed
idempotency_key_reusedidempotency_error422Idempotency-Key was already used with a different request payload
invalid_state_transitionstate_error409<entity> cannot move from <status> to <status>
rate_limitedrate_limit_error429Too many requests
no_route_availableprovider_error503No eligible provider is available for this request
internal_errorinternal_error500An internal error occurred
Retry rules for errors
429 and 503 are worth retrying with backoff, and 409 idempotency_in_progress is worth retrying shortly. Every 4xx that describes the request itself — validation, an invalid key, an IP that is not allowed, a reused idempotency key — will produce the same answer forever. Retrying is safe only because your mutating calls carry an Idempotency-Key.

Payment failures are not errors

A payment that no provider would complete is still a successful API call: 201 Created, with status: "failed" and a failure object. The same object appears on each failed entry in attempts[], and on refunds and payouts that fail.

201 Created — a payment that failed
{
  "id": "pay_Bu0waXeXrCN5FbyPnbzy",
  "status": "failed",
  "failure": {
    "code": "stolen_card",
    "category": "hard",
    "message": "Stolen card, pick up"
  },
  "route": {
    "provider": { "code": "demo_acquirer_a", "name": "NATIO Demo Acquirer A" },
    "attempts": 1,
    "rule": "Cards → Acquirer A, fallback Acquirer B"
  }
}

Provider codes are not returned raw as the decision: each adapter normalises the provider vocabulary into one dictionary, so failure.code means the same thing regardless of which acquirer produced it. The unmapped provider values are still available on the attempt as provider_code and provider_message, for provider support tickets.

Categories drive retry policy

failure.category is the field that decides what happens next, inside NATIO and in your own code.

CategoryCascade behaviourWhat it means
softMay cascadeThe issuer or bank refused this attempt but the instrument itself is usable. NATIO may cascade the payment to the next eligible provider.
hardNever retriedThe instrument or the customer decision is final. NATIO never retries a hard decline on another provider; the payment fails immediately.
technicalMay cascadeThe provider, not the payment, failed. NATIO may fail over to the next eligible provider, after confirming that no charge was created.
policyNo further attemptThe orchestration layer itself stopped the payment: no eligible route, risk block, limits, or every provider already attempted.
Soft and technical may cascade. Hard never retries.
A soft decline or a technical failure lets NATIO try the next eligible provider inside the same request, which is what turns a single provider outage into a successful payment. A hard decline is a statement about the instrument or the customer: retrying it anywhere produces the same answer, costs a second authorisation attempt and can count against you at the issuer. NATIO will not do it, and neither should your code.

Failure dictionary

The complete set of normalised codes, grouped by category.

Soft declines soft · may cascade

Careful
The issuer or bank refused this attempt but the instrument itself is usable. NATIO may cascade the payment to the next eligible provider.
failure.codemessage
insufficient_fundsThe account has insufficient funds
do_not_honorThe issuer declined the transaction without a specific reason
issuer_unavailableThe issuer or bank could not be reached
try_again_laterTemporary decline; retry later
limit_exceededA transaction or velocity limit was exceeded
generic_declineThe transaction was declined

Hard declines hard · never retried

Important
The instrument or the customer decision is final. NATIO never retries a hard decline on another provider; the payment fails immediately.
failure.codemessage
card_declinedThe card was declined
stolen_cardThe card was reported stolen
lost_cardThe card was reported lost
expired_cardThe card has expired
invalid_cardThe card details are invalid
invalid_accountThe account is invalid or closed
restricted_cardThe card is restricted for this transaction
fraud_suspectedThe provider suspects fraud
authentication_failedCustomer authentication failed
cancelled_by_customerThe customer cancelled the payment
payment_expiredThe payment was not completed in time

Technical failures technical · may cascade

Note
The provider, not the payment, failed. NATIO may fail over to the next eligible provider, after confirming that no charge was created.
failure.codemessage
technical_errorThe provider returned a technical error
timeoutThe provider did not respond in time
provider_unavailableThe provider is unavailable
provider_configuration_errorProvider account is misconfigured

Policy outcomes policy · no further attempt

Good practice
The orchestration layer itself stopped the payment: no eligible route, risk block, limits, or every provider already attempted.
failure.codemessage
unsupported_currencyThe currency is not supported by any eligible provider
unsupported_methodThe payment method is not supported by any eligible provider
no_route_availableNo eligible provider route was found
risk_blockedThe payment was blocked by risk rules
amount_out_of_limitsAmount is outside provider limits
attempts_exhaustedAll eligible providers were attempted without success

A code that is not in this dictionary is possible in principle if a provider returns something entirely new; it is treated as a soft decline with the message The transaction could not be completed. Write your handling so that an unknown code degrades to the soft path rather than throwing.

Handling both in one place

Branch on the HTTP status first, then on the failure category. Never branch on a message string.

Node
const res = await fetch("https://api.natio.me/v1/payments", init);
const body = await res.json();

if (!res.ok) {
  // Transport-level error: the payment was not created.
  switch (body.error.code) {
    case "idempotency_in_progress":
      return retryShortly();                 // the first call is still running
    case "rate_limited":
      return backOff(res.headers.get("x-ratelimit-reset"));
    case "validation_failed":
      throw new BugInOurCode(body.error.details);
    default:
      throw new NatioError(body.error, body.request_id);
  }
}

// The call succeeded. The payment may still have failed.
if (body.status === "failed") {
  switch (body.failure.category) {
    case "hard":
      return declineCheckout(body.failure.code);   // never retry this instrument
    case "policy":
      return escalate(body.failure.code);          // configuration or risk, not the customer
    default:
      return offerAnotherMethod(body.failure.code); // soft / technical: already cascaded
  }
}
What you gotWhat to tell the customerWhat to do
failed · hardThat payment method was declined. Offer another one.Do not retry the same instrument. Log the code for your fraud and chargeback analysis.
failed · softThe payment did not go through. Try again or use another method.NATIO already cascaded across the eligible providers. A later retry can succeed; an immediate one usually will not.
failed · technicalSomething went wrong on our side. Please try again.Every eligible provider failed technically. Alert your operators — this is an availability signal, not a customer problem.
failed · policyNothing, in most cases — this is a configuration or risk outcome.Check routing coverage and provider limits for no_route_available and unsupported_*; check risk rules for risk_blocked.
4xx / 5xxNothing — the request never became a payment.Fix the request, or retry with the same Idempotency-Key. Log request_id either way.

Every failed payment also carries the whole story on its timeline: which providers were eligible, what each one answered and why the cascade stopped where it did.