CL
Clausgap
/Docs

API Reference

All REST endpoints for the Clausgap control plane. The proxy itself exposes /healthz, /readyz, and /metrics — see Proxy configuration.

Base URLhttps://api.clausgap.dev
Auth

API key: Authorization: Bearer clk_free.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

OIDC session: HttpOnly cookie clausgap_session (set after dashboard login)

Roles

viewer · author · approver · admin — escalating permissions

API key scopes

The auth label on each endpoint below is for a signed-in person (Viewer+, Admin and so on). An API key reaches only the routes its scopes allow; anything else is refused with 403 KEY_SCOPE.

  • proxy: for clausgap-proxy. Fetch the policy bundle, heartbeat, send decision events and forensic records, create and check escrow holds, read agent-key and token-issuer configuration.
  • pipeline: for CI and policies as code. List, create, update, validate, export, revoke and roll back policies, read their diffs and summaries, and read decisions (GET /v1/events) and the audit trail (GET /v1/audit).
  • admin: every route a key can reach, including issuing keys and agent keys and changing settings. Not POST /v1/evaluate: agents use agent keys.

Agent keys are not API keys: an agent key can call only POST /v1/evaluate (with X-Clausgap-Tenant) and GET /v1/escrow/{id} for its own holds.

Authentication

GET/auth/loginpublic

Redirect to OIDC provider. Returns 302 to the configured identity provider login page.

GET/auth/callbackpublic

OIDC callback. Validates code + state, sets session cookie, redirects to /policies.

POST/auth/magic-linkpublic

Emails a one-time sign-in link. Body: {"email": "..."}. Always returns 200, whether or not the address is known; rate limited.

GET/auth/magic-link/verifypublic

Exchanges the link's ?token= for a 24-hour session. An invalid or expired token returns 401.

GET/auth/sso/startpublic

Single sign-on. Takes ?email=, finds the organization that verified that email's domain, and redirects to its identity provider.

POST/auth/logoutsession

Clears the session cookie. Returns {"status":"logged_out"}.

GET/auth/meAPI key or session

Returns the identity of the authenticated caller.

Response

json
// API key response
{
  "authenticated": true,
  "auth_method": "api_key",
  "tenant_id": "acme-corp"
}

// OIDC session response
{
  "authenticated": true,
  "auth_method": "oidc",
  "tenant_id": "acme-corp",
  "user": {
    "id": "usr_abc",
    "email": "alice@acme.com",
    "name": "Alice",
    "avatar_url": "https://..."
  },
  "roles": ["admin"]
}

Policies

Policies are the core governance artifact. Write them in the Clausgap DSL. On save, the control plane compiles them into a signed binary bundle and pushes it to connected proxies within one poll interval.

GET/v1/policiesViewer+

List all policies. Query params: status (draft|pending_approval|deployed|revoked), agent_role, limit, offset.

Response

json
{
  "policies": [
    {
      "id": "pol_abc123",
      "dsl_source": "policy \"my_policy\" { ... }",
      "status": "deployed",
      "version": 3,
      "content_hash": "sha256:...",
      "author_id": "usr_abc",
      "created_at": "2025-07-01T10:00:00Z",
      "updated_at": "2025-07-24T09:15:00Z"
    }
  ]
}
POST/v1/policiesAuthor+

Create a new policy. The policy starts in draft status and must be approved before deploying.

Request body

json
{
  "id": "my_policy",
  "dsl_source": "policy \"my_policy\" {\n  agent_role = \"analyst\"\n  ...\n}"
}

Response

json
{
  "id": "my_policy",
  "status": "draft",
  "version": 1,
  "content_hash": "sha256:...",
  "created_at": "2025-07-24T10:00:00Z"
}
GET/v1/policies/{id}Viewer+

Get a single policy with full DSL source and version history.

PUT/v1/policies/{id}Author+

Update a policy's DSL source. Creates a new version.

Request body

json
{ "dsl_source": "policy \"my_policy\" { ... }" }
DELETE/v1/policies/{id}Author+

Revoke a policy. The proxy stops enforcing it within one poll interval.

POST/v1/policies/{id}/rollbackAuthor+

Roll back to a previous version.

Request body

json
{ "target_version": 2 }
POST/v1/policies/validateViewer+

Validate DSL syntax without saving. Returns structured errors with line and column positions.

Request body

json
{ "dsl_source": "policy \"test\" { ... }" }

Response

json
{
  "valid": false,
  "errors": [
    { "line": 4, "column": 3, "message": "unexpected token 'denye'" }
  ],
  "warnings": []
}
GET/v1/policies/bundleAPI key

Fetch the compiled binary policy bundle. Used by the proxy. Returns 304 Not Modified if bundle hash matches current_hash query param. Pass ?scopes=default,finance to receive only policies tagged with those scopes — the control plane filters and re-signs the bundle before returning it.

Governance workflow — submit a draft for review (POST /v1/policies/{id}/submit), then approve (POST /v1/policies/{id}/approve, body { "version": N } — the version you reviewed; a policy that changed since answers 409 VERSION_CHANGED), reject or request changes. Break-glass (POST /v1/policies/{id}/break-glass, admin only, requires { "reason": "..." }) makes the latest version enforced at once and is audited at critical severity.

With approval required (a tenant setting), no change is enforced until someone other than its author approves it: creating, editing (PUT) or rolling back a policy — from the dashboard, the API or a pipeline's API key — adds a draft revision, and proxies keep enforcing the last approved version (live_version in every policy response) until the revision is approved. Revoking a live policy then needs a signed-in admin. Signed-in users need the author role to create, edit, roll back or submit policies.

API Keys

API keys authenticate programmatic access to the control plane and are used by the proxy to fetch policy bundles and drain events. The key value is only returned at creation time.

GET/v1/keysAdmin

List all API keys. The plaintext key value is never returned after creation.

Response

json
{
  "keys": [
    {
      "id": "key_abc",
      "label": "production-proxy",
      "prefix": "clk_free.xxxx",
      "created_at": "2025-07-01T10:00:00Z"
    }
  ]
}
POST/v1/keysAdmin

Issue a new API key with the scopes it needs: proxy (for clausgap-proxy), pipeline (policies as code) or admin. A key may make only the requests its scopes allow. The plaintext key is returned once.

Request body

json
{ "label": "production-proxy", "scopes": ["proxy"] }

Response

json
{
  "id": "key_abc",
  "key": "clk_free.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "label": "production-proxy",
  "scopes": ["proxy"],
  "created_at": "2025-07-24T10:00:00Z"
}
DELETE/v1/keys/{id}Admin

Revoke an API key. Takes effect immediately — the proxy will fail to fetch bundles on the next poll.

Escrow

Escrow holds agent actions (423 Locked) until approval conditions are satisfied — a single manual approval, multi-party voting, conditional release on an external webhook, or a time-delayed auto-release. The agent retries with X-Clausgap-Escrow-Id once released.

GET/v1/escrowViewer+

List escrow holds. Query params: status (held|released|rejected|consumed|expired|cancelled), agent_role, agent_id, action_type, min_value, max_value, limit, offset.

GET/v1/escrow/{id}API key or Viewer+

Get a single escrow hold. Poll this endpoint to check if the hold has been released.

Response

json
{
  "id": "esw_abc123",
  "agent_role": "treasury_bot",
  "agent_id": "bot-1",
  "action_type": "api_call",
  "action_summary": "POST /submit_payment",
  "status": "held",
  "hold_type": "multi_party",
  "required_approvals": 2,
  "current_approvals": 1,
  "estimated_value": 48200.00,
  "value_currency": "USD",
  "risk_score": 0.85,
  "ttl_seconds": 3600,
  "expires_at": "2025-07-24T15:30:00Z",
  "created_at": "2025-07-24T14:30:00Z"
}
POST/v1/escrow/{id}/voteApprover

Cast an approval or rejection vote (one per person). When required_approvals is reached the hold is released; any rejection rejects it.

Request body

json
{
  "decision": "approve",
  "reason": "Confirmed with procurement team"
}

Response

json
{
  "escrow_id": "esw_abc123",
  "your_decision": "approve",
  "current_approvals": 2,
  "required_approvals": 2,
  "status": "released",
  "released_at": "2025-07-24T14:55:00Z"
}
POST/v1/escrow/{id}/consumeAPI key (the proxy)

Used by the proxy when the agent resends the approved request. A released hold is consumed exactly once, atomically, and only by the agent that requested it for the exact action approved. Refusals: 409 ALREADY_CONSUMED, 422 HASH_MISMATCH (a different request), 403 AGENT_MISMATCH (another agent), ESCROW_EXPIRED, INVALID_STATUS.

Request body

json
{ "action_hash": "sha256:…", "agent_role": "treasury_bot", "agent_id": "bot-1" }
POST/v1/escrow/{id}/satisfyAPI key

Mark an external condition as satisfied (for conditional hold type).

Request body

json
{ "condition_id": "ci_check", "satisfied": true, "evidence": "build:12345" }
POST/v1/escrow/{id}/cancelAPI key or Admin

Cancel an escrow hold.

Request body

json
{ "reason": "Action no longer needed" }

Events

Every proxy decision (allow, allow_with_masking, deny, escrow, rate_limited) is batched and drained to the control plane as an event. The control plane stores them as a tamper-evident forensic log. Events are ingested by the proxy; the dashboard reads them.

GET/v1/eventsViewer+

List decision events. Query params: event_type, agent_id, session_id, decision (allow|allow_with_masking|deny|escrow|rate_limited), since (ISO 8601), after (cursor), limit (1–1000).

Response

json
{
  "events": [
    {
      "id": "evt_abc",
      "event_type": "decision.deny",
      "proxy_id": "proxy-1",
      "agent_role": "analyst",
      "agent_id": "a-123",
      "session_id": "sess-xyz",
      "tool_name": "delete_records",
      "method": "DELETE",
      "decision": "deny",
      "policy_id": "my_policy",
      "deny_reason": "explicit deny rule matched",
      "evaluation_us": 312,
      "created_at": "2025-07-24T14:30:00Z"
    }
  ],
  "next_cursor": "evt_xyz",
  "has_more": true
}
GET/v1/events/{id}Viewer+

Get a single event with full detail including input hash, policy snapshot reference, and forensic chain position.

POST/v1/events/ingestAPI key

Batch-ingest events from the proxy. Max 100 events per request. Used internally by the proxy.

Request body

json
{
  "events": [
    {
      "event_id": "evt_abc",
      "event_type": "decision.allow",
      "timestamp": "2025-07-24T14:30:00.000Z",
      "tenant_id": "acme-corp",
      "proxy_id": "proxy-1",
      "agent_role": "analyst",
      "agent_id": "a-123",
      "session_id": "sess-xyz",
      "tool_name": "web_search",
      "method": "GET",
      "decision": "allow",
      "policy_id": "my_policy",
      "confidence": 0.95,
      "evaluation_us": 312,
      "total_latency_ms": 45
    }
  ]
}

Forensics & evidence

Every decision is also a forensic record in a per-tenant BLAKE3 hash chain. Verify the live chain any time, and export signed evidence packages that anyone can check offline with clausgap-verify (shipped in the control-plane image) — no access to your Clausgap needed. See Verifying evidence, including the package format for writing your own verifier.

GET/v1/forensics/verifyAPI key or Viewer+

Recompute the tenant's whole forensic chain. verified: false means a record was altered, inserted or removed; first_mismatch_id is where tampering starts. GET /v1/audit/verify does the same for the administrative audit log.

Response

json
{ "verified": true, "linear": true, "records_checked": 1520 }
POST/v1/forensics/exportSigned-in user

Export every record in a time window (default: the last month), or one session (sequence_id), as a signed evidence package — up to 10,000 records. The package is stored exactly as signed; record package_hash to prove later which package you were given.

Request body

json
{ "purpose": "regulatory_inquiry", "from": "2026-09-01T00:00:00Z", "to": "2026-10-01T00:00:00Z" }

Response

json
{
  "package_id": "epkg_…",
  "record_count": 1520,
  "chain_intact": true,
  "package_hash": "sha256:2365cc1e…",
  "public_key_fingerprint": "sha256:c3385619…"
}
GET/v1/forensics/packages/{id}/downloadSigned-in user

Download the package — the same bytes every time, after restarts and later database changes. Verify it offline: clausgap-verify --pubkey clausgap.pub package.json (the public key is served at /pubkey).

GET/v1/forensics/packagesAPI key or Viewer+

List exported packages, with each package_hash.

Subscriptions (Webhooks)

Subscribe to control-plane events over HTTP. Default destination_type is webhook — raw JSON, signed with HMAC-SHA256. Set it to slack or teams instead to post a pre-formatted, natively-authenticated Block Kit message or Adaptive Card straight to an incoming webhook URL — no signature verification needed, and escrow cards get one-click Approve/Deny buttons. See Webhook verification for signature verification, or its Slack & Microsoft Teams section for native destinations.

GET/v1/subscriptionsAdmin

List all webhook subscriptions.

POST/v1/subscriptionsAdmin

Create a subscription. destination_type is webhook (default), slack, or teams — see the note above. Leave event_types empty for every event. Private, loopback and cloud-metadata destinations are refused when saved.

Request body

json
{
  "url": "https://your-server.com/clausgap-webhook",
  "secret": "whsec_...",
  "event_types": [
    "escrow.created",
    "escrow.released",
    "escrow.vote_cast",
    "policy.approved"
  ],
  "active": true
}

// Or, for a native Slack/Teams destination — no secret needed:
{
  "destination_type": "slack",
  "url": "https://hooks.slack.com/services/T000/B000/XXXXXXXX",
  "event_types": ["escrow.created", "escrow.released"],
  "destination_config": { "app_url": "https://app.clausgap.dev" }
}

Response

json
{
  "id": "sub_abc",
  "url": "https://your-server.com/clausgap-webhook",
  "destination_type": "webhook",
  "event_types": ["escrow.created", "escrow.released"],
  "active": true,
  "created_at": "2025-07-24T10:00:00Z"
}
PUT/v1/subscriptions/{id}Admin

Update a subscription (url, secret, event_types, active).

DELETE/v1/subscriptions/{id}Admin

Delete a subscription. Deliveries already queued for it are not sent.

POST/v1/subscriptions/{id}/testAdmin

Send a signed test event (clausgap.test) to the destination now and report the result: DELIVERED, DESTINATION_BLOCKED, TIMEOUT, DNS_ERROR, TLS_ERROR, CONNECTION_FAILED, REJECTED_BY_DESTINATION, NOT_FOUND or DESTINATION_ERROR, with a reason. Webhook results include the payload sent.

Response

json
{
  "delivered": false,
  "result": "REJECTED_BY_DESTINATION",
  "reason": "the destination refused it (HTTP 401) — if it verifies X-Clausgap-Signature, check it uses this subscription's signing secret",
  "http_status": 401,
  "duration_ms": 41
}
GET/v1/subscriptions/{id}/deliveriesAdmin

List recent deliveries for a subscription: status (delivered, retrying, failed), attempt count, HTTP status, the reason for the last failure in plain words, and when a retrying delivery is next attempted. Failed deliveries are retried with backoff for 24 hours; a refused destination fails at once.

Users & Team

GET/v1/usersAdmin

List all team members.

Response

json
{
  "users": [
    {
      "id": "usr_abc",
      "email": "alice@acme.com",
      "name": "Alice",
      "avatar_url": "https://...",
      "roles": ["admin"],
      "created_at": "2025-06-01T00:00:00Z",
      "last_login": "2025-07-24T08:00:00Z"
    }
  ]
}
POST/v1/users/{id}/rolesAdmin

Grant a role (viewer, author, approver or admin). Granting a role the user already holds is a 409; a role revoked earlier is granted again.

Request body

json
{ "role": "approver" }
DELETE/v1/users/{id}/roles/{role}Admin

Revoke a role from a user.

POST/v1/invitesAdmin or API key

Invite someone by email with a role. They join by the emailed link, or — with SSO set up — simply by signing in through your identity provider.

Request body

json
{ "email": "rae@acme.com", "role": "approver" }
DELETE/v1/team/{userId}Admin

Remove a member: their roles go and their sessions end at once, and they can't sign in again — by SSO or emailed link — until invited again (which restores the same account).

Single sign-on

Connect your organisation's OIDC identity provider. An email domain routes sign-ins only once you've proved you own it with a DNS TXT record — and only one organisation can verify a domain. Roles are given in Clausgap (by invite or by an admin), and a sign-in through your identity provider is the person's existing account, matched by email. See Connecting your identity provider.

PUT/v1/settings/ssoAdmin

Configure the identity provider. The issuer is checked straight away (OIDC discovery).

Request body

json
{ "issuer_url": "https://login.acme.com/", "client_id": "clausgap", "client_secret": "…" }
POST/v1/settings/sso/domainsAdmin

Claim an email domain. The response's domain_records give the TXT record to publish; another organisation having verified the domain is a 409.

Request body

json
{ "domain": "acme.com" }

Response

json
{
  "domain_records": [{
    "domain": "acme.com",
    "verified": false,
    "txt_name": "_clausgap-verification.acme.com",
    "txt_value": "clausgap-domain-verification=2d076bea…"
  }]
}
POST/v1/settings/sso/domains/{domain}/verifyAdmin

Check DNS for the TXT record and, if it's there, start routing the domain's sign-ins to your identity provider. 422 DOMAIN_NOT_VERIFIED says which record was missing.