# 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://support.swisspay.ai/api-reference/errors.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
