Errors, rate limits and idempotency

Last verified

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.

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
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
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 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. 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 lists these actions.

Stuck? Contact support — replies within one business day.