API

Version v0, September 2026

This is a beta and it is unstable. Shapes may change, endpoints may be withdrawn, and there is no compatibility promise, no deprecation window, no uptime commitment and no support commitment. The first stable version will be v1, and it does not exist yet. Do not build anything you cannot afford to fix.

Describe a business in one sentence and get back a composed website you can publish. The same orchestration that runs behind the portal, reached over HTTP.

Getting a key

Sign in to the portal and say what you want. Something like create an API key is enough. Dark Orb will ask which permissions the key should carry and then show you the key once, on screen, at the moment it is made. It is never shown again and it cannot be recovered, so copy it somewhere safe before you move on. If you lose it, ask for a new one and revoke the old.

Keys belong to your account, not to you personally. If you lose access to the account, the key stops working with it.

Making a request

The base URL is https://portal.darkorb.ai. Send the key as a bearer token:

Authorization: Bearer $DARK_ORB_KEY

Every response is JSON and carries Cache-Control: no-store and X-Dark-Orb-Api-Version: v0. Errors always take the same shape:

{ "error": { "code": "scope_insufficient", "message": "This key cannot do that." } }

A worked example

Compose a site, then publish it. Two requests, start to finish:

curl -X POST https://portal.darkorb.ai/api/v0/sites \
  -H "Authorization: Bearer $DARK_ORB_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"intent":"A joinery in Leeds making bespoke kitchens and fitted wardrobes."}'

That answers 201 with the site, including its siteId and the url the site will live at. Then publish it:

curl -X POST https://portal.darkorb.ai/api/v0/sites/st_.../publish \
  -H "Authorization: Bearer $DARK_ORB_KEY"

Open the url from either response and the site is there, live, for anyone.

Endpoints

EndpointScopeLimit
GET /api/v0/whoamiany120 per hour, per key
GET /api/v0/sitessites:read120 per hour, per key
GET /api/v0/sites/{siteId}sites:read120 per hour, per key
POST /api/v0/sitessites:compose5 per hour, per account
POST /api/v0/sites/{siteId}/publishsites:publish30 per hour, per key

GET /api/v0/whoami

What this key is and what it can do right now. Useful for checking a key works before you rely on it.

{
  "accountId": "acc_...",
  "keyId": "ak_...",
  "scopes": ["sites:read", "sites:compose", "sites:publish"],
  "expiresAt": null
}

The scopes reported are the ones the key can exercise at this moment, which can be narrower than the ones it was issued with if the person who issued it has since lost the access it was drawn from.

GET /api/v0/sites

The account's sites, one page at a time:

{
  "sites": [ ... ],
  "nextCursor": "WyJ2MCIsIjIwMjYtMDkt..."
}

Ask for a page size with ?limit= (25 by default, 100 at most), and ask for the next page with ?cursor=, passing back the nextCursor you were given. Keep going until nextCursor is null, which is the end. A limit or a cursor we cannot use is refused with 422 rather than quietly changed to something else.

A page can come back shorter than you asked for and still not be the last one, so read nextCursor, not the number of sites. The cursor marks your place rather than counting from the start, so a site made while you are part-way through cannot push one you have not seen out of view.

Each entry is the same shape:

{
  "siteId": "st_...",
  "slug": "brae-joinery",
  "createdAt": "2026-09-01T09:12:44.019Z",
  "updatedAt": "2026-09-01T09:12:44.019Z",
  "currentVersionId": "v_...",
  "publishedVersionId": null,
  "published": false,
  "url": "https://portal.darkorb.ai/site/brae-joinery"
}

published is true only when the version that is live is also the newest one. A site composed again after publishing reads as not published until you publish the new version.

GET /api/v0/sites/{siteId}

One site, in the shape above. A site id belonging to another account answers 404, exactly as an id that does not exist does, so the endpoint cannot be used to find out what else is out there.

POST /api/v0/sites

Compose a site. The body carries one field, and an unknown field is refused rather than ignored:

{ "intent": "A joinery in Leeds making bespoke kitchens and fitted wardrobes." }

An Idempotency-Key header is required, between 16 and 128 characters. A composed site costs real money to make, so the key is what stops a retry, a timeout or a dropped connection paying for a second one. Send a fresh value per site, a UUID is ideal, and reuse the same value if you retry.

Answers 201 with the site. Repeating a request with a key that already completed replays the original answer verbatim, with Idempotency-Replayed: true, and composes nothing. Keys are held for 24 hours, and are scoped to your account and to this endpoint.

The site is composed but not live. Publish it when you are happy with it.

POST /api/v0/sites/{siteId}/publish

Make the newest version of a site public. No body. Answers 200 with the site, now carrying "published": true and the url anyone can open.

Scopes

A key carries the permissions you asked for when it was made, and nothing else. Ask for the narrowest set that does the job.

ScopeWhat it allows
sites:readListing sites and reading one
sites:composeComposing a new site
sites:publishPublishing a composed site

A key can never do more than the person who issued it can do. If they lose access to the account, the key loses it at the same moment, without anyone having to remember to revoke it.

Errors

StatusCodeWhat it means
400malformed_requestThe body is not valid JSON, or the Idempotency-Key header is missing or the wrong length
401unauthenticatedThe key is missing, malformed, revoked, expired or not a key at all. Every one of those answers identically, on purpose
403scope_insufficientThe key is valid but does not carry the scope this endpoint needs
404not_foundNo site with that id on this account
409idempotency_conflictThat key is still running, or it was used for a different request. Wait, or send a new key
422invalid_requestThe body is valid JSON but wrong: a missing intent, an empty one, one that is too long, or an unknown field
429rate_limitedOver the limit for this endpoint. Retry-After carries the seconds until it resets
429budget_exhaustedThe account has used its AI allowance for the day. It resets tomorrow, and nothing was spent on this request
503unavailableSomething Dark Orb depends on could not answer. Nothing was half-done: a compose that could not be saved is reported as a failure and its idempotency key is freed, so retrying is safe

Rate limits

Limits reset on the hour rather than easing back gradually, so a 429 carries a Retry-After that tells you when the next hour begins. Waiting less than that will only be refused again.

Composing counts against your account, because composing spends Dark Orb's own AI credits rather than yours. Reading and publishing count against the individual key. Above both sits a daily AI allowance per account, shared with the portal: one account, one meter, whichever way you reach it.

What is here, and what is not

v0 covers composing a site, publishing it, and reading what you have. Correcting a site, uploading images, custom domains and the rest of the toolkit are reachable in the portal today but not yet over the API. More to come.

Changes

DateChange
September 2026v0 opens: whoami, sites, compose and publish

Questions

Write to hello@danieltodd.uk. Use of the API is covered by the Terms of Service and the Acceptable Use Policy.

← Back