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#
Authorization: Bearer <clerk-session-token>
Content-Type: application/jsonEvery 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.
{
"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
| Field | Required | Notes |
|---|---|---|
email | Yes | Owner email — who this key was issued to |
label | Yes | 1–100 characters |
role | No | USER or ADMIN. Defaults to USER |
agentId | No | 1–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
{
"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
| Field | Required | Notes |
|---|---|---|
senderAgentId | Yes | 1–200 chars. * matches any agent |
targetEndpoint | Yes | 1–500 chars. * matches any run of characters |
action | Yes | ALLOW, BLOCK or REQUIRE_APPROVAL |
enabled | No | Defaults to true |
dlpEnabled | No | Defaults 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:
{
"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
| Field | Required | Notes |
|---|---|---|
label | Yes | 1–120 chars. Shown in findings and approvals |
field | Yes | 1–200 chars. Dotted path — data.amount, data.items[*].price, or a bare name |
condition | Yes | One of the seven |
value | Depends | Threshold, [min, max], scalar, or keyword array. Omitted for matches_destructive |
action | Yes | block, hold_for_approval or flag_only |
senderAgentId | No | Defaults to * |
minSeverity | No | matches_destructive only. low / medium / high / critical |
enabled | No | Defaults to true |
403 when the enabled-rule limit is reached:
{
"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_APPROVAL → PENDING, the original payload is enqueued, and delivery proceeds with normal retries. Audit: message.approved, with the approver.
POST /v1/admin/messages/:id/reject
{ "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
| Parameter | Notes |
|---|---|
status | Filter to one of the nine TransferStatus values |
limit | 1–200, default 50 |
cursor | The id of the last row from the previous page |
{ "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
| Parameter | Notes |
|---|---|
type | One of the 23 event types, ≤60 chars |
limit | 1–200, default 50 |
cursor | Sequence 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.
{ "valid": true, "eventCount": 1428, "firstSeq": 1, "lastSeq": 1428,
"verifiedAt": "2026-08-24T09:20:11.402Z" }{ "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
| Parameter | Notes |
|---|---|
format | csv or json. Defaults to csv |
from | Start of range, inclusive |
to | End 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
| Parameter | Notes |
|---|---|
from / to | The 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:
{
"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
| Field | Required | Notes |
|---|---|---|
email | Yes | Billing email |
successUrl | Yes | Where to return after payment |
tier | No | pro 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
| Parameter | Notes |
|---|---|
q | Search, ≤200 chars |
tier | free, pro or max |
sort | recent or email |
PATCH /v1/admin/tenants/:id/tier
{ "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#
| Code | When |
|---|---|
400 | Malformed body — the message names the field |
401 | Missing, malformed or expired session token; or an API key was used |
403 | Tier limit reached (with code), or a capability is missing |
404 | No such resource, or code: "NO_ACCOUNT" |
409 | Conflict — an abuse signal already claimed by another operator |
500 | Server 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 group | Events |
|---|---|
| Keys | key.created, key.revoked |
| Policies | policy.created, policy.deleted |
| Guardrails | guardrail.created, guardrail.updated, guardrail.deleted |
| Approvals | message.approved, message.rejected |
| Threats | threat.released |
| Subscription / tenants | subscription.changed |
Reads — logs, stats, audit events, verification, exports — do not write events.