# Errors, rate limits and idempotency

> The one error format, what each error type means, how rate limits work, and how to retry safely with Idempotency-Key.

Source: https://veldhost.eu/docs/api-errors-and-limits

Last verified: 2026-11-13

Every veldhost API response follows the same rules for errors, limits, retries and paging. Write your client against this page and it works for every endpoint in the [endpoint list](https://veldhost.eu/docs/api-endpoints).

The API lives at `https://manage.veldhost.eu/api/v1`; in the examples below, `API_BASE` stands for that address. The OpenAPI document at `https://manage.veldhost.eu/api/v1/openapi.json` describes the same rules.

## The error format

Every `4xx` and `5xx` response has this body, whatever caused it:

```
{"error": {"type": "validation_error",
           "message": "The content field is required.",
           "details": {"content": ["The content field is required."]},
           "request_id": "req_01j9x3k8v2q0f4s7bq5e9a2m3n"}}
```

- `type` is stable: base your code's decisions on it.
- `message` is for people, and its wording may change.
- `details` appears on `422` and lists the problems for each field.
- `request_id` is also sent as the `X-Request-Id` header on **every** response, successful or not. Quote it when you contact support and we can find the exact request.

Some errors add a field next to `error`. The most common is `continue_url`: a portal page where a person finishes something the API does not do.

## Error types


| `type` | Status | What it means | What to do |
|---|---|---|---|
| `unauthenticated` | 401 | Token missing, wrong, expired or revoked | Check the header; [replace the token](https://veldhost.eu/docs/api-authentication) |
| `forbidden` | 403 | The token lacks the scope, is not allowed on this site, or your role may not do it | Add the scope to a new token, or ask an owner |
| `two_factor_required` | 403 | The account requires two-factor authentication and you have none | Set it up in the portal, then retry |
| `not_found` | 404 | No such resource for this token | Check the slug, zone or id |
| `conflict` | 409 | Not possible in the current state, such as a site that is not active, or a step for a person (see `continue_url`) | Open `continue_url`, or wait for the state to change |
| `idempotency_conflict` | 409 | This `Idempotency-Key` was already used for a different request | Use a new key |
| `idempotency_in_progress` | 409 | The first request with this key is still running | Retry shortly |
| `requires_order` | 402 | The action adds something paid | Propose an order; see [Ordering with an AI assistant](https://veldhost.eu/docs/ai-ordering) |
| `validation_error` | 422 | Invalid input | Fix the fields named in `details` |
| `rate_limited` | 429 | Too many requests | Wait `Retry-After` seconds |
| `server_error` | 500 | A fault on our side | Retry later; quote `request_id` if it persists |
| `upstream_error` | 502 | A system we depend on, such as a domain registry, did not answer | Retry later |
| `unavailable` | 503 | Briefly unavailable | Retry after `Retry-After`, if sent |

Other HTTP errors get a type named after their status, for example `method_not_allowed` (405) or `payload_too_large` (413). A few endpoints name a more specific type, such as `limit_reached` when an account is at a limit or `already_finished` when you cancel a job run that has ended. Treat an unknown `type` like its status code.

## Rate limits

There are two limits, and a request must stay within both:

- **Per token.** Each token, and each connected AI assistant, may make 120 requests a minute. The API and the MCP server share it, and it counts every request from that token, however many addresses they come from. Of those, at most 30 a minute may be long polls (requests with `wait_s`). The **Rate limit** line in the **Connect** panel on [API & MCP](https://manage.veldhost.eu/portal/api-tokens) shows the same number.
- **Per endpoint.** Each endpoint also has its own limit for each user, shared by all of that user's tokens. A few actions have tighter limits, some counted by the hour, such as release decisions, connecting a domain, redirects and orders. A busy endpoint does not use up another endpoint's limit.

Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` for the endpoint's limit. Going over either limit returns `429 rate_limited` with `Retry-After` in seconds. Wait that long. Do not retry in a tight loop.

Poll `/health` or `/deploys` at a calm interval. When you need a whole list, use the largest page size rather than many small pages.

## Retrying safely: Idempotency-Key

If a request times out, you cannot tell whether it ran. Retrying a deploy or a DNS change blindly might run it twice. To avoid that, send an `Idempotency-Key` header with your own unique value on the first attempt: up to 200 printable ASCII characters. A UUID is ideal. Use the same value on every retry:

```
curl -X POST -H "Authorization: Bearer mpc_YOUR_TOKEN" \
     -H "Idempotency-Key: 6c1f0d2e-9b7a-4f2e-8e1a-3d5c7b9a1f00" \
     API_BASE/sites/SITE_SLUG/deploy
```

- **Same key, same request**, within 24 hours: you get the stored first response, with the same status and body, and the header `Idempotent-Replayed: true`. Nothing runs again.
- **Same key, different request** (another method, path or body): `409 idempotency_conflict`. Each key belongs to one action.
- **While the first is still running**: `409 idempotency_in_progress`. Retry shortly to get its result.
- **Only successful responses are stored.** If the first attempt failed, fix the cause and retry with the same key.
- Keys belong to the token that sent them. A one-time secret, such as a new webhook's signing secret, is not shown again in a replayed response.

The endpoints that accept the header have **Yes** in the **Idempotency-Key** column of the [endpoint list](https://veldhost.eu/docs/api-endpoints). Without the header, an endpoint works exactly as it would otherwise.

## Paging through lists

List endpoints take `?limit=` and `?cursor=`, and return a `meta` object next to the list:

```
{"sites": [ ... ], "meta": {"next_cursor": "eyJvIjoyMH0", "has_more": true}}
```

Send `next_cursor` back as `cursor` until `has_more` is `false`. Do not try to read or build a cursor yourself. `limit` defaults to 20 and can be at most 100. A `limit` outside that range, or a damaged cursor, returns `422 validation_error`.

## Actions that ask twice

A few actions cannot be undone, such as enabling DNSSEC or dropping a warehouse database. When an AI assistant (an OAuth connection or the MCP server) calls one, the first call does nothing. It returns `409` with `"status": "needs_confirmation"`, a warning in `error.message` and a `confirm_token`. Repeat the same request with that `confirm_token` within 10 minutes of the first call, and only once the person has agreed. A token is tied to the person, the connection, the action and its exact arguments.

Enabling DNSSEC answers `409 conflict` with a `continue_url` to the domain's page when we do not host the zone, when the registry already holds a DS record that is not ours, or when the domain is not served by our nameservers alone. For a domain registered elsewhere it only signs the zone and returns the DS to add at your registrar, so there is nothing to confirm.

A script using a personal API token is not asked: its author chose the action when they wrote it. [Scopes and permissions](https://veldhost.eu/docs/scopes-and-permissions#actions-that-ask-first) lists these actions.

## Related

- [API tokens and authentication](https://veldhost.eu/docs/api-authentication)
- [All API endpoints](https://veldhost.eu/docs/api-endpoints)
- [API reference](https://veldhost.eu/docs/api-reference)