Skip to content

Management API ​

When running in gateway mode (not stdio), Airlock can expose a separate control-plane REST API for health checks, approval management, hook decisions, admin tool catalog access, and audit log queries. This listener is distinct from the agent data-plane listener.

Configuration ​

The management API is disabled by default. When enabled, it binds to loopback on port 4113 unless configured otherwise:

yaml
server:
  api_secret: ${AIRLOCK_API_SECRET}
  management_api:
    enabled: true
    api_secret: ${MANAGEMENT_API_SECRET}
    host: 127.0.0.1
    port: 4113
    insecure_remote_bind: false

The management API port must not equal server.port; Airlock refuses to start if the control-plane and agent data-plane would share a socket. Binding the management API to a non-loopback address also refuses to start unless management_api.insecure_remote_bind: true is set. Only use that opt-in behind trusted network controls such as private interfaces, VPN-only routes, or a reverse proxy route that excludes the agent-reachable path.

server.expose_management_api is deprecated. For one release it maps to server.management_api.enabled and emits a warning.

Authentication ​

All management endpoints require a bearer token when management_api.enabled is true. Prefer a dedicated control-plane secret:

yaml
server:
  api_secret: ${AIRLOCK_API_SECRET}
  management_api:
    enabled: true
    api_secret: ${MANAGEMENT_API_SECRET}

server.management_api.api_secret protects the control-plane listener: /health, /hitl/*, /audit, the dashboard approval bridge (/events, /approve, /deny, /version*), /activity, /mobile/*, /admin/tools, and /hook. server.api_secret remains the data-plane fallback for tokenless agents on MCP and REST tools routes. When the management API is enabled, every agent must also set its own token; Airlock rejects tokenless agents in split mode so the data-plane fallback cannot become an implicit agent credential.

For backward compatibility, if server.management_api.api_secret is unset, the management API falls back to server.api_secret and emits a deprecation warning during config validation. This keeps existing single-secret deployments working while nudging operators to split and rotate the control-plane secret. If both fields resolve to the same value, config validation warns because the two planes are still sharing one credential.

bash
curl -H "Authorization: Bearer $MANAGEMENT_API_SECRET" http://localhost:4113/health

To migrate an existing deployment, generate a fresh MANAGEMENT_API_SECRET, set server.management_api.api_secret: ${MANAGEMENT_API_SECRET}, recreate or restart Airlock, and update companion apps or dashboards to send Authorization: Bearer $MANAGEMENT_API_SECRET. Leave server.api_secret in place only as the data-plane fallback, or remove it entirely when all agents have per-agent tokens and no tokenless fallback is needed.

Per-device or per-client companion tokens are a follow-up hardening step. They would let operators revoke one companion without rotating the shared management secret, but that is separate from this shared control-plane secret split.

yaml
server:
  management_api:
    enabled: false

When management_api.enabled is false, /health, /hitl/*, /audit, the dashboard approval bridge (/events, /approve, /deny, /version*), /activity, /mobile/*, /admin/tools, and /hook are not registered on any listener.

Agent-facing REST tool execution is not part of the management API. When server.expose_tools_api enables it (mode all, or per-agent with the agent opted in via expose_tools_api: true), /agents/:agentId/tools and /agents/:agentId/tools/invoke are served by the data-plane listener alongside the MCP transports and use agent-scoped authentication.

Endpoints ​

GET /health ​

Returns gateway health status, pending HITL count, and uptime.

json
{
  "status": "ok",
  "dataPlane": {
    "status": "ok",
    "host": "127.0.0.1",
    "port": 4111
  },
  "mcpHealth": {
    "github": "ok"
  },
  "credentialHealth": {
    "github": {
      "status": "ok",
      "source": "probe",
      "checkedAt": "2026-04-01T12:00:00.000Z"
    }
  },
  "toolCatalog": {
    "github": {
      "count": 42,
      "fingerprint": "sha256:9f2b…"
    }
  },
  "uptime": 3600,
  "pendingApprovals": 2
}

dataPlane is a live TCP probe of the agent MCP listener. mcpHealth reports the downstream MCP provider pool behind that listener.

credentialHealth answers a different question: not "is this provider reachable?" but "does its credential still work?" The two are independent — a provider whose OAuth refresh token has died keeps serving MCP happily, so mcpHealth stays ok while every real call fails. One entry per pooled provider:

FieldMeaning
statusok, auth_required, error, or unknown
sourceconnection (the transport's own OAuth state), probe (an actual call), or none
reasonShort explanation; omitted on a clean ok
checkedAtISO timestamp of the last completed probe; only when source is probe

Providers Airlock authenticates itself (oauth: true) automatically receive a cached, read-only listTools probe, so they earn a positive result without configuration as well as reporting negative failures from the transport. Define a credential_probe to override that generic check with a stronger business call. Everything else stays unknown until it has an explicit probe — a green that hasn't been earned is worse than an honest unknown.

toolCatalog answers a third question neither of the others can: has the provider changed what its tools do? One entry per provider, hashing every tool name, description, and input schema as agents actually receive them.

FieldMeaning
countTools currently served by that provider
fingerprintsha256:<hex> over the provider's whole tool contract

The fingerprint is order-independent — tools are sorted by name and schema keys are sorted at every depth — so a reconnect never looks like a change. It moves only when the contract does.

Pin it. A monitor that asserts the fingerprint you last reviewed will tell you the day a provider renames a parameter, drops one, or rewrites a description, none of which move count and none of which any other check notices. This is also a prompt-injection tripwire: a tool description is instructions to an agent, so an upstream silently rewriting one deserves a human read before it takes effect. Expect it to change on legitimate upstream updates too — that is the point, and re-pinning is the act of reviewing what changed.

Recomputed on registry refresh, never per request, so polling /health costs nothing extra.

Probes are cached per provider and refreshed lazily on read, so polling /health on a short interval costs at most one real call per interval_ms and never blocks on one. Alert on auth_required; treat unknown as "not yet answered", not as a failure.

GET /hitl/pending ​

Lists all pending approval requests.

json
[
  {
    "id": "abc123",
    "code": "A1B2C3",
    "agent_id": "claude-code",
    "tool": "exec/run",
    "args": { "command": "git push origin main" },
    "status": "pending",
    "created_at": "2026-04-01T12:00:00Z"
  }
]

POST /hitl/approve/:id ​

Approve a pending request by canonical approval ID. Short approval codes are not accepted on this programmatic endpoint.

bash
curl -X POST http://localhost:4113/hitl/approve/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET"

To also allow similar calls for a while, pass a grant. Any other pending request the grant covers is approved too, and the response lists every approved id:

bash
curl -X POST http://localhost:4113/hitl/approve/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"grant": {"scope": "tier", "tier": "read", "duration_ms": 3600000}}'

scope is tool, tier (with tier: read|write), provider or profile (with profile). Optional: duration_ms, max_uses, session_only. Requests flagged by a security check can't be widened into a grant; trying returns 400.

session_only binds the grant to the agent session that made the request, so other agents sharing the same profile aren't covered. Over streamable HTTP the session is the id the agent sends (X-Airlock-Session-Id, ?session=, or _meta["dev.airlock/session"], see Connecting HTTP Agents); in stdio mode it's the process. A request with no session id can't take a session_only grant (400), so it is never silently widened to the whole profile.

POST /hitl/approvals/decisions ​

Decide an explicit list of approvals in one call. Flagged (escalated) requests are skipped on approve unless you pass "include_flagged": true.

bash
curl -X POST http://localhost:4113/hitl/approvals/decisions \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["…", "…"], "decision": "approved"}'
# → {"ok": true, "approved": ["…"], "skipped": ["…"]}

POST /hitl/batches/:batch/decision ​

Approve or deny a notification batch by batch id or batch code: {"decision": "approved"}. Approving covers only the batch's own (non-flagged) members.

GET /hitl/grants · POST /hitl/grants · DELETE /hitl/grants/:id ​

List active grants (?agent= to filter), pre-approve without waiting for a request, or revoke one.

bash
curl -X POST http://localhost:4113/hitl/grants \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"agent": "research", "scope": {"kind": "provider", "provider": "gmail"}, "duration_ms": 1800000}'

The same routes are available to companion devices under /mobile/approvals/decisions, /mobile/batches/:batch/decision and /mobile/grants.

POST /hitl/deny/:id ​

Deny a pending request by canonical approval ID. Short approval codes are not accepted on this programmatic endpoint. Optionally include a reason:

bash
curl -X POST http://localhost:4113/hitl/deny/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Not authorized for production pushes"}'

GET /audit ​

Query the audit log. All parameters are optional:

ParameterDescription
agentFilter by agent ID
toolFilter by tool name
sinceISO 8601 timestamp — only entries after this time
limitMaximum number of entries to return
bash
curl -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  "http://localhost:4113/audit?agent=claude-code&tool=exec/run&limit=50"

Returns an array of audit entries:

json
[
  {
    "id": "xyz789",
    "agent_id": "claude-code",
    "tool": "exec/run",
    "args": "{\"command\":\"git status\"}",
    "result": "success",
    "duration_ms": 142,
    "hitl_outcome": null,
    "created_at": "2026-04-01T12:01:00Z"
  }
]

GET /events ​

Streams approval events for dashboard-style clients with Server-Sent Events. This endpoint is used by the in-process dashboard, standalone dashboard, and macOS Companion app.

The stream replays currently pending approvals when a client connects, then sends live new, resolved, and activity messages. Resolution events are emitted no matter which surface resolves the approval, including dashboard, mobile, companion, timeout, or cancellation paths.

Browser EventSource cannot set an Authorization header directly. In split mode, run airlock dashboard with --gateway-secret or AIRLOCK_GATEWAY_SECRET; the dashboard server proxies the SSE connection to the gateway with the bearer token. If your gateway config uses MANAGEMENT_API_SECRET, pass that value to the dashboard explicitly.

Dashboard and companion clients should treat the persisted approval ID as the canonical identity. The short approval code is display/manual-entry sugar.

POST /approve?code=ABC123 ​

Approve a pending request by short approval code. This manual-entry endpoint is used by dashboard-style browser flows. App clients should post decisions by canonical approval ID instead.

bash
curl -X POST \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  "http://localhost:4113/approve?code=ABC123"

POST /deny?code=ABC123 ​

Deny a pending request by short approval code. App clients should post decisions by canonical approval ID instead.

bash
curl -X POST \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  "http://localhost:4113/deny?code=ABC123"

POST /approval-preview/:id ​

Read configured operator context for a pending approval by canonical ID. This route is available only on the management listener and operator dashboard, not the agent tool listener. The request body cannot select the tool or arguments; those come from approvals.previews and the pending request.

The response is one of:

json
{
  "status": "ready",
  "tool": "provider/read",
  "text": "Subject: Replacement approved",
  "truncated": false,
  "fields": [{ "label": "Subject", "value": "Replacement approved", "primary": false }],
  "requestedFields": [{ "label": "Message to send", "value": "Thank you.", "primary": true }]
}
json
{ "status": "unavailable" }
json
{ "status": "error", "message": "Preview read failed." }

fields and requestedFields are optional ordered lists. fields come from the configured provider read; requestedFields come from selected request arguments and are displayed separately as the requested action. text remains the readable fallback for clients without field rendering.

Unknown or resolved approvals return HTTP 409. Responses use Cache-Control: no-store. Reads leave the approval pending and never return their content to the requesting agent. See approval previews and dashboard access boundaries.

Mobile companion API ​

The mobile endpoints are exposed with the management API. Registering or revoking devices requires the management bearer token. Queue, history, push token update, preview, and decision calls may use either the management token or the per-device token returned during registration.

POST /mobile/devices/register ​

Register an iOS device for APNs approval notifications.

bash
curl -X POST http://localhost:4113/mobile/devices/register \
  -H "Authorization: Bearer $MANAGEMENT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"name":"Charles iPhone","platform":"ios","pushToken":"<apns-device-token>"}'

Returns the device id and a one-time-visible device bearer token:

json
{
  "id": "device-id",
  "name": "Charles iPhone",
  "platform": "ios",
  "token": "airlock_mobile_..."
}

GET /mobile/devices ​

List active registered mobile devices. Requires the admin bearer token.

DELETE /mobile/devices/:id ​

Revoke a registered mobile device. Requires the admin bearer token.

DELETE /mobile/device ​

Revoke the calling mobile device. Requires that device's bearer token, or the admin bearer token.

PUT /mobile/device ​

Update the calling device's APNs token after iOS rotates it.

GET /mobile/approvals ​

List currently pending approvals in the mobile app shape.

GET /mobile/approvals/stream ​

Streams the same approval lifecycle as /events, under the mobile namespace and with mobile authentication. The endpoint accepts either the management bearer token or a registered device token. It replays pending approvals on connect, sends live new, resolved, and activity messages, and keeps the connection alive with SSE comments.

bash
curl -N http://localhost:4113/mobile/approvals/stream \
  -H "Authorization: Bearer $AIRLOCK_MOBILE_DEVICE_TOKEN" \
  -H "Accept: text/event-stream"

GET /mobile/approvals/history?limit=50 ​

List recently resolved approvals from the persisted HITL queue.

GET /mobile/activity ​

List recent activity events in the mobile app shape.

POST /mobile/approvals/:id/preview ​

Fetch operator-only context for a pending approval using a management or active per-device bearer token. This uses the same configured lookup, pending-only access, in-memory cache, response shapes, and no-store behavior as POST /approval-preview/:id. Caller-supplied bodies cannot change the lookup. It works before notification delivery; unknown or resolved IDs return HTTP 409. Revoked device tokens return HTTP 401.

The updated iOS and macOS companions call this route when opening a pending approval's detail view. Preview content is not added to queue, history, streams, or push payloads. Gateways without this route return HTTP 404, which companions treat as an unavailable preview.

POST /mobile/approvals/:id/decision ​

Approve or deny a pending approval by canonical approval ID. Short approval codes are not accepted here.

An already-resolved approval returns HTTP 409 with {"error":"Approval already resolved","code":"approval_resolved"}. The original decision and remembered permissions remain unchanged. An unknown ID returns 404; a pending database row with no active engine request returns 409 with {"error":"Pending approval is not active in the approval engine"}.

bash
curl -X POST http://localhost:4113/mobile/approvals/550e8400-e29b-41d4-a716-446655440000/decision \
  -H "Authorization: Bearer $AIRLOCK_MOBILE_DEVICE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"decision":"approved","remember":"temporary","duration_ms":3600000}'

GET /admin/tools ​

Returns the full gateway tool catalog for the dashboard. This is an admin view, not the filtered tool list an agent receives.

json
{
  "tools": [],
  "errors": []
}

GET /activity ​

Returns recent activity events for dashboard-style clients.

GET /version ​

Returns the running Airlock version.

GET /version/latest ​

Checks npm for the latest published airlock-bot version. Dashboard clients use this for the upgrade banner.

Hook API ​

Airlock can also expose a /hook endpoint on the management listener for non-MCP tools that want policy and approval decisions. See Hook Endpoint for details.

MIT Licensed