> ## Documentation Index
> Fetch the complete documentation index at: https://docs.feddi.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Reference

> Every typed error code the Partner API returns, the HTTP status it pairs with, and the exact recovery action. Branch on error.code, never on the HTTP number.

## The one rule

Branch on `error.code`. Every failure is a typed string code in `error.code`, never a bare HTTP number.

The HTTP status is a coarse hint for proxies and logs. The contract lives in the string. Two failures can share status 422 and need opposite handling, so a switch on the number routes you wrong. A switch on `error.code` routes you right.

```jsonc theme={null}
{
  "ok": false,
  "data": null,
  "error": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "Actual plus released promo cannot cover the debit.",
    "details": { "shortfall_minor": 1200, "available_actual_minor": 1800, "available_promo_minor": 500 }
  },
  "meta": { "request_id": "uuid", "idempotency_replayed": false, "api_version": "2026-06-01" }
}
```

<Info>
  When a code carries a valid set (supported currencies, supported credentials, candidate programs),
  it lives in `error.details`. Read `details` instead of hard-coding the set. The list is per-integration
  and changes as your enablement changes; the runtime source of truth is `GET /capabilities`.
</Info>

## The complete table

| Code                          | HTTP | Meaning                                                                                                                                                                                                                                         | What to do                                                                                      |
| ----------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `INVALID_API_KEY`             | 401  | Missing or invalid `x-api-key` or terminal JWT (expired, malformed, wrong audience).                                                                                                                                                            | Re-auth. Refresh the terminal JWT: `GET /auth/token/validate`, then `POST /auth/token`.         |
| `FORBIDDEN`                   | 403  | Valid key, not authorized for this merchant or branch scope. Also returned for a `PENDING_PROOF` wallet on a payment, and on a transaction read when the transaction exists but belongs to another integration.                                 | Check your tenant scope, or route the customer to enrollment.                                   |
| `NOT_FOUND`                   | 404  | Resource absent. On most endpoints a cross-tenant id also collapses to 404 to avoid leaking existence or PII; transaction reads are the exception (cross-tenant returns 403 `FORBIDDEN`, never 404, so their 404 means a genuinely unknown id). | Verify the ids belong to your tenant.                                                           |
| `VALIDATION_ERROR`            | 400  | Malformed body, missing field, or a domain rule violated.                                                                                                                                                                                       | Read `details` for the per-field errors. Fix and retry with a NEW idempotency key.              |
| `INSUFFICIENT_FUNDS`          | 402  | Actual plus released promo cannot cover the debit. `details`: `shortfall_minor`, `available_actual_minor`, `available_promo_minor`.                                                                                                             | Route to top-up or recovery. The credential is NOT consumed; retry after the reload.            |
| `IDEMPOTENCY_KEY_REUSED`      | 422  | Same `Idempotency-Key`, different payload.                                                                                                                                                                                                      | Fix key generation. A key is stable for one logical operation and unique across different ones. |
| `CONFLICT`                    | 409  | Concurrent state collision (for example, a version-CAS on a budget envelope).                                                                                                                                                                   | Re-read current state, then retry.                                                              |
| `RATE_LIMITED`                | 429  | Too many requests.                                                                                                                                                                                                                              | Back off per the `Retry-After` header. `details` and `X-RateLimit-*` headers carry the limits.  |
| `CURRENCY_NOT_SUPPORTED`      | 422  | No active provider or program for the requested currency.                                                                                                                                                                                       | `details.supported` lists the valid set. Pick a supported currency.                             |
| `WALLET_PROGRAM_AMBIGUOUS`    | 422  | The customer resolves to more than one active program.                                                                                                                                                                                          | Pass a `wallet_program_id` from `details.candidates`.                                           |
| `CREDENTIAL_TYPE_UNSUPPORTED` | 422  | The credential type is not enabled for this integration.                                                                                                                                                                                        | `details` lists the supported types. Read `GET /capabilities`; use a supported type.            |
| `REQUIRES_DYNAMIC_CREDENTIAL` | 422  | The path needs a dynamic credential (QR or OTP), not a static one.                                                                                                                                                                              | Mint or collect the dynamic credential, then retry.                                             |
| `INTERNAL_SERVER_ERROR`       | 500  | A Feddi-side error.                                                                                                                                                                                                                             | Retry with the SAME idempotency key (safe; the call replays). Escalate if it persists.          |

<Info>
  This is the complete `error.code` enum the API returns today. The set grows additively, so treat an
  unrecognized code as a non-retryable failure: surface it, log it, and check `GET /openapi` for the
  current set.
</Info>

## The codes that carry a valid set

Four codes return the answer in `details`. Read it instead of guessing.

<ResponseField name="INSUFFICIENT_FUNDS.details" field-type="object">
  `shortfall_minor`, `available_actual_minor`, and `available_promo_minor`, all minor integers. The
  credential is not consumed, so the customer can top up the `shortfall_minor` and you can retry the
  same payment.
</ResponseField>

<ResponseField name="CURRENCY_NOT_SUPPORTED.details" field-type="object">
  `supported`: the array of ISO-4217 currency codes with an active provider for this integration.
</ResponseField>

<ResponseField name="WALLET_PROGRAM_AMBIGUOUS.details" field-type="object">
  `candidates`: the array of `wallet_program_id` values the credential resolves to. Re-send the call with one
  of them in `wallet_program_id`.
</ResponseField>

<ResponseField name="CREDENTIAL_TYPE_UNSUPPORTED.details" field-type="object">
  The supported credential types for this integration. The authoritative live source is
  `GET /capabilities`.
</ResponseField>

## INSUFFICIENT\_FUNDS is a recovery path, not a dead end

Model `INSUFFICIENT_FUNDS` as a recovery, never a bare decline.

The credential is not consumed on this failure. A failed debit leaves the OTP retryable and the QR nonce unspent, so the same payment succeeds after a reload.

<Steps>
  <Step title="Read the shortfall" icon="calculator" title-type="p">
    `details.shortfall_minor` is the exact gap. `available_actual_minor` and `available_promo_minor`
    are the current balance split.
  </Step>

  <Step title="Top up the gap" icon="wallet" title-type="p">
    Credit the wallet by at least `shortfall_minor`. Confirm the top-up against the provider settlement
    reference with `POST /topup/confirm`, which credits the cashable balance and accrues any reload bonus
    in one atomic operation.
  </Step>

  <Step title="Retry the original payment" icon="rotate-ccw" title-type="p">
    Retry with a NEW idempotency key (the first attempt's key replays the failure). The credential was
    never consumed, so it still resolves.
  </Step>
</Steps>

<Info>
  A standalone top-up entry point is not yet available. Today, confirm a top-up against a provider
  settlement reference with `POST /topup/confirm`. Read `GET /capabilities` for the operations enabled
  for your integration.
</Info>

## Retrying safely

Two failures are safe to retry with the SAME `Idempotency-Key`, and the distinction matters for money.

<Columns cols="2">
  <Card title="INTERNAL_SERVER_ERROR (500)" icon="server" horizontal="false">
    Retry with the SAME key. If the original call committed, you get a byte-identical replay with
    `meta.idempotency_replayed: true`. If it did not, the retry executes. Either way, no double-debit.
  </Card>

  <Card title="RATE_LIMITED (429)" icon="timer" horizontal="false">
    Back off for the seconds in `Retry-After`, then retry with the SAME key. The same-key rule still
    protects you against a double-debit if an earlier attempt slipped through.
  </Card>
</Columns>

Everything else needs a NEW key. A `VALIDATION_ERROR`, `INSUFFICIENT_FUNDS`, or `IDEMPOTENCY_KEY_REUSED` means the request as sent will never succeed. Fix the request, then retry under a fresh `Idempotency-Key`. Reusing the old key with a changed payload returns `IDEMPOTENCY_KEY_REUSED`.

<Warning>
  `IDEMPOTENCY_KEY_REUSED` is a key-generation bug on your side, not a transient error. A key must be
  stable for one logical operation and unique across different ones. Never retry it; regenerate the key
  for the corrected request.
</Warning>

## Branching in code

Switch on the string, handle the carriers of a valid set, and treat the two retry-safe codes apart from the rest.

<CodeGroup tabs="TypeScript,JSON">
  ```typescript theme={null}
  if (!res.ok) {
    switch (res.error.code) {
      case "INSUFFICIENT_FUNDS":
        return routeToTopup(res.error.details.shortfall_minor); // credential not consumed
      case "WALLET_PROGRAM_AMBIGUOUS":
        return retryWithProgram(res.error.details.candidates[0]);
      case "CURRENCY_NOT_SUPPORTED":
        return pickSupportedCurrency(res.error.details.supported);
      case "CREDENTIAL_TYPE_UNSUPPORTED":
      case "REQUIRES_DYNAMIC_CREDENTIAL":
        return collectDynamicCredential();
      case "RATE_LIMITED":
      case "INTERNAL_SERVER_ERROR":
        return retrySameKey(res); // safe replay
      default:
        return surfaceToCashier(res.error); // VALIDATION_ERROR, FORBIDDEN, NOT_FOUND, CONFLICT, ...
    }
  }
  ```

  ```json theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "WALLET_PROGRAM_AMBIGUOUS",
      "message": "This phone resolves to more than one active program.",
      "details": { "candidates": ["prog_8f1c...", "prog_2a90..."] }
    },
    "meta": { "request_id": "uuid", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```
</CodeGroup>

<Info>
  This list is stable for the codes above, and the contract grows additively. Treat an unrecognized
  `error.code` as a non-retryable failure: surface it, log it, and check `GET /openapi` for the current
  set rather than crashing.
</Info>

## Where to go next

<Columns cols="2">
  <Card title="Idempotency & Errors" href="/concepts/idempotency-and-errors" icon="repeat" horizontal="false">
    The conceptual model behind the same-key replay rule and the typed-error envelope.
  </Card>

  <Card title="Conventions" href="/api-reference/conventions" icon="ruler" horizontal="false">
    The request and response envelope, headers, and the minor-units money rule.
  </Card>

  <Card title="Authentication" href="/api-reference/authentication" icon="key" horizontal="false">
    Resolving `INVALID_API_KEY` and `FORBIDDEN`: keys, the terminal JWT, and tenant scope.
  </Card>

  <Card title="Money: Actual vs Promotional" href="/concepts/money-classes" icon="coins" horizontal="false">
    The balance split that `INSUFFICIENT_FUNDS.details` reports.
  </Card>
</Columns>
