Arcus Docs

API reference

API overview

Two surfaces, two credentials, one convention. Base URLs, authentication, status codes, error shapes, pagination and rate limits — everything common to every Arcus endpoint.

Base URL#

text
https://your-arcus-host/v1

All endpoints are under /v1. The one exception is the health probe:

bash
curl -sS https://your-arcus-host/health
json
{ "status": "ok", "timestamp": "2026-08-24T09:14:02.881Z" }

Unauthenticated, intended for load balancers and uptime monitors. It reports that the process is serving requests — not that PostgreSQL and Redis are healthy. For that, a platform operator has /v1/admin/platform/health.

Two surfaces#

GatewayControl plane
Endpoint/v1/dispatch/v1/admin/*
CallerYour agentsYou, or your backend
CredentialArcus API keyClerk session token
HeaderAuthorization: Bearer ark_live_…Authorization: Bearer <clerk-session-token>

There is exactly one gateway endpoint. An API key authenticates nothing else — it cannot read logs, list keys, change a policy, disable a guardrail or revoke itself. See Authentication & keys.

Do not put a Clerk session token in an agent

An agent holding a control-plane credential can rewrite the policies that govern it. The whole architecture rests on the agent holding only a dispatch credential.

Content type#

Every request with a body is JSON:

text
Content-Type: application/json

Responses are always JSON, including errors. There is no form encoding, no XML, and no multipart/form-data on any endpoint.

Status codes#

CodeMeaning
200Read succeeded
201Resource created
202Dispatch accepted — read queued and status
400Malformed request body; the response names the field
401Missing, malformed, unknown or revoked credential; or an identity mismatch
403Authenticated but not permitted — policy denial, tier limit, or insufficient role
404No such resource, or no account
409Conflict — an abuse signal already claimed by another operator
422Semantically rejected — DLP or a guardrail refused the payload
429Rate-limited or quarantined; Retry-After is set
500Server error

The distinction between 403 and 422 is deliberate: 403 means you may not reach this destination, 422 means this content may not be sent. Same refusal from the agent's point of view, very different fix.

`202` is not a delivery confirmation

Three outcomes return 202: accepted and queued (queued: true), held for approval (queued: false, status: "PENDING_APPROVAL"), and accepted-but-unqueueable when Redis is down. Branch on queued and status, never on the HTTP code alone.

Error shape#

Every error carries error as a readable sentence. Enforcement refusals add status; tier limits add code.

json
{ "error": "Payload blocked by DLP", "status": "BLOCKED_BY_DLP", "transferLogId": "clx…" }
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"
}

Validation errors name the field and the expectation:

json
{ "error": "greater_than needs a numeric threshold, e.g. { \"value\": 1000 }" }

status values map 1:1 to TransferStatusBLOCKED_BY_POLICY, BLOCKED_BY_DLP, BLOCKED_BY_PARAMETER, THREAT_DETECTED, PENDING_APPROVAL. Branch on those rather than on error strings, which are written for humans and may be reworded.

Where a refusal produced a record, transferLogId is present — so an agent can reference the decision without needing control-plane access.

Pagination#

List endpoints use cursor pagination:

bash
curl -sS "$ARCUS_URL/v1/admin/logs?limit=100" -H "$AUTH"
json
{ "logs": [ /* newest first */ ], "nextCursor": "clx8m2p4k0032abcd" }

Pass nextCursor as cursor for the following page; null means the end. Cursors are stable while new rows arrive and do not get more expensive as you page back — unlike an offset.

limit accepts 1–200 and defaults to 50 on every paginated endpoint. Audit events page by sequence number rather than id; everything else pages by id.

Rate limits#

The only rate limit Arcus enforces is the per-agent dispatch limit: 60 dispatches per 60 seconds per (tenant, agent), breach quarantines the agent for 300 seconds. 429 with Retry-After. See Threat detection.

Control-plane endpoints are not rate-limited. They are behind a browser session, and the expensive reads are memoised server-side.

Monthly request figures are advisory

Plan pages list indicative monthly volumes per tier (10,000 / 250,000 / 2,000,000). These are not enforced — no endpoint rejects a request for exceeding a monthly count, and nothing meters you toward a cutoff. The only hard numeric limits are active keys and enabled guardrail rules. See Pricing & plans.

Tier limits and capabilities#

Two mechanisms.

Numeric limits — active keys (2 / 10 / 50) and enabled guardrail rules (2 / unlimited / unlimited). Exceeding one returns 403 with a code, your tier, the limit, the current count and requiredTier.

Capabilities — named permissions like audit.verify, policy.custom, dlp.configure, reports. A missing capability returns 403 naming what would grant it, or degrades the response: DLP refusals on Free include detailAvailableOn: "pro" rather than failing.

Enforcement never depends on tier. All five gates run on every request on every plan.

CORS#

The dashboard origin is the only browser origin permitted, configured server-side. There is no wildcard CORS policy, which means you cannot call the Arcus API from arbitrary browser JavaScript — deliberately. An ark_live_ key in a browser is a published key. Dispatch from your server.

Idempotency#

Arcus does not implement idempotency keys. A repeated dispatch is a second dispatch: a second transfer log row, a second audit event, and a second delivery.

Two consequences to design around:

  • Make your receiving endpoint idempotent. The delivery worker retries up to 3 times on non-2xx, so your endpoint must tolerate seeing the same body more than once. Put a deduplication key in data and check it on arrival.
  • Do not retry a refusal. 401, 403 and 422 are decisions, not transient failures. Retrying produces more refusals and more audit events. Only 429 (after Retry-After) and 5xx are worth retrying.

Timeouts#

BoundaryValue
Enqueue after a decision2 s — exceeded means the dispatch is recorded FAILED
Delivery attempt to your target10 s per attempt
Delivery attempts3, exponential backoff from 1 s

Set a client timeout of 15 seconds or so on your dispatch call. The synchronous portion is a Redis read, an indexed Postgres read, an in-process payload walk and one transaction — it does not wait for your target.

Versioning#

The /v1 prefix is the version. Additive changes — new fields, new event types, new endpoints — happen within v1. Removing a field or changing the meaning of a status code would require /v2.

Write clients that ignore unknown fields. New keys will appear in responses.

A minimal client#

python
import os
import requests

class Arcus:
    """Dispatch client. Holds only an agent credential — no control-plane access."""

    def __init__(self, agent: str):
        self.agent = agent
        self.url = os.environ["ARCUS_URL"].rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {os.environ['ARCUS_KEY']}",
            "Content-Type": "application/json",
        })

    def dispatch(self, target: str, message: str, data: dict | None = None, to: str | None = None):
        body = {"target": target, "from": self.agent, "message": message, "data": data or {}}
        if to:
            body["to"] = to

        r = self.session.post(f"{self.url}/v1/dispatch", json=body, timeout=15)
        payload = r.json()

        if r.status_code == 202:
            if payload.get("status") == "PENDING_APPROVAL":
                return {"held": True, "id": payload["transferLogId"]}
            return {"held": False, "id": payload["transferLogId"],
                    "flagged": payload.get("flagged", False)}

        if r.status_code == 429:                       # quarantined — honour Retry-After
            raise RuntimeError(f"quarantined for {r.headers.get('Retry-After', '?')}s")

        # 401 identity, 403 policy, 422 DLP or guardrail. All are decisions, not retryable.
        raise RuntimeError(f"refused {r.status_code}: {payload.get('error')} "
                           f"({payload.get('status', 'n/a')})")

Endpoint index#

Gatewayfull reference

/v1/dispatchSend a governed request

Control planefull reference

AreaEndpoints
Keys/v1/admin/keys /v1/admin/keys /v1/admin/keys/:id
Policies/v1/admin/policies /v1/admin/policies /v1/admin/policies/:id
Guardrails/v1/admin/parameter-rules /v1/admin/parameter-rules /v1/admin/parameter-rules/:id /v1/admin/parameter-rules/:id /v1/admin/baselines
Approvals/v1/admin/messages/pending /v1/admin/messages/:id/approve /v1/admin/messages/:id/reject
Logs/v1/admin/logs /v1/admin/stats /v1/admin/analytics/overview
Audit/v1/admin/audit/events /v1/admin/audit/verify
Compliance/v1/admin/compliance/export /v1/admin/compliance/report
Threats/v1/admin/threats /v1/admin/threats/:agent /v1/admin/threats/:agent/release
Billing/v1/admin/subscription /v1/admin/subscription/checkout /v1/admin/subscription/manage /v1/admin/subscription/sync
Tenants/v1/admin/tenants /v1/admin/tenants/:id/tier

Platform operatorfull reference. Cross-tenant, restricted to an Clerk-side operator role or a server-only email allow-list.