Introduction
Quickstart
Sign in, issue a key, allow one route, and send a governed dispatch — then watch Arcus refuse a request that tries to leak a credential. About five minutes end to end.
Before you start#
You need an Arcus account and somewhere for a dispatch to land. If you do not have a receiving endpoint yet, step 4 includes a nine-line one you can run locally.
Note
The examples below use https://your-arcus-host for the gateway. Replace it with the host your Arcus instance runs on. If you are running Arcus yourself, see Running Arcus.
1. Create your account#
Open the Arcus dashboard and sign in. Authentication is handled by Clerk, so email or a social provider both work.
Your account is your tenant. There is no separate workspace to create and no team to invite before you can do anything — keys, policies, logs and your audit chain all hang off your account directly. See Core concepts.
Every new account starts on the Free plan, which includes the full enforcement pipeline.
2. Issue an API key#
Go to API Keys and create one.
| Field | What it does |
|---|---|
| Label | For your own reference — intake-agent (staging). |
| Owner email | Who this key was issued to. Distinct from your account email, so a key handed to a contractor is attributable. |
| Agent ID (recommended) | Binds the key to one agent identity. A dispatch from this key must declare from equal to this value, or it is rejected as spoofing. |
The raw key is shown once. Arcus stores only its SHA-256 hash and a 15-character display prefix, so there is no "show key again" — copy it now or revoke and reissue.
ark_live_4f8c21a90b73e5d6c8a41f27b9e03d5a6c17f82be40d95c3
└──┬───┘ └─────────────────────────┬──────────────────────┘
prefix 48 hex characters (24 random bytes)Treat it like a production credential
This key authorizes dispatches on your account, under your policies and your audit chain. Put it in an environment variable or a secret manager — never in source control, and never in a file you might publish. Arcus's own DLP engine recognises the ark_live_ shape and will refuse a payload containing one.
Store it:
export ARCUS_KEY="ark_live_xxxxxxxx"
export ARCUS_URL="https://your-arcus-host"3. Allow one route#
Arcus is default-deny. A key with no matching policy can dispatch nowhere, which means a stolen key on a fresh account is worth nothing.
Go to Policies and create one:
| Field | Value |
|---|---|
| Sender agent | intake-agent |
| Target endpoint | http://localhost:4101/* |
| Action | ALLOW |
| DLP enabled | Leave on |
* is the only wildcard, and it matches any run of characters. http://localhost:4101/* allows every path on that host and port; * alone in the target field allows any endpoint, which is worth reaching for only in development.
Start narrow
A policy of intake-agent → https://api.example.com/webhook/triage is a better first policy than a wildcard, because widening it later is a deliberate, audited act. See Policy engine for how overlapping policies resolve.
4. Run a receiver#
Any HTTP server that accepts a POST and returns 2xx will do. This one has no dependencies:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
print(json.dumps(json.loads(self.rfile.read(length) or b'{}'), indent=2))
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(b'{"status":"received"}')
print('listening on :4101')
HTTPServer(('127.0.0.1', 4101), Handler).serve_forever()python3 receiver.py5. Send your first dispatch#
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
-H "Authorization: Bearer $ARCUS_KEY" \
-H "Content-Type: application/json" \
-d '{
"target": "http://localhost:4101/webhook/triage",
"from": "intake-agent",
"message": "Patient reports severe pain in the lower left molar.",
"data": { "priority": "urgent", "caseId": "C-4471" }
}'Arcus responds as soon as the decision is made — not when your target replies:
{
"ok": true,
"transferLogId": "clx8m2p4k0001abcd",
"queued": true
}Your receiver prints the payload a moment later. Open Transfer Logs in the dashboard and the dispatch is there with status SUCCESS, the target URL, the payload size, the attempt count and the delivery time.
6. Watch it refuse something#
The point of a gateway is what it stops. Send the same request with a credential in the payload — exactly the mistake an agent makes when a secret is sitting in its context:
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
-H "Authorization: Bearer $ARCUS_KEY" \
-H "Content-Type: application/json" \
-d '{
"target": "http://localhost:4101/webhook/triage",
"from": "intake-agent",
"message": "Here is the database password: hunter2-prod-primary",
"data": { "aws_access_key": "AKIAIOSFODNN7EXAMPLE" }
}'{
"error": "Payload blocked by DLP",
"status": "BLOCKED_BY_DLP",
"transferLogId": "clx8m2p4k0002abcd",
"violations": ["PASSWORD_FIELD", "AWS_ACCESS_KEY"],
"matchCount": 2,
"detailAvailableOn": "pro"
}422, nothing delivered, and your receiver never saw it. The violations array names the categories on every plan; the exact field paths that matched require the dlp.configure capability — see Data loss prevention.
Two more worth trying:
# Wrong sender for a bound key -> 401, recorded as a spoofing attempt
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
-H "Authorization: Bearer $ARCUS_KEY" -H "Content-Type: application/json" \
-d '{"target":"http://localhost:4101/x","from":"some-other-agent","message":"hi"}'
# Unallowed target -> 403, default-deny
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
-H "Authorization: Bearer $ARCUS_KEY" -H "Content-Type: application/json" \
-d '{"target":"https://evil.example.com/exfil","from":"intake-agent","message":"hi"}'7. Verify the record#
Open Audit Trail in the dashboard. Every decision above is there in sequence — allowed, blocked by DLP, spoof rejected, blocked by policy — each with its actor, its summary and its hash.
Click Verify chain. Arcus recomputes every hash from the first event forward and reports whether the chain is intact. If a row had been edited or deleted, this is where it would show, with the sequence number of the first break.
That is the whole loop: govern the request, record the decision, prove the record.
Point your real agent at it#
The change to an existing agent is usually one function. Instead of calling the target:
import os
import requests
ARCUS_URL = os.environ["ARCUS_URL"] + "/v1/dispatch"
ARCUS_KEY = os.environ["ARCUS_KEY"] # never hardcode this
def dispatch(target: str, message: str, data: dict | None = None):
response = requests.post(
ARCUS_URL,
headers={
"Authorization": f"Bearer {ARCUS_KEY}",
"Content-Type": "application/json",
},
json={
"target": target,
"from": "intake-agent",
"message": message,
"data": data or {},
},
timeout=15,
)
if response.status_code == 202:
return response.json()
# 401 identity, 403 policy, 422 DLP or guardrail, 429 quarantined.
# The body always names the reason.
raise RuntimeError(f"Arcus refused: {response.status_code} {response.text}")Full field and status-code reference: Dispatch API.
Next steps#
Add a guardrail
Hold any dispatch whose data.amount exceeds a threshold, or that carries a destructive command.
Require approval
Put a human in the loop on a route: the payload is held, not delivered, until someone releases it.
Understand the gates
Why the five checks run in that order, and exactly what each one refuses.
Check your limits
Free allows 2 active keys and 2 enabled guardrails. See what Pro and Max change.