Arcus Docs

API reference

Admin API

Every control-plane endpoint — keys, policies, guardrails, approvals, logs, audit, compliance, threats, billing and tenants — with its parameters, response shape and the audit event it writes.

Authentication#

text
Authorization: Bearer <clerk-session-token>
Content-Type: application/json

Every endpoint on this page requires a Clerk session token. An ark_live_ API key returns 401 — there is no privilege escalation path from the gateway to the control plane. See Authentication & keys.

Everything is scoped to your account. You cannot read or modify another tenant's resources through any endpoint here.

Never give an agent a control-plane credential

An agent holding a session token can rewrite the policies that govern it, disable its guardrails and revoke the keys that constrain it. Call the admin API from your backend or the dashboard, never from the agent process.


Keys#

GET /v1/admin/keys

Lists your keys — metadata only. Never a hash, never a raw key.

json
{
  "keys": [
    {
      "id": "clx8m2p4k0001abcd",
      "prefix": "ark_live_4f8c21",
      "label": "intake-agent (production)",
      "email": "ops@yourcompany.com",
      "agentId": "intake-agent",
      "role": "USER",
      "createdAt": "2026-08-01T10:22:11.004Z",
      "lastUsedAt": "2026-08-24T09:14:02.881Z",
      "revokedAt": null
    }
  ],
  "limit": { "tier": "free", "max": 2, "active": 1, "canCreate": true }
}

lastUsedAt is written best-effort so a slow write cannot add latency to the enforcement path. Treat it as "recently active", not a precise access log — for that, read the audit chain, where every dispatch event carries the key prefix as actor.

POST /v1/admin/keys

FieldRequiredNotes
emailYesOwner email — who this key was issued to
labelYes1–100 characters
roleNoUSER or ADMIN. Defaults to USER
agentIdNo1–200 characters. Binds the key to one agent identity

Returns rawKey once. Arcus stores only the SHA-256 hash and a display prefix.

403 when the active-key limit for your tier is reached (2 / 10 / 50). Audit: key.created.

DELETE /v1/admin/keys/:id

Revokes immediately — the next dispatch with that key returns 401. The row is kept with revokedAt set, so historical logs still resolve to a recognisable prefix. Revoked keys do not consume a limit slot.

Audit: key.revoked, carrying actor, prefix, label and owner email.


Policies#

GET /v1/admin/policies

json
{
  "policies": [
    {
      "id": "clx7k1n3j0001wxyz",
      "senderAgentId": "intake-agent",
      "targetEndpoint": "https://api.internal.example.com/webhook/*",
      "action": "ALLOW",
      "enabled": true,
      "dlpEnabled": true,
      "createdAt": "2026-08-01T10:24:03.117Z"
    }
  ]
}

POST /v1/admin/policies

FieldRequiredNotes
senderAgentIdYes1–200 chars. * matches any agent
targetEndpointYes1–500 chars. * matches any run of characters
actionYesALLOW, BLOCK or REQUIRE_APPROVAL
enabledNoDefaults to true
dlpEnabledNoDefaults to true. Taken from the winning policy at dispatch time

Requires the policy.custom capability (Pro+). Audit: policy.created.

DELETE /v1/admin/policies/:id

Audit: policy.deleted. Removing the last ALLOW for a route returns that route to default-deny immediately. See Policy engine.


Parameter guardrails#

GET /v1/admin/parameter-rules

Returns your rules and the full vocabulary, so a client never hardcodes the condition list or the destructive catalogue:

json
{
  "rules": [ /* … */ ],
  "limit":    { "tier": "free", "max": 2, "enabled": 2, "canCreate": false },
  "baseline": { "enabled": false, "requiredTier": "pro",
                "window": 50, "minSamples": 5, "multiple": 5, "sigma": 4,
                "maxTrackedFields": 10 },
  "vocabulary": {
    "conditions": ["greater_than", "less_than", "outside_range", "equals",
                   "not_equals", "contains_keyword", "matches_destructive"],
    "actions": ["block", "hold_for_approval", "flag_only"],
    "suggestedKeywords": ["delete", "drop", "wipe", "purge", "transfer_all",
                          "rm -rf", "truncate", "shutdown", "revoke", "disable_audit"],
    "destructivePatterns": [
      { "id": "sql.drop_object", "label": "DROP TABLE / DATABASE / SCHEMA", "severity": "critical" }
    ]
  }
}

destructivePatterns carries ids, labels and severities — not the regexes. Publishing those would document the bypass.

POST /v1/admin/parameter-rules

FieldRequiredNotes
labelYes1–120 chars. Shown in findings and approvals
fieldYes1–200 chars. Dotted path — data.amount, data.items[*].price, or a bare name
conditionYesOne of the seven
valueDependsThreshold, [min, max], scalar, or keyword array. Omitted for matches_destructive
actionYesblock, hold_for_approval or flag_only
senderAgentIdNoDefaults to *
minSeverityNomatches_destructive only. low / medium / high / critical
enabledNoDefaults to true

403 when the enabled-rule limit is reached:

json
{
  "error": "Free accounts can keep 2 parameter rules enabled",
  "code": "PARAMETER_RULE_LIMIT_REACHED",
  "tier": "free", "limit": 2, "activeRules": 2,
  "upgradeAvailable": true, "requiredTier": "pro"
}

404 with code: "NO_ACCOUNT" if no tenant record resolves. Audit: guardrail.created.

PATCH /v1/admin/parameter-rules/:id

Partial update; same validation. Setting enabled: false frees a limit slot and keeps the definition — the recommended way to maintain a rule library on Free. Audit: guardrail.updated.

DELETE /v1/admin/parameter-rules/:id

Audit: guardrail.deleted.

GET /v1/admin/baselines

Learned statistics per (agent, numeric field) — sample count, rolling mean, standard deviation, last observation. Requires guardrails.baseline (Pro+). See Statistical baselines.


Approvals#

GET /v1/admin/messages/pending

Every dispatch currently held, with the full retained payload, the reason it was held, and any guardrail findings. Approving a request you cannot see is not review, so the body is included.

POST /v1/admin/messages/:id/approve

:id is the transfer log id. The status moves PENDING_APPROVALPENDING, the original payload is enqueued, and delivery proceeds with normal retries. Audit: message.approved, with the approver.

POST /v1/admin/messages/:id/reject

json
{ "reason": "Amount unverified — no matching invoice in the ledger" }

reason is optional, ≤500 characters. Write one — it is the highest-value free-text field in the system. Status becomes REJECTED; nothing is delivered. Audit: message.rejected.

See Approval workflow.


Logs and statistics#

GET /v1/admin/logs

ParameterNotes
statusFilter to one of the nine TransferStatus values
limit1–200, default 50
cursorThe id of the last row from the previous page
json
{ "logs": [ /* newest first */ ], "nextCursor": "clx8m2p4k0032abcd" }

Cursor pagination — stable while new rows arrive. See Transfer logs.

GET /v1/admin/stats

Aggregate counts: dispatches by status, blocks per gate, active keys, policies, enabled guardrails.

GET /v1/admin/analytics/overview

Throughput over time, block distribution across the five gates, per-agent activity.


Audit#

GET /v1/admin/audit/events

ParameterNotes
typeOne of the 23 event types, ≤60 chars
limit1–200, default 50
cursorSequence number to page from

Each event carries seq, type, actor, summary, metadata, prevHash, hash, createdAt. Filtering is server-side.

On Free: the most recent 100 events within the last 7 days. Every event is written regardless of tier — Free limits reading, not recording.

GET /v1/admin/audit/verify

Recomputes every hash from sequence 1 and compares against both the stored hash and the next event's prevHash.

json
{ "valid": true, "eventCount": 1428, "firstSeq": 1, "lastSeq": 1428,
  "verifiedAt": "2026-08-24T09:20:11.402Z" }
json
{ "valid": false, "eventCount": 1428, "brokenAtSeq": 872,
  "reason": "hash mismatch: recomputed hash does not match stored hash" }

Requires audit.verify (Pro+). See Audit chain.


Compliance#

GET /v1/admin/compliance/export

ParameterNotes
formatcsv or json. Defaults to csv
fromStart of range, inclusive
toEnd of range, inclusive

JSON preserves hash and prevHash, so an export can be re-verified independently of Arcus. Archive these where your database credentials do not reach.

GET /v1/admin/compliance/report

ParameterNotes
from / toThe reporting period

A summarised period report — volume, outcome distribution, approval activity, credential lifecycle, configuration changes. Requires reports (Max). See Compliance reporting.


Threats#

GET /v1/admin/threats

Every currently-quarantined agent in your account with its live remaining TTL, read from Redis rather than computed from a stored timestamp.

GET /v1/admin/threats/:agent

Per-agent detail: quarantine state, remaining seconds, recent enforcement history.

POST /v1/admin/threats/:agent/release

Clears the quarantine immediately. Audit: threat.released, with the actor — the release is the audited decision, because tripping a rate limit is an accident and deciding to let the agent continue is a judgement.

Fix the cause first: releasing an agent still in a retry loop restarts the flood. See Threat detection.


Subscription#

GET /v1/admin/subscription

Your tier, status, limits and the capability set:

json
{
  "tier": "pro",
  "status": "active",
  "limits": { "keys": 10, "parameterRules": null },
  "capabilities": ["identity.advanced", "audit.full", "audit.verify",
                   "policy.custom", "dlp.configure", "guardrails.baseline"]
}

Entitlement holds while the status is active, trialing or past_due — a payment problem does not immediately strip your governance configuration.

POST /v1/admin/subscription/checkout

FieldRequiredNotes
emailYesBilling email
successUrlYesWhere to return after payment
tierNopro or max. Defaults to pro

Returns a hosted checkout URL. Requires the billing provider to be configured server-side; otherwise the response says so rather than inventing a link.

POST /v1/admin/subscription/manage

Returns a customer-portal URL for changing payment details or cancelling.

POST /v1/admin/subscription/sync

Re-reads your subscription state from the billing provider. Useful if a webhook was missed. Audit: subscription.changed when the tier actually moves.


Tenants#

GET /v1/admin/tenants

ParameterNotes
qSearch, ≤200 chars
tierfree, pro or max
sortrecent or email

PATCH /v1/admin/tenants/:id/tier

json
{ "tier": "pro", "reason": "Migrated from an annual invoice — provisioned manually" }

An administrative tier override, independent of the billing provider — for a trial, a manual invoice or a support remedy. reason is ≤500 characters. Audit: subscription.changed on the affected tenant's chain, so a tier change always appears in the record of the account it affected.


Error shapes#

CodeWhen
400Malformed body — the message names the field
401Missing, malformed or expired session token; or an API key was used
403Tier limit reached (with code), or a capability is missing
404No such resource, or code: "NO_ACCOUNT"
409Conflict — an abuse signal already claimed by another operator
500Server error

Tier refusals are actionable rather than opaque: they carry your tier, the limit, the current count, upgradeAvailable and requiredTier.

What writes to the audit chain#

Every mutation here appends a sealed event. A change to your enforcement configuration is itself a governance event, in the same chain as the traffic it affects — so "the policy was different at the time" is verifiable rather than asserted.

Endpoint groupEvents
Keyskey.created, key.revoked
Policiespolicy.created, policy.deleted
Guardrailsguardrail.created, guardrail.updated, guardrail.deleted
Approvalsmessage.approved, message.rejected
Threatsthreat.released
Subscription / tenantssubscription.changed

Reads — logs, stats, audit events, verification, exports — do not write events.