Public testing phase We’re in a public testing phase — feel free to look around, but we’re not taking orders yet. Ordering opens 1 October 2026. See plans →

API reference

The veldhost API lets you script everything the portal buttons do: deploys, logs, health, staging, cron, backups, domains and DNS. It powers the mpc CLI and works from any HTTP client, CI pipeline or AI agent.

Authentication

Create a token under API tokens and send it as a bearer token:

curl -H "Authorization: Bearer mpc_..." https://manage.veldhost.eu/api/v1/sites

When you create a token you choose which apps it can touch and what it may do: read (always on), deploy, manage (staging, cron, backups & restore) and dns. Rate limit: 120 requests/minute. A machine-readable spec lives at /api/v1/openapi.json.

Sites

GET    /api/v1/sites                      your sites: state, url, health, deploy_state
POST   /api/v1/sites/{slug}/deploy        deploy from git ({"ref": "main"} optional)
GET    /api/v1/sites/{slug}/deploys       deploy history
GET    /api/v1/sites/{slug}/logs?log=app  last 200 lines (app, php, nginx-error, access)
GET    /api/v1/sites/{slug}/health        live health + open incidents
GET    /api/v1/sites/{slug}/traffic       disk + monthly traffic

Backups & staging

GET    /api/v1/sites/{slug}/backups       list restore points
POST   /api/v1/sites/{slug}/backups       back up now
POST   /api/v1/sites/{slug}/restore       {"backup_id": 12} — replaces live content
GET    /api/v1/sites/{slug}/staging       staging status + url
POST   /api/v1/sites/{slug}/staging       {"action": "create" | "refresh" | "off"}

Staging is owner-only and billed on plans that don't include it — the same rules as the portal.

Scheduled tasks

GET    /api/v1/sites/{slug}/cron          list jobs with their index
POST   /api/v1/sites/{slug}/cron          {"schedule": "*/5 * * * *", "command": "php artisan queue:work"}
DELETE /api/v1/sites/{slug}/cron/{index}  remove a job

Domains & DNS

GET    /api/v1/domains                    registered domains: expiry, auto-renew, DNSSEC
GET    /api/v1/domains/{domain}/registrar registry vs live nameservers, DNSSEC state, lock, privacy
PUT    /api/v1/domains/{domain}/nameservers  {"mode": "custom", "nameservers": ["ns1.example.com", "ns2.example.com"]}
GET    /api/v1/dns                        your DNS zones
POST   /api/v1/dns/zones/probe            {"domain": "example.com"} dry run: live zone as an import plan + registrar steps (dns scope)
GET    /api/v1/dns/{zone}/records         zone records
POST   /api/v1/dns/{zone}/records         {"name": "www", "type": "CNAME", "ttl": 3600, "content": "example.com."}
PUT    /api/v1/dns/{zone}/records/{id}    update a record
DELETE /api/v1/dns/{zone}/records/{id}    delete a record

DNS changes are live on our nameservers in under a second.

Changing nameservers needs the dns scope, and is refused with a 409 and a continue_url while DNSSEC is active on the domain: the registry vouches for our signing key, so moving the delegation before that is withdrawn (and its cached lifetime waited out) would make validating resolvers refuse the domain outright. The portal walks you through it. Your zone and records here are kept either way, so switching back is immediate.

Pagination

Every list endpoint (/sites, /deploys, /backups, /cron, /domains, /dns, /dns/{zone}/records, /apps) takes ?limit= (default 20, max 100) and ?cursor=, and answers with a meta object next to its array:

GET /api/v1/sites?limit=50

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

GET /api/v1/sites?limit=50&cursor=eyJ2IjoxLCJvIjo1MH0

Pass next_cursor back as cursor until has_more is false. Cursors are opaque and expire with nothing — a cursor from an hour ago still works. Deploy and backup histories are keyed on the row, so new entries arriving at the head don't shift the page you're on. A bad limit or cursor is a 422.

Errors

Every 4xx and 5xx has the same body, whatever produced it:

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

type is stable and safe to branch on: unauthenticated (401), forbidden (403 — missing scope, site not pinned to the token, or owner-only), not_found (404), conflict (409 — site not active, or a step that needs a human in the portal; those add continue_url), validation_error (422, with details per field), rate_limited (429), idempotency_conflict (409), upstream_error (502 — a registry didn't answer; nothing was changed) and server_error (500). message is for people. request_id is the same value as the X-Request-Id header that every response carries — quote it when you write to support and we can find the exact request in our logs.

Idempotency

Networks time out. To retry an action without queuing it twice, send an Idempotency-Key header (any string of 1–200 printable characters; a UUID is ideal) on POST /deploy, /backups, /restore, /staging, /cron, DELETE /cron/{index}, the DNS record writes and PUT /nameservers:

curl -X POST -H "Authorization: Bearer mpc_..." \
     -H "Idempotency-Key: 6c1f0d2e-9b7a-4f2e-8e1a-3d5c7b9a1f00" \
     https://manage.veldhost.eu/api/v1/sites/my-site/deploy

For 24 hours, the same key with the same request returns the stored first response — same status, same body — with Idempotent-Replayed: true, and nothing runs again. The same key with a different request (another path or body) is a 409 idempotency_conflict: a key names one action. Keys are scoped to the token that sent them, and only successful responses are remembered, so a request that failed can be retried with the same key once you've fixed the cause. Without the header the endpoints behave as before.

Rate limits

120 requests a minute per token owner — every token of the same user shares one bucket, which is what makes a runaway script easy to spot and stop. Each response carries X-RateLimit-Limit and X-RateLimit-Remaining; going over returns 429 rate_limited with Retry-After in seconds. Poll /health and /deploys at a sane interval rather than in a tight loop, and page with limit=100 when you need everything.

Versioning

This is v1, at /api/v1, currently spec version 1.3. Additive changes — new endpoints, optional parameters, extra response fields such as meta — may ship at any time; ignore fields you don't know. Breaking changes only ever ship under a new prefix (/api/v2), and v1 keeps working for at least 12 months after that with a Deprecation header on its responses and a sunset date in the changelog.

What the API deliberately won't do

Anything that spends money or needs a legal consent stays with you: POST /api/v1/sites validates and prices a new site, then returns a continue_url into the portal where you review the cost and confirm — the API never charges, and destructive account actions (deleting a site, disconnecting a domain) aren't exposed at all.