> For the complete documentation index, see [llms.txt](https://support.swisspay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://support.swisspay.ai/api-reference/errors.md).

# Errors

SwissPay returns conventional HTTP status codes plus a JSON body with a stable `error.code` and a human-readable `error.message`.

```json
{
  "error": {
    "code": "invalid_params",
    "message": "amount must be a positive integer"
  }
}
```

The `code` is **stable** — match against it in your code. The `message` may evolve to be clearer over time and should be treated as human-readable only.

## Error catalogue

| HTTP      | `code`                       | When you'll see it                                                                 |
| --------- | ---------------------------- | ---------------------------------------------------------------------------------- |
| 400       | `missing_idempotency_key`    | No `Idempotency-Key` header on `POST /payments`.                                   |
| 401       | `missing_api_key`            | No `Authorization` header.                                                         |
| 401       | `invalid_api_key`            | Unknown or revoked key.                                                            |
| 404       | `customer_not_found`         | Payment referenced an unknown `cus_...`, or one belonging to a different merchant. |
| 404       | *(no body)*                  | Any other unknown ID (`pay_...`, etc.) returns 404 without a body.                 |
| 409       | `key_reused`                 | Same `Idempotency-Key`, different request body.                                    |
| 422       | `invalid_params`             | Validation failure — see *Common 422 causes* below.                                |
| 422       | `customer_email_taken`       | Email already on file for this merchant (unique per merchant, case-insensitive).   |
| 422       | `customer_external_id_taken` | `external_id` already on file for this merchant.                                   |
| 422       | `provider_not_configured`    | Your account has no active payment-provider connection.                            |
| 502 / 503 | `provider_error`             | The upstream payment provider failed or timed out.                                 |

## Decline handling

**Declined card payments return HTTP 200**, not a 4xx. We do this because the API call itself succeeded — the provider received the authorisation request and the issuer made a decision. The decision happened to be "no".

```json
{
  "id": "pay_01HABC...",
  "status": "failed",
  "failure": {
    "code": "refused",
    "reason": "Refused by issuer"
  },
  "amount": 2999,
  "currency": "CHF"
}
```

If your test framework asserts only on HTTP status, declines will silently pass as successes. Always check `status` first:

```python
res = requests.post(url, json=body, headers=headers)
res.raise_for_status()              # 4xx / 5xx → exception
data = res.json()
if data["status"] != "succeeded":   # 200 OK with failure
    handle_decline(data["failure"]["code"], data["failure"]["reason"])
```

## Failure codes

When `status: "failed"`, the `failure.code` will be one of:

| Code                                      | Meaning                                                                          |
| ----------------------------------------- | -------------------------------------------------------------------------------- |
| `refused`                                 | Generic issuer refusal — most common.                                            |
| `expired_card`                            | The card on file is past its expiry.                                             |
| `insufficient_funds`                      | Self-explanatory.                                                                |
| `lost_card`, `stolen_card`, `pickup_card` | Card reported in the issuer's risk lists.                                        |
| `3ds_failed`                              | The cardholder failed the 3-D Secure challenge.                                  |
| `3ds_abandoned`                           | The cardholder never completed the challenge.                                    |
| `3ds_token_expired`                       | The cardholder arrived at the challenge page after the 15-minute window.         |
| `3ds_not_available`                       | Your connection is in 3DS-required mode but the issuer can't perform 3-D Secure. |

## Common 422 causes

* `amount` ≤ 0 or non-integer.
* Bad `email` (failed RFC 5322 validation).
* `locale` not in `xx-XX` form.
* `metadata` exceeds 20 keys or a value exceeds 500 chars.
* `payment_method.holder_name` missing (required on every card payment).
* `success_url` / `failure_url` missing or non-HTTPS when 3-D Secure is enabled.

## What to log

For every API request, log the **request ID** we return in the `Swisspay-Request-Id` response header. If something goes wrong and you contact support, quoting that ID gets you to a resolution faster than any other piece of information.

## Retries

* `5xx` and connection timeouts: safe to retry with the **same** `Idempotency-Key`.
* `4xx`: don't retry — the request is malformed; fix it first.
* `200 status: failed` (decline): don't retry the same card. The customer can choose a different payment method.
