Evidence
Transfer logs
The operational record of every dispatch — status, target, attempts, HTTP result, decision reason. Mutable by design, cursor-paginated, and the first place to look when something did not arrive.
The operational view#
A transfer log row is one dispatch as it actually happened. Unlike an audit event, it changes as the delivery progresses — that is precisely what makes it useful for operations and unsuitable as evidence.
| Transfer log | Audit event | |
|---|---|---|
| Purpose | What is happening now | What happened, provably |
| Mutable | Yes, by design | Never |
| Structure | A row per dispatch | A hash-chained sequence |
| Read by | The Logs view, dashboards | Compliance export, verification |
Both are written in the same transaction for every decision, so they never disagree about whether something happened — only about how much of it can change afterwards. See Audit chain.
What a row contains#
{
"id": "clx8m2p4k0001abcd",
"status": "SUCCESS",
"sourceAgent": "intake-agent",
"targetUrl": "https://api.internal.example.com/webhook/triage",
"payloadSize": 412,
"attempts": 1,
"httpStatus": 200,
"error": null,
"policyId": "clx7k1n3j0001wxyz",
"dlpTriggered": false,
"parameterTriggered": false,
"decisionReason": "Allowed by policy intake-agent -> https://api.internal.example.com/webhook/*",
"createdAt": "2026-08-24T09:14:02.881Z",
"deliveredAt": "2026-08-24T09:14:03.106Z"
}| Field | Notes |
|---|---|
id | Also the value returned as transferLogId, and the id used to approve or reject a hold |
status | One of the nine values below |
sourceAgent | The sender, after identity binding resolved it |
targetUrl | Where it was going |
payloadSize | Bytes. The size, never the content |
attempts | Delivery attempts made — 1 to 3 |
httpStatus | What your target returned on the last attempt |
error | Failure reason, if any |
policyId | The winning policy, or null when nothing matched |
dlpTriggered | Whether gate 4 fired |
parameterTriggered | Whether gate 5 fired |
decisionReason | Human-readable: which rule decided, and why |
createdAt / deliveredAt | The two ends of the latency measurement |
Payload bodies are not stored on completed dispatches
payloadSize is a byte count. The only case where the full payload is retained is a dispatch held for approval — because approving a request you cannot see is not review. Once approved or rejected, the retention has served its purpose.
The nine statuses#
| Status | Meaning | Terminal? |
|---|---|---|
PENDING | Accepted and queued, not yet delivered | No |
SUCCESS | The target returned 2xx | Yes |
FAILED | Delivery failed after retries, or the queue was unavailable | Yes |
BLOCKED_BY_POLICY | No policy allowed the route, or one blocked it | Yes |
BLOCKED_BY_DLP | Credential-shaped data in the payload | Yes |
BLOCKED_BY_PARAMETER | A block guardrail matched | Yes |
PENDING_APPROVAL | Held for a human; payload retained | No |
REJECTED | An approver refused it | Yes |
THREAT_DETECTED | The sending agent was rate-limited or quarantined | Yes |
The transitions that actually occur:
PENDING → SUCCESS | FAILED
PENDING_APPROVAL → PENDING → SUCCESS | FAILED (approved)
PENDING_APPROVAL → REJECTED (rejected)Everything else is written once and never moves. A BLOCKED_BY_DLP row does not later become SUCCESS; the agent has to send a new, clean dispatch, which produces a new row.
Reading logs#
curl -sS "$ARCUS_URL/v1/admin/logs?limit=50" \
-H "Authorization: Bearer <clerk-session-token>"| Parameter | Notes |
|---|---|
status | Filter to one status |
limit | 1–200, default 50 |
cursor | The id of the last row from the previous page |
{
"logs": [ /* … newest first … */ ],
"nextCursor": "clx8m2p4k0032abcd"
}Cursor pagination, not offset — a stable window even while new dispatches arrive, and no deepening cost as you page back. Keep requesting with the returned nextCursor until it comes back null.
# Everything DLP refused, most recent first
curl -sS "$ARCUS_URL/v1/admin/logs?status=BLOCKED_BY_DLP&limit=100" \
-H "Authorization: Bearer <clerk-session-token>"def all_logs(session, arcus_url, status=None, page=200):
"""Walk the full log history, one page at a time."""
cursor, out = None, []
while True:
params = {"limit": page}
if status:
params["status"] = status
if cursor:
params["cursor"] = cursor
body = session.get(f"{arcus_url}/v1/admin/logs", params=params, timeout=20).json()
out.extend(body["logs"])
cursor = body.get("nextCursor")
if not cursor:
return outRequires a Clerk session token. An ark_live_ key cannot read logs — see credential separation.
The dashboard view#
Transfer Logs shows the same data with status filtering, the decision reason inline, and per-row expansion for the guardrail findings and DLP categories. It is the fastest way to answer "did that dispatch land, and if not, which gate stopped it".
Diagnosing with logs#
The status tells you which gate decided; decisionReason tells you which rule.
| Status | Read next | Usual cause |
|---|---|---|
BLOCKED_BY_POLICY | policyId — null or a rule id | null means nothing matched: default-deny. A populated id means a deliberate BLOCK. Policies |
BLOCKED_BY_DLP | The DLP categories and paths | A credential reached the payload. Fix upstream of the agent. DLP |
BLOCKED_BY_PARAMETER | findings — field, expected, observed | A value crossed a threshold. observed tells you whether the rule or the request is wrong. Guardrails |
THREAT_DETECTED | sourceAgent | That agent exceeded 60 dispatches in 60 s. Something is looping. Threat detection |
PENDING_APPROVAL | createdAt | Waiting on a person. Holds do not expire. Approvals |
FAILED with httpStatus | Your target's response | Your endpoint returned non-2xx three times |
FAILED with error and no httpStatus | The error text | Timeout (10 s), DNS, TLS, or connection refused |
FAILED with Queue unavailable | — | Redis was unreachable at enqueue. The dispatch was recorded, not delivered |
PENDING for a long time | Whether the worker is running | Accepted and queued, but nothing is consuming the queue |
`PENDING` that never moves means the worker, not the gateway
The gateway accepted and enqueued the dispatch — that part worked. If rows stay PENDING, the delivery worker is not running or cannot reach Redis. Nothing is lost; they deliver when it returns. See Troubleshooting.
Reading delivery latency#
deliveredAt - createdAt is the end-to-end time: enforcement, queue wait, and however long your target took, including retry backoff. A row with attempts: 3 includes roughly 3 seconds of exponential backoff, so a large value there is your endpoint's behaviour rather than the gateway's.
The synchronous portion your agent experiences is a different number, and much smaller — see the latency budget.
Statistics#
curl -sS "$ARCUS_URL/v1/admin/stats" \
-H "Authorization: Bearer <clerk-session-token>"Aggregate counts for your account: dispatches by status, block counts per gate, active keys, policies and enabled guardrails. This is what the dashboard overview renders.
curl -sS "$ARCUS_URL/v1/admin/analytics/overview" \
-H "Authorization: Bearer <clerk-session-token>"Richer: throughput over time, block distribution across the gates, and per-agent activity — the view that answers "which agent is generating all the refusals".
Retention#
Transfer logs are retained for your account without a hard cap and are readable in full on every plan. Audit event reading is what Free limits (100 rows / 7 days) — see retention on Free.
If you need history outside Arcus — for a SIEM, a warehouse, or an archive your database credentials do not reach — use /v1/admin/compliance/export, which emits CSV or JSON over a date range. See Compliance reporting.
A log is not evidence
Transfer logs are mutable and deletable, on purpose. If what you need is a record that can be shown to survive scrutiny, read the audit chain instead — same events, hash-sealed, with verification. The two exist separately for exactly this reason.