Engine HTTP API overview and conventions
The engine is the in-account Cloudflare Worker that writes your backups. This page describes the HTTP surface it exposes, the conventions every endpoint follows, and the boundary of what is reachable, before you read the per-endpoint reference. It is for a developer integrating with the engine, an operator reading the audit feed into a SIEM, or anyone who needs to know what the Worker answers and what it does not.
The single fact to hold first is that the engine runs entirely inside your own Cloudflare account. There is no public engine URL by default, and there is no vendor-facing API. The console reaches the engine through a service binding in the same account, and the console is the only intended client of the admin surface. Everything below is reached from your own infrastructure, never from the vendor side.
The path families
The Worker’s fetch entry routes by path prefix into seven top-level families, and nothing else (engine/src/index.ts). A request that matches none of them gets a plain 404.
| Path family | Purpose | CORS | Hardening |
|---|---|---|---|
/admin/* | The admin API the console calls: downpipe CRUD, runs, restore, identity, audit, posture, status | Allowlisted to CONSOLE_ORIGIN only, except GET /admin/health (see below) | Full set, including cache-control: no-store |
/support/diagnostics | Server-to-server pull of the signed and (where configured) sealed support bundle, under a minted pull credential | None | Full set, including cache-control: no-store |
/support/audit-feed | Server-to-server pull of the hash-chained audit feed for your own SIEM collector, under a separate scoped credential | None | Full set |
GET /metrics | Server-to-server Prometheus scrape, under its own minted, bearer-credentialed pull credential | None | Full set |
GET / | A static, unauthenticated banner string | None | Base set (content-type, framing, referrer, resource policy) |
GET /ready | An unauthenticated readiness probe returning {status, service, version} JSON; it never reads account state | None | Base set |
/scim/v2/* | The SCIM 2.0 deprovision-only facade an IdP connector calls to offboard a leaver, under its own SCIM_BEARER_TOKEN; it deprovisions only and never provisions or reads users | None | Base set |
The /scim/v2 facade is covered in full by SCIM and deprovisioning. GET /metrics is covered by the metrics endpoint.
GET /admin/health is the one deliberate exemption from the CONSOLE_ORIGIN allowlist inside /admin/*: it answers before the authorisation gate, reads no request state and returns only the constant {ok:true, service:"downpipe-engine"}, with a wildcard access-control-allow-origin and no credentials header, so a browser blocked by a misconfigured CONSOLE_ORIGIN can still tell a genuinely dead engine from a CORS misconfiguration.
Everything that is not one of those seven is a catch-all 404 returning the body not found. The /admin API has no DELETE, PUT or PATCH, by design: every admin mutation is a POST. The separate /scim/v2 deprovision facade is the only surface that handles DELETE or PATCH, and only to offboard a leaver (see conventions below). The admin family forwards almost every route inward to a per-account scheduler Durable Object. Those internal Durable Object paths are not part of the public surface, and are not documented as endpoints you call.
The /support/* pair and GET /metrics are the credentialed pull surfaces outside the console. Each is a read-only pull: vendor support fetches the diagnostic bundle during a live ticket, your own collector polls the audit feed, and your own monitoring scrapes metrics. All three present a minted bearer credential scoped to exactly that one read, with no admin route reachable from any of them. See the support and audit-feed endpoints, the metrics endpoint and pull credentials.
How the console reaches the engine
There is no public engine hostname in the default deployment. The console is a separate Worker in the same account, and it talks to the engine over an in-account binding, so the admin API never needs a public route to be useful. When this documentation shows a request against console.example.com, that is the console’s own custom domain, which then proxies inward to the engine. Substitute your own console domain.
The browser calls /admin on the console’s own origin, and the console forwards the call over the binding. The engine still emits CORS headers, but it allowlists exactly one origin. corsHeaders returns the access-control headers only when the request’s Origin header is an exact match for the configured CONSOLE_ORIGIN; there is no wildcard outside GET /admin/health, and any other origin receives no CORS grant at all (engine/src/index.ts). The allowed methods are GET, POST, OPTIONS, which is the whole admin verb set. A browser preflight OPTIONS is answered with a 204 carrying the CORS and hardening headers.
The admin family also carries a standard set of hardening headers on every response, including the error responses and the preflight. The headers are applied uniformly by copying each response into a fresh one, so a route that forwards the Durable Object’s reply still ends up hardened (SECURITY_HEADERS, engine/src/index.ts).
| Header | Value | Why |
|---|---|---|
strict-transport-security | max-age=63072000; includeSubDomains | Transport safety on the admin host |
x-content-type-options | nosniff | No content-type sniffing |
referrer-policy | no-referrer | No referrer leakage from an authenticated page |
x-frame-options | DENY | No framing of an authenticated surface |
cross-origin-resource-policy | same-origin | No cross origin embed of a response through a <script>, <img> or <link> |
cache-control | no-store | Every admin response is authenticated and may carry account state, so nothing is cached |
pragma | no-cache | Pairs with no-store for older intermediaries |
Two other surfaces are credentialed and do carry account state, so they get the same full set rather than a weaker one: the /support/* pulls and GET /metrics are wrapped in withSecurity, which applies SECURITY_HEADERS verbatim, so a support bundle or a metrics scrape carries cache-control: no-store and pragma: no-cache and no intermediary may retain it (handleNonAdminRoute, engine/src/index.ts).
Only the genuinely unauthenticated responses carry the base subset: x-content-type-options, referrer-policy, x-frame-options and cross-origin-resource-policy. That is the root banner, the catch-all 404, GET /ready and the /scim/v2 facade. They hold no account state, so they do not need no-store or the HSTS header, but the sniffing, framing and referrer guards still apply to every byte the Worker emits (BASE_SECURITY_HEADERS, engine/src/index.ts).
Conventions every route follows
A handful of conventions hold across the admin surface, so you can read any single endpoint against the same expectations.
Every mutation is a POST. On the /admin API there is no DELETE, PUT or PATCH. A read is a GET; a change of any kind, including a deletion such as removing a downpipe or a role mapping, is a POST to a named route (for example POST /admin/downpipes to add or update, and a .../delete route to remove). Do not write client code that issues DELETE or PUT against the admin API; no admin route answers those verbs.
Every mutation is authorised, but not always by capability, and a read is not always gated at all. Of the 116 POST cases the admin routers dispatch, 95 are authorised by gate(caller, capability) and 21 are not, so do not write a client that infers a capability check from the verb. The two /update/ramp routes call gate inside their own handler. Four of the 21 are break-glass routes checked against the bare ADMIN_TOKEN, five are test-only routes that return 404 on a customer deployment, and three (POST /admin/canary/config, POST /admin/support/credentials and its delete) use a bare owner-role test that is deliberately not mapped to a capability. The remaining nine are the account and session routes, and they split two ways: the four that can affect another person (/signin-factors/revoke, /sessions/terminate-user, /sessions/terminate-all, and revoking someone else’s passkey) are authorised in the Durable Object on roles.write or an owner guard rather than at the router, while /stepup/begin, /stepup/finish, /sessions/terminate, /sessions/terminate-others and /email/test act only on the caller’s own session or address and are open to any authenticated caller by design.
A GET may or may not: of the 57 GET cases the admin routers dispatch, 39 call gate and 18 call none at all, and a read in the second group needs an authenticated session and nothing further (GET /admin/status, GET /admin/whoami, GET /admin/licence and GET /admin/drill-evidence are in that set). Of the 39 that do gate, 34 name one of the seven capabilities every built-in role holds, so no built-in caller can tell them from an open read, and 5 name a narrower capability and are genuinely restricted. Do not infer from a 403 on a write that the matching read is closed. Do not build a client that treats a read as authorisation evidence. The authentication precedence, the full capability matrix and the list of narrower reads are documented in authentication and authorisation.
Bodies and replies are JSON. A mutating route reads a JSON request body and returns a JSON reply. A malformed body is caught at the route boundary and answered as a generic error rather than crashing the Worker.
A read needs only an authenticated session, so a GET is the simplest call to start with. As above, the request goes to your own console’s custom domain, which proxies inward to the engine; substitute your console domain, and your bootstrap ADMIN_TOKEN for $DOWNPIPES_TOKEN. The bearer header takes only that token; a passkey or IdP session travels as the session cookie instead.
curl -sS https://console.example.com/admin/downpipes \
-H "authorization: Bearer $DOWNPIPES_TOKEN"const res = await fetch("https://console.example.com/admin/downpipes", {
headers: { authorization: `Bearer ${process.env.DOWNPIPES_TOKEN}` },
});
const downpipes = await res.json(); // the configured downpipes with their statereq, _ := http.NewRequest("GET", "https://console.example.com/admin/downpipes", nil)
req.Header.Set("authorization", "Bearer "+os.Getenv("DOWNPIPES_TOKEN"))
res, err := http.DefaultClient.Do(req)
// res.Body lists the configured downpipes with their state.This page shows the example once as the template.
The error channels
The engine answers a failed request on one of a small, fixed set of channels. The status code plus the body shape tells the console which kind of failure it was. Distinguishing them on the status code is deliberate: the console shows a sign-in prompt for one and an “your role cannot do this” message for another.
| Status | Body | When | What to do |
|---|---|---|---|
| 401 | Plain text unauthorised | No usable credential was presented, or a presented credential did not verify | Authenticate, or re-authenticate. See the sign-in flows |
| 403 | JSON { "error": "forbidden", "required": "<capability>", "have": "<role>" } | The caller authenticated but their role lacks the capability the route requires | Use a role that holds the required capability, or have an Owner grant it |
| 403 | JSON { "error": "csrf origin check failed" } | A cookie-session caller sent a state-changing request whose Origin did not match CONSOLE_ORIGIN | Send the request from the console origin (a real browser fetch always does) |
| 429 | JSON { "error": "rate limited" } with a Retry-After header in whole seconds | The caller exceeded the per-identity anti-automation window on a mutating route | Wait the Retry-After interval, then retry |
| 4xx | JSON, route-specific, for example { "error": "..." } | A bad request: a missing field, an invalid value, or a not-found target | Fix the request per the route’s reference |
| 500 | JSON { "error": "internal error" } | An unexpected fault | Retry; the engine logs a coarse error id internally with no detail in the reply |
Only the 429 and 500 bodies are RFC 9457 application/problem+json: each carries type, title and status alongside the error field shown above (for example the 500 is { "type": "urn:downpipe:error:internal-error", "title": "Internal error", "status": 500, "error": "internal error", "requestId": "<cid>" }, with the correlation id from the response header always present as an extra member). The 429 also carries advisory RateLimit and RateLimit-Policy headers beside Retry-After. The two 403 bodies and route-specific 4xx bodies are plain application/json, carrying only the table’s fields and no type, title or status. The 401 and the not found 404 for an unknown /admin path are plain text. The SAML metadata route also answers a failed read with a plain-text 503.
Every other channel is JSON. The 403 forbidden body names the exact required capability and the caller’s have role, so a client can render a precise message without guessing (Forbidden, engine/src/admin/identity.ts). The 429 carries a Retry-After value rounded up to whole seconds, with a floor of one second, so a caller never retries a hair too early into a still-saturated window (rateLimited, engine/src/admin/router-core.ts).
A worked 403 body:
{
"error": "forbidden",
"required": "restore.apply",
"have": "operator"
}
| Field | Meaning |
|---|---|
error | Always the literal forbidden for this channel, so the console discriminates it from a 401 sign-in failure |
required | The exact capability the route gates on (a capability, not a role rank). The three owner-only routes put owner here instead |
have | The caller’s resolved role, so the console can say which role would be needed |
Where the OpenAPI spec comes from
No OpenAPI or Swagger document ships from the engine itself. The engine’s route surface is the switch over METHOD path cases, split across the admin router’s dispatch hub and the twenty route-group spokes it delegates to in order (for example router-identity.ts and router-ops.ts), plus the dynamic-path handlers matched ahead of the switch (engine/src/admin/router.ts and the sibling router-*.ts spokes it imports). The admin endpoint catalogue lists them.
An OpenAPI spec of that route surface is served at /api/openapi/engine.json, and the API explorer renders it. The spec carries paths, methods, summaries, operation ids and tags only, with no request or response schemas. It is not a document the engine emits and not a live scan of a running router.
The OpenAPI spec is a snapshot of one engine version. It does not connect to a live engine, and it does not detect differences between this documentation and the engine you are running. Where the two differ, the engine is the authority.
Where this fits
The rest of the API reference builds on the surface and conventions above.
Authentication and authorisation
The three authentication methods, their strict precedence, and the full role-by-capability matrix that gates every route.
Admin endpoint catalogue
Every /admin route by family, with method, path, required capability, request fields and notable behaviour.
API explorer
The interactive view of the OpenAPI spec, with the caveat above in mind.
Support and audit-feed endpoints
The two server-to-server /support/* pull paths and the credentials that authorise them.
Sign-in flows
The passkey, native OIDC and native SAML front doors that mint a session, plus bootstrap and recovery.
Error and status codes
The cross-surface reference for every status code and error body the platform returns.
Last updated .