API Reference
BlackLake runs in two modes. In local mode, started with npx @blacklake-systems/surface-cli, the API is available at http://localhost:3100. In cloud mode, the base URL is https://api.blacklake.systems.
The API is identical in both modes. All /v1/* endpoints require authentication via the x-api-key header. In local mode, authentication is present for compatibility but is not enforced — you can pass any non-empty string. Proxy endpoints (/proxy/*) do not require a BlackLake key.
Authentication
x-api-key: <your-api-key>
Error Format
All error responses use a consistent envelope:
{
"error": {
"code": "AGENT_NOT_FOUND",
"message": "Agent agent_01j not found"
}
}
System
GET /health
Health check endpoint. Does not require authentication.
Response — 200 OK
{ "status": "ok" }
curl
curl http://localhost:3100/health
GET /v1/mode
Returns the current operating mode.
Response — 200 OK
{ "mode": "local" }
The mode field is either "local" or "cloud". Use this to verify which environment your client is connected to.
curl
curl http://localhost:3100/v1/mode \
-H "x-api-key: local"
MCP Servers
GET /v1/mcp
List MCP servers currently configured in ~/.blacklake/mcp-config.json and their status.
Response — 200 OK
{
"servers": [
{
"name": "filesystem",
"status": "running",
"proxy_url": "http://localhost:3100/mcp/filesystem",
"tool_count": 5,
"last_seen": "2026-04-10T08:30:00.000Z",
"error": null
}
]
}
| Field | Description |
|---|---|
name | The server name from mcp-config.json. |
status | running, stopped, or error. |
proxy_url | The URL your MCP client should connect to. |
tool_count | Number of tools discovered from this server. |
last_seen | Timestamp of the most recent tool call through this proxy. null if no calls have been made yet. |
error | Error message if the server failed to start or crashed. null otherwise. |
curl
curl http://localhost:3100/v1/mcp \
-H "x-api-key: local"
POST /v1/mcp/:name/reconnect
Restart a stopped or errored MCP server without restarting the full CLI. Has no effect if the server is already running.
Path parameters
| Parameter | Description |
|---|---|
name | The server name as it appears in mcp-config.json. |
Response — 200 OK
{
"name": "filesystem",
"status": "running",
"proxy_url": "http://localhost:3100/mcp/filesystem",
"tool_count": 5,
"last_seen": null,
"error": null
}
Returns 404 if no server with that name is configured.
curl
curl -X POST http://localhost:3100/v1/mcp/filesystem/reconnect \
-H "x-api-key: local"
Usage
GET /v1/usage
Return a usage summary for LLM API calls proxied through BlackLake.
Query parameters
| Parameter | Type | Description |
|---|---|---|
period | string | Time range: today, 7d, 30d, or all. Default: 7d. |
provider | string | Filter by provider: anthropic, openai, or ollama. |
Response — 200 OK
{
"period": "7d",
"total_requests": 142,
"total_input_tokens": 284000,
"total_output_tokens": 71000,
"estimated_cost_usd": 1.24,
"by_model": [
{
"provider": "anthropic",
"model": "claude-sonnet-4-5",
"requests": 98,
"input_tokens": 196000,
"output_tokens": 49000,
"estimated_cost_usd": 0.86
},
{
"provider": "openai",
"model": "gpt-4o",
"requests": 44,
"input_tokens": 88000,
"output_tokens": 22000,
"estimated_cost_usd": 0.38
}
]
}
Cost estimates are based on published per-token rates and are approximations. Ollama calls are tracked but have no cost estimate.
curl
curl "http://localhost:3100/v1/usage?period=30d&provider=anthropic" \
-H "x-api-key: local"
API Proxy
The API proxy forwards requests to upstream LLM providers using your own API keys, and records usage metadata from the response. No request or response body content is stored.
These endpoints do not require a BlackLake x-api-key. Pass your provider API key in the Authorization header exactly as you would when calling the provider directly.
/proxy/anthropic/*
Forwards to https://api.anthropic.com. Replace your Anthropic SDK's baseURL with http://localhost:3100/proxy/anthropic.
curl http://localhost:3100/proxy/anthropic/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [{ "role": "user", "content": "Hello" }]
}'
/proxy/openai/*
Forwards to https://api.openai.com. Replace your OpenAI SDK's baseURL with http://localhost:3100/proxy/openai.
curl http://localhost:3100/proxy/openai/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{ "role": "user", "content": "Hello" }]
}'
/proxy/ollama/*
Forwards to http://localhost:11434. No API key required.
curl http://localhost:3100/proxy/ollama/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "llama3",
"messages": [{ "role": "user", "content": "Hello" }]
}'
Agents
POST /v1/agents
Create a new agent.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent name. Must be unique within the organisation. |
description | string | No | Optional description. |
environment | development | staging | production | Yes | Deployment environment. |
risk_classification | low | medium | high | critical | Yes | Risk level of the agent. |
approval_mode | auto_approve | require_approval | block | No | Default: auto_approve. |
Response — 201 Created
{
"id": "agent_01jv2k8tq3e4f5g6h7j8k9l0m",
"organisation_id": "org_01jv...",
"name": "customer-support-agent",
"description": "Handles tier-1 customer queries",
"environment": "production",
"risk_classification": "medium",
"status": "active",
"approval_mode": "auto_approve",
"created_at": "2026-04-06T09:00:00.000Z",
"updated_at": "2026-04-06T09:00:00.000Z"
}
curl
curl -X POST https://api.blacklake.systems/v1/agents \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "customer-support-agent",
"description": "Handles tier-1 customer queries",
"environment": "production",
"risk_classification": "medium"
}'
GET /v1/agents
List all agents in the organisation.
Query parameters
| Parameter | Type | Description |
|---|---|---|
environment | string | Filter by environment. |
status | string | Filter by status. |
Response — 200 OK — array of agent objects.
curl
curl https://api.blacklake.systems/v1/agents?environment=production \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/agents/:id
Retrieve a single agent by ID.
Response — 200 OK or 404 if not found.
curl
curl https://api.blacklake.systems/v1/agents/agent_01jv2k8tq3e4f5g6h7j8k9l0m \
-H "x-api-key: $BLACKLAKE_API_KEY"
PATCH /v1/agents/:id
Update agent fields. Only provided fields are modified.
Request body — all fields optional
| Field | Type | Description |
|---|---|---|
name | string | New name. |
description | string | New description. |
environment | string | New environment. |
risk_classification | string | New risk classification. |
status | string | New status. |
approval_mode | string | New approval mode. |
Response — 200 OK with the updated agent.
curl
curl -X PATCH https://api.blacklake.systems/v1/agents/agent_01jv2k8tq3e4f5g6h7j8k9l0m \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "risk_classification": "high" }'
POST /v1/agents/:id/suspend
Set the agent status to suspended. All subsequent governance requests for this agent will return deny until the agent is reactivated. No request body required.
Response — 200 OK with the updated agent.
curl
curl -X POST https://api.blacklake.systems/v1/agents/agent_01jv2k8tq3e4f5g6h7j8k9l0m/suspend \
-H "x-api-key: $BLACKLAKE_API_KEY"
POST /v1/agents/:id/activate
Set the agent status back to active. No request body required.
Response — 200 OK with the updated agent.
curl
curl -X POST https://api.blacklake.systems/v1/agents/agent_01jv2k8tq3e4f5g6h7j8k9l0m/activate \
-H "x-api-key: $BLACKLAKE_API_KEY"
Agent-Tool Bindings
POST /v1/agents/:id/tools
Bind a tool to an agent. Returns 409 if the binding already exists.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
tool_id | string | Yes | ID of the tool to bind. |
Response — 201 Created
{
"id": "bind_01jv...",
"agent_id": "agent_01jv...",
"tool_id": "tool_01jv...",
"created_at": "2026-04-06T09:01:00.000Z"
}
curl
curl -X POST https://api.blacklake.systems/v1/agents/agent_01jv.../tools \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tool_id": "tool_01jv..." }'
GET /v1/agents/:id/tools
List all tools bound to an agent. Returns tool records joined with binding metadata.
Response — 200 OK
[
{
"binding_id": "bind_01jv...",
"binding_created_at": "2026-04-06T09:01:00.000Z",
"tool": {
"id": "tool_01jv...",
"organisation_id": "org_01jv...",
"name": "send-email",
"description": "Sends an email to a customer",
"risk_classification": "medium",
"created_at": "2026-04-06T09:00:30.000Z"
}
}
]
curl
curl https://api.blacklake.systems/v1/agents/agent_01jv.../tools \
-H "x-api-key: $BLACKLAKE_API_KEY"
DELETE /v1/agents/:id/tools/:tool_id
Remove a binding between an agent and a tool. After this call, governance requests from this agent for this tool will be denied.
Response — 204 No Content
curl
curl -X DELETE https://api.blacklake.systems/v1/agents/agent_01jv.../tools/tool_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
Tools
POST /v1/tools
Register a new tool.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tool name. Must be unique within the organisation. |
description | string | No | Optional description. |
risk_classification | low | medium | high | critical | Yes | Risk level of the tool. |
Response — 201 Created
{
"id": "tool_01jv...",
"organisation_id": "org_01jv...",
"name": "send-email",
"description": "Sends an email to a customer",
"risk_classification": "medium",
"created_at": "2026-04-06T09:00:30.000Z"
}
curl
curl -X POST https://api.blacklake.systems/v1/tools \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "send-email",
"description": "Sends an email to a customer",
"risk_classification": "medium"
}'
GET /v1/tools
List all tools in the organisation.
Response — 200 OK — array of tool objects.
curl
curl https://api.blacklake.systems/v1/tools \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/tools/:id
Retrieve a single tool by ID.
Response — 200 OK or 404 if not found.
curl
curl https://api.blacklake.systems/v1/tools/tool_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
Policies
POST /v1/policies
Create a new policy.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Descriptive policy name. |
priority | integer | Yes | Evaluation order. Lower = evaluated first. |
agent_selector | object | No | Key-value pairs to match against the agent. Default: {} (matches all). |
tool_selector | object | No | Key-value pairs to match against the tool. Default: {} (matches all). |
outcome | allow | deny | approval_required | Yes | Decision to return when this policy matches. |
enabled | boolean | No | Default: true. |
Response — 201 Created
{
"id": "pol_01jv...",
"organisation_id": "org_01jv...",
"name": "block-high-risk-tools-in-prod",
"priority": 1,
"agent_selector": { "environment": "production" },
"tool_selector": { "risk_classification": "high" },
"outcome": "deny",
"enabled": true,
"created_at": "2026-04-06T09:02:00.000Z",
"updated_at": "2026-04-06T09:02:00.000Z"
}
curl
curl -X POST https://api.blacklake.systems/v1/policies \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "block-high-risk-tools-in-prod",
"priority": 1,
"agent_selector": { "environment": "production" },
"tool_selector": { "risk_classification": "high" },
"outcome": "deny"
}'
GET /v1/policies
List all policies, ordered by priority ascending.
Response — 200 OK — array of policy objects.
curl
curl https://api.blacklake.systems/v1/policies \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/policies/:id
Retrieve a single policy by ID.
Response — 200 OK or 404 if not found.
curl
curl https://api.blacklake.systems/v1/policies/pol_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
PATCH /v1/policies/:id
Update policy fields. Only provided fields are modified. The policy cache is invalidated immediately.
Request body — all fields optional. Same fields as POST /v1/policies.
Response — 200 OK with the updated policy.
curl
curl -X PATCH https://api.blacklake.systems/v1/policies/pol_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'
DELETE /v1/policies/:id
Permanently delete a policy. The policy cache is invalidated immediately.
Response — 204 No Content
curl
curl -X DELETE https://api.blacklake.systems/v1/policies/pol_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
Governance
POST /v1/govern
Evaluate whether an agent is permitted to invoke a tool. This is the core endpoint of Surface.
The request identifies the agent and tool by name. The engine resolves them to records, checks agent status, verifies the binding, evaluates policies in priority order, and records the decision.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent | string | Yes | Agent name. |
tool | string | Yes | Tool name. |
action | object | No | Payload describing the intended action. Stored on the evaluation record. |
context | object | No | Caller-supplied context (user ID, session, run ID, etc.). Stored on the evaluation record. |
Response — 200 OK
| Field | Type | Description |
|---|---|---|
decision | string | allow, deny, approval_required, or default_deny. |
evaluation_id | string | ID of the recorded evaluation. |
policy_id | string | null | ID of the matching policy, or null. |
reason | string | Human-readable explanation. |
evaluated_at | string | ISO 8601 UTC timestamp. |
approval_id | string | Present only when decision is approval_required. ID of the created approval record. |
{
"decision": "allow",
"evaluation_id": "eval_01jv...",
"policy_id": "pol_01jv...",
"reason": "Matched policy: allow-support-agent-email",
"evaluated_at": "2026-04-06T09:05:00.000Z"
}
curl
curl -X POST https://api.blacklake.systems/v1/govern \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "customer-support-agent",
"tool": "send-email",
"action": { "to": "user@example.com" },
"context": { "ticket_id": "T-1234" }
}'
Denial reasons
| Scenario | decision | reason |
|---|---|---|
| Agent is suspended | deny | Agent is suspended |
| Agent is disabled | deny | Agent is disabled |
| No binding for this agent+tool | deny | Tool is not bound to agent |
| Policy matched with deny outcome | deny | Matched policy: <policy name> |
| No policy matched | default_deny | No matching policy found |
Evaluations
GET /v1/evaluations
List evaluation records. Results are ordered by evaluated_at descending (most recent first).
Query parameters
| Parameter | Type | Description |
|---|---|---|
agent_id | string | Filter by agent ID. |
tool_id | string | Filter by tool ID. |
outcome | string | Filter by outcome: allow, deny, approval_required, or default_deny. |
limit | integer | Number of records to return. Default: 50. |
offset | integer | Pagination offset. Default: 0. |
Response — 200 OK
{
"data": [
{
"id": "eval_01jv...",
"organisation_id": "org_01jv...",
"agent_id": "agent_01jv...",
"tool_id": "tool_01jv...",
"policy_id": "pol_01jv...",
"action_payload": { "to": "user@example.com" },
"outcome": "allow",
"evaluated_at": "2026-04-06T09:05:00.000Z",
"request_context": { "ticket_id": "T-1234" }
}
],
"total": 142
}
curl
curl "https://api.blacklake.systems/v1/evaluations?outcome=deny&limit=20" \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/evaluations/:id
Retrieve a single evaluation by ID.
Response — 200 OK or 404 if not found.
curl
curl https://api.blacklake.systems/v1/evaluations/eval_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
Approvals
GET /v1/approvals
List approval records. Results are ordered by created_at descending.
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: pending, approved, rejected, or expired. |
agent_id | string | Filter by agent ID. |
tool_id | string | Filter by tool ID. |
limit | integer | Number of records to return. Default: 50. |
offset | integer | Pagination offset. Default: 0. |
Response — 200 OK
{
"data": [
{
"id": "approval_01jv...",
"organisation_id": "org_01jv...",
"evaluation_id": "eval_01jv...",
"agent_id": "agent_01jv...",
"tool_id": "tool_01jv...",
"policy_id": "pol_01jv...",
"action_payload": { "amount": 4200, "vendor": "Acme Corp" },
"request_context": null,
"status": "pending",
"decided_by": null,
"decision_reason": null,
"decided_at": null,
"created_at": "2026-04-08T12:34:56.789Z",
"expires_at": "2026-04-09T12:34:56.789Z"
}
],
"total": 3
}
curl
curl "https://api.blacklake.systems/v1/approvals?status=pending" \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/approvals/:id
Retrieve a single approval by ID.
Response — 200 OK or 404 APPROVAL_NOT_FOUND.
curl
curl https://api.blacklake.systems/v1/approvals/approval_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/approvals/:id/status
Fetch only the status fields for an approval. Cheaper than a full fetch when polling.
Response — 200 OK
{
"status": "pending",
"decided_at": null,
"expires_at": "2026-04-09T12:34:56.789Z"
}
curl
curl https://api.blacklake.systems/v1/approvals/approval_01jv.../status \
-H "x-api-key: $BLACKLAKE_API_KEY"
POST /v1/approvals/:id/approve
Approve a pending approval.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
decided_by | string | Yes | Identifier of the person or system making the decision. |
reason | string | No | Optional explanation. |
Response — 200 OK — the updated Approval object with status: "approved".
Errors
| Code | Status | Cause |
|---|---|---|
APPROVAL_NOT_FOUND | 404 | No approval with this ID in the organisation. |
APPROVAL_ALREADY_DECIDED | 400 | The approval was already approved or rejected. |
APPROVAL_EXPIRED | 400 | The approval has passed its 24-hour expiry. |
curl
curl -X POST https://api.blacklake.systems/v1/approvals/approval_01jv.../approve \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decided_by": "ops-team", "reason": "Verified vendor and amount" }'
POST /v1/approvals/:id/reject
Reject a pending approval.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
decided_by | string | Yes | Identifier of the person or system making the decision. |
reason | string | No | Optional explanation. |
Response — 200 OK — the updated Approval object with status: "rejected".
Errors
| Code | Status | Cause |
|---|---|---|
APPROVAL_NOT_FOUND | 404 | No approval with this ID in the organisation. |
APPROVAL_ALREADY_DECIDED | 400 | The approval was already approved or rejected. |
APPROVAL_EXPIRED | 400 | The approval has passed its 24-hour expiry. |
curl
curl -X POST https://api.blacklake.systems/v1/approvals/approval_01jv.../reject \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decided_by": "ops-team", "reason": "Amount exceeds policy limit" }'
Webhooks
Webhook payload format
All webhook deliveries share a common envelope:
{
"id": "whd_01jv...",
"event": "approval.created",
"created_at": "2026-04-08T12:34:56.789Z",
"data": {
"approval": { }
}
}
The data field always contains an approval key with the full Approval object, for all three event types.
| Event | data contents |
|---|---|
approval.created | { "approval": <Approval> } — approval is pending. |
approval.approved | { "approval": <Approval> } — approval is approved. |
approval.rejected | { "approval": <Approval> } — approval is rejected. |
Signature verification
Every delivery includes four headers:
X-BlackLake-Signature: sha256=<hex-digest>
X-BlackLake-Timestamp: <unix-ms>
X-BlackLake-Event: approval.created
X-BlackLake-Delivery-Id: whd_01jv...
Signed content: HMAC-SHA256(raw_secret, timestamp + "." + rawBody)
Verification example (Node.js):
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhook(
secret: string,
headers: Record<string, string>,
rawBody: string,
): boolean {
const timestamp = headers['x-blacklake-timestamp'];
const signature = headers['x-blacklake-signature'];
if (!timestamp || !signature) return false;
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const provided = Buffer.from(signature);
const calculated = Buffer.from(expected);
if (provided.length !== calculated.length) return false;
return timingSafeEqual(provided, calculated);
}
GET /v1/webhooks
List all webhooks registered for the organisation.
Response — 200 OK
{
"webhooks": [
{
"id": "wh_01jv...",
"organisation_id": "org_01jv...",
"url": "https://myapp.example/webhooks/blacklake",
"secret_suffix": "a3f9",
"events": ["approval.created", "approval.approved", "approval.rejected"],
"enabled": true,
"created_at": "2026-04-08T10:00:00.000Z"
}
]
}
curl
curl https://api.blacklake.systems/v1/webhooks \
-H "x-api-key: $BLACKLAKE_API_KEY"
POST /v1/webhooks
Register a new webhook. The secret field in the response is the raw signing secret — shown once and cannot be retrieved again.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to deliver events to. |
events | array | Yes | One or more of approval.created, approval.approved, approval.rejected. |
enabled | boolean | No | Default: true. |
Response — 201 Created
{
"id": "wh_01jv...",
"url": "https://myapp.example/webhooks/blacklake",
"secret": "whsec_8c17dd680f3059175a80a7c75b565cc0480c464a0d39e6d31674341173ed09f5",
"secret_suffix": "a3f9",
"events": ["approval.created", "approval.approved", "approval.rejected"],
"enabled": true,
"created_at": "2026-04-08T10:00:00.000Z",
"warning": "Save this secret — it will not be shown again."
}
curl
curl -X POST https://api.blacklake.systems/v1/webhooks \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://myapp.example/webhooks/blacklake",
"events": ["approval.created", "approval.approved", "approval.rejected"]
}'
GET /v1/webhooks/:id
Retrieve a single webhook by ID. Does not return the signing secret.
Response — 200 OK or 404 WEBHOOK_NOT_FOUND.
curl
curl https://api.blacklake.systems/v1/webhooks/wh_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
PATCH /v1/webhooks/:id
Update webhook fields. Only provided fields are modified.
Request body — all fields optional
| Field | Type | Description |
|---|---|---|
url | string | New receiver URL. |
events | array | Replacement event list. |
enabled | boolean | Enable or disable the webhook. |
Response — 200 OK with the updated Webhook object.
curl
curl -X PATCH https://api.blacklake.systems/v1/webhooks/wh_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'
DELETE /v1/webhooks/:id
Permanently delete a webhook.
Response — 204 No Content
curl
curl -X DELETE https://api.blacklake.systems/v1/webhooks/wh_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
GET /v1/webhooks/:id/deliveries
List delivery attempts for a webhook. Results are ordered by delivered_at descending.
Query parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Number of records to return. Default: 50. |
offset | integer | Pagination offset. Default: 0. |
Response — 200 OK
{
"data": [
{
"id": "whd_01jv...",
"webhook_id": "wh_01jv...",
"event": "approval.created",
"payload": { "id": "whd_01jv...", "event": "approval.created", "created_at": "...", "data": { } },
"status_code": 200,
"response_body": "ok",
"error": null,
"delivered_at": "2026-04-08T12:34:57.100Z",
"duration_ms": 312
}
],
"total": 14
}
curl
curl "https://api.blacklake.systems/v1/webhooks/wh_01jv.../deliveries?limit=10" \
-H "x-api-key: $BLACKLAKE_API_KEY"
Organisation
GET /v1/organisation
Return the current organisation. The organisation is derived from the API key.
Response — 200 OK
{
"id": "org_01jv...",
"name": "Acme Corp",
"contact_email": "ops@acme.example",
"created_at": "2026-04-06T09:00:00.000Z"
}
curl
curl https://api.blacklake.systems/v1/organisation \
-H "x-api-key: $BLACKLAKE_API_KEY"
POST /v1/organisation/delete
Permanently delete the organisation and all associated data: agents, tools, policies, bindings, the evaluation audit log, and every API key. The deletion runs in a single database transaction and is irreversible.
The caller must pass the organisation's exact name as confirmation — any other value is rejected with 400 CONFIRMATION_MISMATCH.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
confirmation | string | yes | The organisation's exact name. |
Response — 204 No Content
curl
curl -X POST https://api.blacklake.systems/v1/organisation/delete \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "confirmation": "Acme Corp" }'
API Keys
GET /v1/api-keys
List all API keys for the current organisation. The raw key is never returned — only the last four characters as key_suffix for identification.
Response — 200 OK
{
"keys": [
{
"id": "key_01jv...",
"name": "production",
"key_suffix": "af7d",
"created_at": "2026-04-06T09:00:00.000Z",
"revoked_at": null
}
]
}
curl
curl https://api.blacklake.systems/v1/api-keys \
-H "x-api-key: $BLACKLAKE_API_KEY"
POST /v1/api-keys
Generate a new API key. The raw bl_-prefixed key is returned once in the key field. Store it securely on receipt.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | A label for the key (1–100 chars). |
Response — 201 Created
{
"id": "key_01jv...",
"name": "production",
"key": "bl_8c17dd680f3059175a80a7c75b565cc0480c464a0d39e6d31674341173ed09f5",
"created_at": "2026-04-06T09:00:00.000Z",
"warning": "Save this key — it will not be shown again."
}
curl
curl -X POST https://api.blacklake.systems/v1/api-keys \
-H "x-api-key: $BLACKLAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "production" }'
DELETE /v1/api-keys/:id
Revoke an API key by ID. Sets revoked_at to the current timestamp; the row is preserved for audit purposes. Subsequent requests using the revoked key return 401 UNAUTHORIZED.
The API rejects attempts to revoke the key currently being used to make the request, returning 400 CANNOT_REVOKE_SELF. Idempotent: revoking an already-revoked key returns 204 without changes.
Response — 204 No Content
curl
curl -X DELETE https://api.blacklake.systems/v1/api-keys/key_01jv... \
-H "x-api-key: $BLACKLAKE_API_KEY"
Signup
POST /v1/signup
Create a new organisation and its initial API key. Public endpoint — does not require authentication. Rate-limited to 5 requests per hour per IP address.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
organisation_name | string | yes | Display name for the new organisation (2–100 chars). |
email | string | yes | Contact email (also used to detect duplicate organisations). |
accept_terms | true | yes | Must be the literal boolean true. |
turnstile_token | string | no | Cloudflare Turnstile token if Turnstile is enabled on the deployment. |
Response — 201 Created
{
"organisation": {
"id": "org_01jv...",
"name": "Acme Corp",
"contact_email": "ops@acme.example"
},
"api_key": "bl_8c17dd680f3059175a80a7c75b565cc0480c464a0d39e6d31674341173ed09f5",
"api_key_id": "key_01jv...",
"email_sent": true,
"warning": "This is the only time your API key will be shown. Save it now."
}
The api_key is shown once. A welcome email is sent to the contact address with the same key (best-effort — email_sent: false if delivery failed).
Errors
| Code | Status | Cause |
|---|---|---|
VALIDATION_ERROR | 400 | Missing or malformed fields. |
EMAIL_EXISTS | 409 | Another organisation already uses this email. |
CAPTCHA_FAILED | 400 | Turnstile token verification failed. |
RATE_LIMITED | 429 | More than 5 signup attempts from this IP in the last hour. |
curl
curl -X POST https://api.blacklake.systems/v1/signup \
-H "Content-Type: application/json" \
-d '{
"organisation_name": "Acme Corp",
"email": "ops@acme.example",
"accept_terms": true
}'