Skip to content
BLACKLAKE

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.

Response200 OK

{ "status": "ok" }

curl

curl http://localhost:3100/health

GET /v1/mode

Returns the current operating mode.

Response200 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.

Response200 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
    }
  ]
}
FieldDescription
nameThe server name from mcp-config.json.
statusrunning, stopped, or error.
proxy_urlThe URL your MCP client should connect to.
tool_countNumber of tools discovered from this server.
last_seenTimestamp of the most recent tool call through this proxy. null if no calls have been made yet.
errorError 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

ParameterDescription
nameThe server name as it appears in mcp-config.json.

Response200 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

ParameterTypeDescription
periodstringTime range: today, 7d, 30d, or all. Default: 7d.
providerstringFilter by provider: anthropic, openai, or ollama.

Response200 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

FieldTypeRequiredDescription
namestringYesAgent name. Must be unique within the organisation.
descriptionstringNoOptional description.
environmentdevelopment | staging | productionYesDeployment environment.
risk_classificationlow | medium | high | criticalYesRisk level of the agent.
approval_modeauto_approve | require_approval | blockNoDefault: auto_approve.

Response201 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

ParameterTypeDescription
environmentstringFilter by environment.
statusstringFilter by status.

Response200 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.

Response200 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

FieldTypeDescription
namestringNew name.
descriptionstringNew description.
environmentstringNew environment.
risk_classificationstringNew risk classification.
statusstringNew status.
approval_modestringNew approval mode.

Response200 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.

Response200 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.

Response200 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

FieldTypeRequiredDescription
tool_idstringYesID of the tool to bind.

Response201 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.

Response200 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.

Response204 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

FieldTypeRequiredDescription
namestringYesTool name. Must be unique within the organisation.
descriptionstringNoOptional description.
risk_classificationlow | medium | high | criticalYesRisk level of the tool.

Response201 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.

Response200 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.

Response200 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

FieldTypeRequiredDescription
namestringYesDescriptive policy name.
priorityintegerYesEvaluation order. Lower = evaluated first.
agent_selectorobjectNoKey-value pairs to match against the agent. Default: {} (matches all).
tool_selectorobjectNoKey-value pairs to match against the tool. Default: {} (matches all).
outcomeallow | deny | approval_requiredYesDecision to return when this policy matches.
enabledbooleanNoDefault: true.

Response201 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.

Response200 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.

Response200 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.

Response200 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.

Response204 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

FieldTypeRequiredDescription
agentstringYesAgent name.
toolstringYesTool name.
actionobjectNoPayload describing the intended action. Stored on the evaluation record.
contextobjectNoCaller-supplied context (user ID, session, run ID, etc.). Stored on the evaluation record.

Response200 OK

FieldTypeDescription
decisionstringallow, deny, approval_required, or default_deny.
evaluation_idstringID of the recorded evaluation.
policy_idstring | nullID of the matching policy, or null.
reasonstringHuman-readable explanation.
evaluated_atstringISO 8601 UTC timestamp.
approval_idstringPresent 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

Scenariodecisionreason
Agent is suspendeddenyAgent is suspended
Agent is disableddenyAgent is disabled
No binding for this agent+tooldenyTool is not bound to agent
Policy matched with deny outcomedenyMatched policy: <policy name>
No policy matcheddefault_denyNo matching policy found

Evaluations

GET /v1/evaluations

List evaluation records. Results are ordered by evaluated_at descending (most recent first).

Query parameters

ParameterTypeDescription
agent_idstringFilter by agent ID.
tool_idstringFilter by tool ID.
outcomestringFilter by outcome: allow, deny, approval_required, or default_deny.
limitintegerNumber of records to return. Default: 50.
offsetintegerPagination offset. Default: 0.

Response200 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.

Response200 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

ParameterTypeDescription
statusstringFilter by status: pending, approved, rejected, or expired.
agent_idstringFilter by agent ID.
tool_idstringFilter by tool ID.
limitintegerNumber of records to return. Default: 50.
offsetintegerPagination offset. Default: 0.

Response200 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.

Response200 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.

Response200 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

FieldTypeRequiredDescription
decided_bystringYesIdentifier of the person or system making the decision.
reasonstringNoOptional explanation.

Response200 OK — the updated Approval object with status: "approved".

Errors

CodeStatusCause
APPROVAL_NOT_FOUND404No approval with this ID in the organisation.
APPROVAL_ALREADY_DECIDED400The approval was already approved or rejected.
APPROVAL_EXPIRED400The 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

FieldTypeRequiredDescription
decided_bystringYesIdentifier of the person or system making the decision.
reasonstringNoOptional explanation.

Response200 OK — the updated Approval object with status: "rejected".

Errors

CodeStatusCause
APPROVAL_NOT_FOUND404No approval with this ID in the organisation.
APPROVAL_ALREADY_DECIDED400The approval was already approved or rejected.
APPROVAL_EXPIRED400The 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.

Eventdata 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.

Response200 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

FieldTypeRequiredDescription
urlstringYesHTTPS URL to deliver events to.
eventsarrayYesOne or more of approval.created, approval.approved, approval.rejected.
enabledbooleanNoDefault: true.

Response201 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.

Response200 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

FieldTypeDescription
urlstringNew receiver URL.
eventsarrayReplacement event list.
enabledbooleanEnable or disable the webhook.

Response200 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.

Response204 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

ParameterTypeDescription
limitintegerNumber of records to return. Default: 50.
offsetintegerPagination offset. Default: 0.

Response200 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.

Response200 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

FieldTypeRequiredDescription
confirmationstringyesThe organisation's exact name.

Response204 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.

Response200 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

FieldTypeRequiredDescription
namestringyesA label for the key (1–100 chars).

Response201 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.

Response204 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

FieldTypeRequiredDescription
organisation_namestringyesDisplay name for the new organisation (2–100 chars).
emailstringyesContact email (also used to detect duplicate organisations).
accept_termstrueyesMust be the literal boolean true.
turnstile_tokenstringnoCloudflare Turnstile token if Turnstile is enabled on the deployment.

Response201 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

CodeStatusCause
VALIDATION_ERROR400Missing or malformed fields.
EMAIL_EXISTS409Another organisation already uses this email.
CAPTCHA_FAILED400Turnstile token verification failed.
RATE_LIMITED429More 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
  }'