REST API & MCP reference
Authenticate once with an org-scoped key, then drive DarkBird from a script, an internal dashboard, or an AI assistant.
DarkBird exposes the same service layer the web app uses through two surfaces: a REST API (/api/v1/*) and an MCP server (/api/mcp) that an MCP-compatible client such as Claude Code can drive directly. Both authenticate identically, with an org-scoped API key rather than a user login.
Generate an API key
Go to Settings → API Keys (Owners and Admins). Name the key after whatever will use it, and click Generate.
The plaintext key is shown exactly once. DarkBird stores only a SHA-256 hash of it plus a short display prefix, so it cannot be recovered or re-shown — copy it immediately. If you lose it, revoke it and generate another.
Keys look like dk_live_ followed by 32 random URL-safe characters. Settings lists only the first 14 (dk_live_ plus six) so you can tell keys apart. Revoking is immediate and permanent — anything using that key starts getting 401s on its next request.
A key carries the same permissions as an Owner/Admin within your organization, and is scoped to that one organization. Treat it like a password: never commit it, never put it in client-side code. Revoke immediately if it is ever exposed.
Authentication
Every request to both surfaces sends the key as a bearer token. The base URL is https://darkbird.app.
curl https://darkbird.app/api/v1/projects \
-H "Authorization: Bearer dk_live_your_key_here"| Status | When | Body |
|---|---|---|
401 | No Authorization header, or not a Bearer scheme | { "error": "Missing or malformed Authorization header. Expected: Bearer <api-key>." } |
401 | Key is unknown or has been revoked | { "error": "Invalid or revoked API key." } |
400 | Request body failed validation (POST only) | { "error": "Invalid request body.", "details": { … } } — details is a flattened Zod error, so it names the offending fields |
500 | Server error | { "error": "<message>" } |
REST endpoints
| Method + path | Body | Returns |
|---|---|---|
GET /api/v1/projects | — | 200 { projects: Project[] } |
POST /api/v1/projects | { name, client, phaseGroupId? } | 201 { project: Project } |
GET /api/v1/projects/{projectId} | — | 200 { addedComponentCards: AddedComponentCard[] } — the cards on that project's estimate |
GET /api/v1/install-cards | — | 200 { installCards: InstallCard[] } |
POST /api/v1/install-cards | A full Install Card (see below) | 201 { installCard: InstallCard } |
That is the whole surface today — five endpoints. Notably there is no update or delete on either resource, and no endpoint for interviews, SOW generation, or calibration. Those exist only in the web app.
Create a project
curl -X POST https://darkbird.app/api/v1/projects \
-H "Authorization: Bearer dk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse Camera Refresh",
"client": "Acme Logistics"
}'name and client are both required and must be non-empty. phaseGroupId is optional — pass a Phase Set id to scope the project's work plan to that set, or omit it (or send null) to show all phases.
Create an Install Card
The POST body is validated against the same schema the card editor uses, so anything the editor accepts the API accepts. Required: name, categoryId (may be null), options, and tickets.
{
"name": "Access Point Installation",
"description": "Mount and configure a wireless access point.",
"categoryId": null,
"behavior": "Standard",
"options": [
{
"id": "opt-base",
"name": "Base",
"unitOfMeasureId": null,
"unitOfMeasureName": "Each"
}
],
"tickets": [
{
"id": "tkt-1",
"name": "Mount and terminate AP",
"phaseId": null,
"optionId": "opt-base",
"resourceId": null,
"quantityRanges": [
{ "id": "qr-1", "startQty": 1, "endQty": 10, "hoursPerUnit": 2 },
{ "id": "qr-2", "startQty": 11, "endQty": null, "hoursPerUnit": 1.5 }
],
"checklist": [
{ "id": "chk-1", "name": "Confirm PoE budget", "completed": false }
]
}
]
}You generate the id values yourself — they only need to be unique within the card, and each ticket's optionId must match one of the options. Name the always-included option exactly "Base" or "Standard", or it renders as an optional add-on instead (see Card behavior modes).
quantityRanges must be contiguous. A range ending at 10 followed by one starting at 12 yields zero hours for a quantity of 11 — silently. Use "endQty": null for the final open-ended bracket.
MCP server
The MCP endpoint is https://darkbird.app/api/mcp, using the Streamable HTTP transport with the same bearer key. It is stateless — no session is retained between requests — so clients should not expect server-side conversation state.
Connect Claude Code
claude mcp add --transport http darkbird https://darkbird.app/api/mcp \
--header "Authorization: Bearer dk_live_your_key_here"Share the connection with your team
To check the connection into a repo so your whole team gets it, add DarkBird to .mcp.json and reference the key as an environment variable rather than pasting it in — that way the config is shareable and the key never lands in git:
{
"mcpServers": {
"darkbird": {
"type": "http",
"url": "https://darkbird.app/api/mcp",
"headers": {
"Authorization": "Bearer ${DARKBIRD_API_KEY}"
}
}
}
}In JSON config, type accepts either http or streamable-http — they mean the same transport, so a config copied from elsewhere works unchanged.
Other MCP clients
Any client that speaks the Streamable HTTP transport and lets you set a custom header can connect: point it at https://darkbird.app/api/mcp and send Authorization: Bearer <your-key>. DarkBird does not use OAuth for this surface — authentication is the API key header, nothing else.
Available tools
| Tool | Arguments | What it does |
|---|---|---|
list_projects | — none | Lists every project in the organization. |
create_project | name (required), client (required), phaseGroupId (optional, nullable) | Creates a project. Same validation as the REST endpoint. |
get_project_cards | projectId (required) | Returns the cards on a project's estimate — the added component cards, each with its frozen template snapshot. |
list_install_cards | — none | Lists every Install Card in the organization's library. |
create_install_card | A full Install Card — same fields as POST /api/v1/install-cards | Creates a labor template in the organization's library. |
Tool errors come back as MCP tool results with isError: true and the message as text — they are not transport-level failures. Auth failures are the exception: a bad key fails the HTTP request itself with a 401 before any tool runs.
Every tool is org-scoped by the key you connected with. There is no way to read or write another organization's data through this surface, regardless of what a client asks for.