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:
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: falseThe 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:
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.
curl -H "Authorization: Bearer $MANAGEMENT_API_SECRET" http://localhost:4113/healthTo 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.
server:
management_api:
enabled: falseWhen 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.
{
"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:
| Field | Meaning |
|---|---|
status | ok, auth_required, error, or unknown |
source | connection (the transport's own OAuth state), probe (an actual call), or none |
reason | Short explanation; omitted on a clean ok |
checkedAt | ISO 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.
| Field | Meaning |
|---|---|
count | Tools currently served by that provider |
fingerprint | sha256:<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.
[
{
"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.
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:
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.
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.
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:
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:
| Parameter | Description |
|---|---|
agent | Filter by agent ID |
tool | Filter by tool name |
since | ISO 8601 timestamp — only entries after this time |
limit | Maximum number of entries to return |
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:
[
{
"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.
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.
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:
{
"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 }]
}{ "status": "unavailable" }{ "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.
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:
{
"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.
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"}.
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.
{
"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.