# Errors and rate limits

> Error response shape, all HTTP status codes the API deliberately returns, retry guidance, and per-path rate limits.

## Error response shape

All API errors use the same JSON envelope:

```json
{ "error": "Hungarian error message" }
```

The `error` field is a human-readable string.

> [!WARNING]
> API error messages are in **Hungarian only**, regardless of the `Accept-Language` header. This will change before general availability. See [Conventions — Language](conventions.md#language) for examples.

Do not parse the `error` string to decide how to retry or branch your code. Use the HTTP status code instead.

## HTTP status codes

The table below lists every HTTP status the API **deliberately** returns and what your client should do.

| Status | When | What to do |
|---|---|---|
| `200` | Success. | Read the response body. |
| `201` | Resource created (`POST` endpoints that create records). | The new resource `id` is in the response body. |
| `400` | Invalid or malformed request: missing required field, type mismatch, out-of-range value, or invalid JSON. | Fix the request; do not retry unchanged. |
| `401` | Missing credentials, or the credentials provided are invalid. | Check your `Authorization` header or Basic auth config. |
| `403` | Authenticated but not permitted. Example: a state-changing request failed the CSRF origin check. | Do not retry without fixing the request origin or authentication context. |
| `404` | Unknown path or resource ID not found. | Verify the path and ID; do not retry. |
| `405` | HTTP method not supported on this path. | Use the correct method; do not retry. |
| `409` | Conflict — the operation cannot proceed in the current state. Example: `POST /api/webhooks/order` with an out-of-stock SKU. Error body includes the specific SKU: `"Elfogyott: DEMO-5"`. | Fix the conflicting condition (e.g. check stock), then retry. |
| `413` | Request body exceeds the [size limit](conventions.md#body-limit) (1 MB default; 10 MB for `POST /api/feeds/import`). | Reduce the payload size; do not retry unchanged. |
| `423` | Resource locked. | Retry after the lock is released. |
| `429` | Rate limit exceeded. | Wait at least 60 seconds and retry. See [Rate limits](#rate-limits). |
| `500` | Unexpected server error. | Retry up to 3 times with exponential back-off (1 s, 2 s, 4 s). Report persisting errors to the Haszno team. |
| `503` | Upstream dependency unavailable (e.g. the AI service required by `POST /api/products/:id/ai-description`). | Retry with back-off; this is typically transient. |

## Retry guidance

### Safe to retry

- **`GET` requests on `5xx`**: retry up to 3 times with exponential back-off (1 s → 2 s → 4 s).
- **`429` responses**: wait at least 60 seconds before retrying.

### Not safe to retry blindly

Write operations (`POST`, `PUT`, `PATCH`, `DELETE`) on `5xx` responses may have partially succeeded server-side. Before retrying:

1. Check the current resource state with the matching `GET`.
2. Use the `batchId` returned by `haszno_proposals_approve` / `POST /api/proposals/approve` to roll back an approved batch if needed.

> [!TIP]
> `POST /api/products/apply-prices` and `POST /api/proposals/approve` return a `batchId` or `applied` count. Record these values before retrying to avoid applying prices twice.

### Never retry

Do not retry `400`, `401`, `403`, `404`, `405`, or `409` responses. The error will recur until you fix the request.

## Rate limits

> [!NOTE]
> Rate limits are per-instance on the hosted Vercel deployment. Because serverless function invocations may land on different cold-start instances, counters are not shared across instances. The effective per-IP budget may be higher than the figures below in practice. For self-hosted deployments, counts are shared within a single process.

Limits are enforced per client IP. The window for all general write limits is **60 seconds**.

### General write limits

Matching is by path prefix; the first matching rule wins.

| Method | Path prefix | Limit per 60 s |
|---|---|---|
| `POST` | `/api/billing/plan` | 5 |
| `POST` | `/api/shop-connections` | 10 |
| `POST` | `/api/support/conversations` | 20 |
| `PUT` | `/api/support/conversations` | 10 |
| `POST` | `/api/partners/apply` | 10 |
| `POST` | `/api/feeds/import` | 20 |
| `POST`, `PUT`, `PATCH` | All other paths | 200 |
| `DELETE` | All paths | 100 |

`GET` requests have no published rate limit. Avoid polling faster than once per minute; use a local cache for background polling.

### Authentication limits

These limits are separate and use different windows.

| Endpoint | Limit |
|---|---|
| `POST /api/auth/register` | 5 per IP per hour |
| `POST /api/auth/login` (by IP) | 30 per IP per 15 minutes |
| `POST /api/auth/login` (by email) | 10 per email address per 15 minutes — **fail-closed**: further login attempts for that email return `429` even with the correct password until the window expires |

### Rate limit response

```json
{ "error": "Túl sok kérés — próbáld újra egy perc múlva." }
```

HTTP status: `429`. The message translates to: "Too many requests — try again in a minute."

The [authentication limits](#auth-limits) return `429` with a different message: `"Túl sok próbálkozás. Próbáld újra néhány perc múlva."` ("Too many attempts. Try again in a few minutes.").
