# Errors and limits

## The error body

Every error, on every route, is JSON with one field, `error`, and an HTTP
status that says what kind of error it is:

```
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: Rz8vKm3pWq6tXc1nLb9sHd4fJg7yAe2M

{"error": "minimum payment in USDT_TRC20 is $6.44 right now (network fees)"}
```

Branch on the **status**, and log the **message**. Messages are written for
developers, in English, and may be reworded over time: do not match on their
exact text, and do not show them to your customers.

Every response, error or not, carries an `X-Request-Id` header. Quote it when
you write to payitgo's support: it finds the request in the logs.

## Status codes

| Status | Meaning | Typical causes | Retry? |
|---|---|---|---|
| `200 OK` | Done | A read, an update, or `POST /v1/payments` returning the order's open payment | |
| `201 Created` | Created | A new payment, key, user, store or payout | |
| `202 Accepted` | Queued | A webhook test or a redelivery: it is sent within seconds | |
| `400 Bad Request` | The request is invalid | A missing or malformed field, an unsupported token, an amount under the minimum, a webhook URL that is not `https://` | No: fix the request |
| `401 Unauthorized` | Not authenticated | `missing api key`, `invalid api key` (unknown or revoked), `missing token`, `invalid or expired token`, `invalid credentials` | No: fix the key, or refresh the access token |
| `403 Forbidden` | Authenticated, but not allowed | Live key while live mode is off, a suspended account, a role without the permission (`insufficient permissions`), a storefront key changing the account's webhook, a read-only key creating or changing something, a payment for a customer you blocked, a wrong password | No |
| `404 Not Found` | No such thing | An unknown id, a payment of the other mode, an order with no open payment, an unknown route (`Not Found`) | No |
| `409 Conflict` | It clashes with the current state | An email already registered, removing the last owner, choosing the coin of a payment twice, simulating a test payment while its simulation runs, cancelling a payout that is no longer `requested` | No |
| `413 Request Entity Too Large` | The body is over 1 MiB | | No |
| `429 Too Many Requests` | Over the rate limit | See below | Yes, after a short wait |
| `500 Internal Server Error` | Something failed on payitgo's side | The message is generic (`payment creation failed`, `could not set the webhook`…); the details are in payitgo's logs | Yes, with backoff |

The message for each error of each endpoint is listed on its page:
[Payments](/docs/payments#errors), [Checkout](/docs/checkout),
[Webhooks](/docs/webhooks#the-webhook-api),
[Authentication](/docs/authentication#api-key-errors),
[Account and team](/docs/account).

## Rate limits

| Routes | Limit | Counted per |
|---|---|---|
| `/v1/*` | 20 requests a second, bursts up to 40 | API key |
| `/merchant/login`, `/merchant/signup` | 5 requests a second, bursts up to 10 | IP address |
| `POST /checkout/{checkout_token}/token` | 5 requests a second, bursts up to 10 | IP address |
| `POST /checkout/{checkout_token}/simulate` | 5 requests a second, bursts up to 10 | IP address |

Over the limit you get:

```
HTTP/1.1 429 Too Many Requests

{"error": "rate limit exceeded"}
```

Wait a second and retry. Webhooks make polling unnecessary for almost every
integration: if you hit the limit, you are probably polling payments you
could be told about.

## Retrying safely

Networks fail. These requests can be sent again without doing anything twice:

| Request | Why it is safe |
|---|---|
| `POST /v1/payments` with the same `order_id` | While the order has an open payment you get that payment back (`200`), not a second one |
| `PUT /v1/webhook` | Setting the URL it already has changes nothing |
| `DELETE /v1/webhook` | Removing a URL that is already removed changes nothing |
| `GET` anything | Reads change nothing |

`POST /v1/webhook/test` is harmless to repeat, but each call sends one more
`ping`.

On a `5xx` or a network error, retry with exponential backoff (for example
1 s, 2 s, 4 s, up to a minute). On a `4xx` other than `429`, retrying the same
request gets the same answer.

## Conventions

- **Requests** are JSON with `Content-Type: application/json`. Without that
  header the body is not read.
- **Amounts** are decimal strings in responses (`"49.9"`, `"0.00117"`). Requests
  accept a JSON number or a decimal string. Parse responses with a decimal type,
  not a float.
- **Times** are RFC 3339 in UTC (`2026-10-10T09:00:00.112Z`). The number of
  fractional digits varies: use a real RFC 3339 parser.
- **Optional fields** are left out of a response when they have no value. Code
  that reads them must handle their absence.
- **New fields** may appear in responses at any time, and unknown fields in
  requests are ignored: parse leniently.
- **Lists** take `limit` (1 to 200, default 50) and `offset`, and return
  `total`.
- **Ids** are opaque strings: store and compare them whole, and never read
  meaning into their shape. An id is 24 lowercase hex characters
  (`6710c3a1f2b4e5d6a7b8c9d0`), whatever it is: a payment, a settlement, an
  event, an API key. API keys and webhook signing secrets are 64 hex
  characters; they do not say their mode either (a key's `livemode` does).
