Arcus Docs

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 logAudit event
PurposeWhat is happening nowWhat happened, provably
MutableYes, by designNever
StructureA row per dispatchA hash-chained sequence
Read byThe Logs view, dashboardsCompliance 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#

json
{
  "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"
}
FieldNotes
idAlso the value returned as transferLogId, and the id used to approve or reject a hold
statusOne of the nine values below
sourceAgentThe sender, after identity binding resolved it
targetUrlWhere it was going
payloadSizeBytes. The size, never the content
attemptsDelivery attempts made — 1 to 3
httpStatusWhat your target returned on the last attempt
errorFailure reason, if any
policyIdThe winning policy, or null when nothing matched
dlpTriggeredWhether gate 4 fired
parameterTriggeredWhether gate 5 fired
decisionReasonHuman-readable: which rule decided, and why
createdAt / deliveredAtThe 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#

StatusMeaningTerminal?
PENDINGAccepted and queued, not yet deliveredNo
SUCCESSThe target returned 2xxYes
FAILEDDelivery failed after retries, or the queue was unavailableYes
BLOCKED_BY_POLICYNo policy allowed the route, or one blocked itYes
BLOCKED_BY_DLPCredential-shaped data in the payloadYes
BLOCKED_BY_PARAMETERA block guardrail matchedYes
PENDING_APPROVALHeld for a human; payload retainedNo
REJECTEDAn approver refused itYes
THREAT_DETECTEDThe sending agent was rate-limited or quarantinedYes

The transitions that actually occur:

text
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#

bash
curl -sS "$ARCUS_URL/v1/admin/logs?limit=50" \
  -H "Authorization: Bearer <clerk-session-token>"
ParameterNotes
statusFilter to one status
limit1–200, default 50
cursorThe id of the last row from the previous page
json
{
  "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.

bash
# 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>"
python
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 out

Requires 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.

StatusRead nextUsual cause
BLOCKED_BY_POLICYpolicyIdnull or a rule idnull means nothing matched: default-deny. A populated id means a deliberate BLOCK. Policies
BLOCKED_BY_DLPThe DLP categories and pathsA credential reached the payload. Fix upstream of the agent. DLP
BLOCKED_BY_PARAMETERfindings — field, expected, observedA value crossed a threshold. observed tells you whether the rule or the request is wrong. Guardrails
THREAT_DETECTEDsourceAgentThat agent exceeded 60 dispatches in 60 s. Something is looping. Threat detection
PENDING_APPROVALcreatedAtWaiting on a person. Holds do not expire. Approvals
FAILED with httpStatusYour target's responseYour endpoint returned non-2xx three times
FAILED with error and no httpStatusThe error textTimeout (10 s), DNS, TLS, or connection refused
FAILED with Queue unavailableRedis was unreachable at enqueue. The dispatch was recorded, not delivered
PENDING for a long timeWhether the worker is runningAccepted 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#

bash
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.

bash
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.