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

# Errors

> Understand the error envelope, HTTP status codes, and how to handle failures

When a Payment API request fails, Quidkey returns a consistent JSON envelope and a meaningful HTTP status code. Use the status code and `code` for programmatic handling, and `message` for logging.

## Error Envelope

Every error response has `success: false` and an `error` object:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable error message",
    "metadata": {
      "errors": [
        {
          "field": "amount",
          "message": "Amount must be greater than 0"
        }
      ]
    }
  }
}
```

| Field                   | Description                                                                   |
| ----------------------- | ----------------------------------------------------------------------------- |
| `error.code`            | Stable, machine-readable error code. Branch on this in your code.             |
| `error.message`         | Human-readable description. Use for logs and debugging, not for control flow. |
| `error.metadata.errors` | Present on validation failures (`400`). An array of per-field problems.       |

<Tip>
  Always branch on `error.code`, never on `error.message`. Messages may be reworded over time; codes are stable.
</Tip>

## HTTP Status Codes

| Status        | Meaning                          | Notes                                                                                |
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
| `200` / `201` | Success                          | Request processed; resource created on `201`.                                        |
| `400`         | `VALIDATION_ERROR`               | Malformed or invalid input. Field-level details in `error.metadata.errors`.          |
| `401`         | `NO_TOKEN` / `INVALID_TOKEN`     | Missing, malformed, or expired access token. Branch on the **status**, not the code. |
| `403`         | Forbidden                        | Authenticated, but insufficient permission for this action.                          |
| `404`         | Not found                        | Resource does not exist **or** belongs to another tenant.                            |
| `409`         | `IDEMPOTENT_REQUEST_IN_PROGRESS` | A request with the same idempotency key is still in flight.                          |
| `410`         | Gone                             | Resource expired or revoked (e.g. `PAYMENT_LINK_EXPIRED`).                           |
| `422`         | Unprocessable                    | Request is well-formed but cannot be fulfilled.                                      |
| `500`         | Internal error                   | Server-side issue (rare). Safe to retry idempotent requests.                         |
| `503`         | `IDEMPOTENCY_STORE_UNAVAILABLE`  | Idempotency store temporarily down. Retryable.                                       |

## Validation Errors (400)

A `400` indicates the request body or parameters failed validation. The `error.metadata.errors` array pinpoints each offending field, so you can surface precise feedback.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "metadata": {
      "errors": [
        {
          "field": "amount",
          "message": "Amount must be a positive integer in minor units"
        },
        {
          "field": "currency",
          "message": "currency must be a valid ISO 4217 code"
        }
      ]
    }
  }
}
```

<Tip>
  Amounts are **integer minor units**: `1999` means £19.99, not £1,999. Sending a decimal or a major-unit value is a common source of `400` validation errors. See [Amounts & Currencies](/guides/payment-api/concepts/amounts-and-currencies).
</Tip>

## Cross-Tenant Access Returns 404

If you request a resource that exists but belongs to **another merchant**, Quidkey returns `404`, not `403`.

<Warning>
  This is deliberate. A `403` would confirm the resource exists, leaking information across tenants. Quidkey returns `404` so a resource you cannot access is **indistinguishable** from one that does not exist. Do not treat a `404` as proof a payment was never created.
</Warning>

## Authentication & Permission Errors

| Code / Status                        | Meaning                             | Resolution                                                                                              |
| ------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `401` (`NO_TOKEN` / `INVALID_TOKEN`) | Missing or expired token            | Refresh your access token and retry. See [Authentication](/guides/payment-api/concepts/authentication). |
| `403` Forbidden                      | Token lacks the required permission | Verify the credentials have access to this operation.                                                   |
| `404` Not found                      | Resource missing or cross-tenant    | Check the ID; confirm it belongs to your merchant.                                                      |

## Error Codes

Branch on these stable `error.code` values:

| Status | `error.code`                     | Meaning                                                                                       |
| ------ | -------------------------------- | --------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_ERROR`               | Request body or parameters failed validation. Field-level details in `error.metadata.errors`. |
| `409`  | `IDEMPOTENT_REQUEST_IN_PROGRESS` | A request with the same idempotency key is still in flight. Retry with the same key.          |
| `410`  | `PAYMENT_LINK_EXPIRED`           | The payment link has passed its expiry.                                                       |
| `410`  | `PAYMENT_LINK_NOT_ACTIVE`        | The payment link is not in an active state.                                                   |
| `422`  | `MISSING_CONVERTED_AMOUNT`       | A required converted amount was not supplied.                                                 |
| `503`  | `IDEMPOTENCY_STORE_UNAVAILABLE`  | Idempotency store temporarily down. Retryable.                                                |

## Handling Errors

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch(url, options);
  const body = await response.json();

  if (!body.success) {
    const { code, message, metadata } = body.error;

    // A 401 (code NO_TOKEN or INVALID_TOKEN) means the token is missing or expired.
    if (response.status === 401) {
      // Refresh the access token and retry once
    } else switch (code) {
      case 'IDEMPOTENT_REQUEST_IN_PROGRESS':
        // Wait briefly, then retry with the SAME idempotency key
        break;
      case 'IDEMPOTENCY_STORE_UNAVAILABLE':
        // Retry with backoff; the request was not processed
        break;
      case 'VALIDATION_ERROR':
        // Surface field errors to the caller
        for (const e of metadata?.errors ?? []) console.error(e.field, e.message);
        break;
      default:
        console.error(code, message);
    }
  }
  ```

  ```python Python theme={null}
  response = requests.post(url, **options)
  body = response.json()

  if not body['success']:
      error = body['error']
      code = error['code']

      # A 401 (code NO_TOKEN or INVALID_TOKEN) means the token is missing or expired.
      if response.status_code == 401:
          ...  # Refresh the access token and retry once
      elif code == 'IDEMPOTENT_REQUEST_IN_PROGRESS':
          ...  # Wait briefly, then retry with the SAME idempotency key
      elif code == 'IDEMPOTENCY_STORE_UNAVAILABLE':
          ...  # Retry with backoff; the request was not processed
      elif code == 'VALIDATION_ERROR':
          for e in error.get('metadata', {}).get('errors', []):
              print(e['field'], e['message'])
      else:
          print(code, error['message'])
  ```

  ```bash cURL theme={null}
  # Inspect the status code and body
  curl -i 'https://core.quidkey.com/api/v1/payment-requests:redirect' \
    -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
    -H 'Content-Type: application/json' \
    -d '{ "amount": 1999, "currency": "GBP", "locale": "en-GB" }'
  ```
</CodeGroup>

<Note>Body abbreviated. See the [Redirect guide](/guides/payment-api/accept-a-payment/redirect) for the full required payload.</Note>

<Info>
  **Retry guidance:** retry `409`, `503`, and `500` with exponential backoff. Do **not** blindly retry `400`, `401`, `403`, `404`, or `410`; fix the request or credentials first.
</Info>

## Next Steps

<CardGroup cols={2}>
  <Card title="Idempotency" icon="fingerprint" href="/guides/payment-api/concepts/idempotency">
    Safe retries for create requests
  </Card>

  <Card title="Authentication" icon="key" href="/guides/payment-api/concepts/authentication">
    Resolve 401 errors with token refresh
  </Card>

  <Card title="Amounts & Currencies" icon="coins" href="/guides/payment-api/concepts/amounts-and-currencies">
    Avoid the most common validation error
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    Response format and conventions
  </Card>
</CardGroup>
