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"}}
typeis stable: base your code's decisions on it.messageis for people, and its wording may change.detailsappears on422and lists the problems for each field.request_idis also sent as theX-Request-Idheader 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.