API
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.
If you run sites for clients, each client is a business in your account. Ask for a key for one of them by name, something like an API key for Hats of Leeds that can build and publish, and that key reaches only that business and its sites. Anything else in your account answers 404 to it, exactly as another account's would. If the business leaves your 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
| Endpoint | Scope | Limit |
|---|---|---|
GET /api/v0/whoami | any | 120 per hour, per key |
GET /api/v0/sites | sites:read | 120 per hour, per key |
GET /api/v0/sites/{siteId} | sites:read | 120 per hour, per key |
POST /api/v0/sites | sites:compose | 5 per hour, per account |
POST /api/v0/sites/{siteId}/publish | sites:publish | 30 per hour, per key |
GET /api/v0/businesses | businesses:read | 120 per hour, per key |
GET /api/v0/businesses/{businessId}/usage | businesses:read | 120 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", "businesses:read"],
"expiresAt": null,
"businessId": null
}
businessId names the one business a key made for a client reaches, and is null for a key that reaches your whole account.
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.
Add ?businessId= to see one of your businesses' sites only. A key made for one business only ever sees that business's sites, whether or not it asks.
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 an intent, and an unknown field is refused rather than ignored:
{ "intent": "A joinery in Leeds making bespoke kitchens and fitted wardrobes." }
To build it for one of your businesses, add its businessId. Leave it out and the site is built for your main business, or for the business a key made for a client belongs to. A businessId the key cannot reach answers 404 and builds nothing.
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, to the business the site is built for, and to this endpoint. The same key used for two different businesses is two separate requests.
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.
GET /api/v0/businesses
The businesses this key can reach, paged exactly like the site list. A key for your whole account lists every business in it; a key made for one client lists that one.
{
"businesses": [
{ "businessId": "bz_...", "name": "Hats of Leeds", "createdAt": "2026-09-01T09:12:44.019Z" }
],
"nextCursor": null
}
GET /api/v0/businesses/{businessId}/usage
What one business has spent on AI, today and on each of the six days before, so an agency can see what each client costs. Amounts are millionths of a US dollar at the AI providers' list prices, the same measure the daily allowance counts in. dailyCapMicroUsd is the business's own daily limit, or null when only the account's allowance applies. A business this key cannot reach answers 404.
{
"businessId": "bz_...",
"currency": "USD",
"unit": "micro",
"days": [ { "date": "2026-09-26", "microUsd": 184000 } ],
"todayMicroUsd": 184000,
"dailyCapMicroUsd": 2500000,
"accountDailyCapMicroUsd": 5000000
}
To give a business its own daily limit, say so in Dark Orb, for example "cap Hats of Leeds at £2 a day". It can never be more than the account's allowance, and when it is reached only that business stops for the day. The others carry on.
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.
| Scope | What it allows |
|---|---|
sites:read | Listing sites and reading one |
sites:compose | Composing a new site |
sites:publish | Publishing a composed site |
businesses:read | Listing the businesses the key can reach, and what each has spent |
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
| Status | Code | What it means |
|---|---|---|
| 400 | malformed_request | The body is not valid JSON, or the Idempotency-Key header is missing or the wrong length |
| 401 | unauthenticated | The key is missing, malformed, revoked, expired or not a key at all. Every one of those answers identically, on purpose |
| 403 | scope_insufficient | The key is valid but does not carry the scope this endpoint needs |
| 404 | not_found | No site or business with that id that this key can reach |
| 409 | idempotency_conflict | That key is still running, or it was used for a different request. Wait, or send a new key |
| 409 | client_contact_required | A client business cannot go live until you have told Dark Orb the email address of its owner. Say it in the portal, then publish again |
| 422 | invalid_request | The body is valid JSON but wrong: a missing intent, an empty one, one that is too long, or an unknown field |
| 429 | rate_limited | Over the limit for this endpoint. Retry-After carries the seconds until it resets |
| 429 | budget_exhausted | The account, or the business the request is for, has used its AI allowance for the day. It resets tomorrow, and nothing was spent on this request |
| 503 | unavailable | Something 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 building a site, publishing it, and reading what you have. Correcting a site, uploading images, custom domains and the other capabilities are reachable in the portal today but not yet over the API. More to come.
Changes
| Date | Change |
|---|---|
| September 2026 | v0 opens: whoami, sites, compose and publish |
| September 2026 | Businesses: GET /api/v0/businesses, businessId on compose and the site list, and keys made for one business |
| September 2026 | What each business spends: GET /api/v0/businesses/{businessId}/usage, and a daily limit per business under the account's allowance |
Questions
Write to hello@darkorb.ai. Use of the API is covered by the Terms of Service and the Acceptable Use Policy.
← Back