Skip to content
ArcFlow Sentinel

API

Everything the site does is available over HTTP: trace a transfer, follow its status, read the safety checks, and download evidence you can verify yourself. JSON in, JSON out.

For tools that read OpenAPI: machine-readable OpenAPI 3.0.

Quick start

Set these once in your shell; every example on this page uses them.

export BASE="https://arcflow-sentinel.icu"
export API_KEY="<one of this deployment's API keys; only needed where marked>"

Trace a real 41,300 USDC transfer from Arc to Base, then read it back:

TRACE_ID=$(curl -s -X POST "$BASE/api/traces" \
  -H "content-type: application/json" \
  -d '{"sourceTxHash":"0xf2b45ee11be4e44ba04a9b5cb0aee99c1cb7a0b4bca42814bd146feb198c5432","sourceChain":"arc"}' | jq -r '.traces[0].id')

curl "$BASE/api/traces/$TRACE_ID"

Within a few seconds the status reads MINT_CONFIRMED. The same trace is on the website at /traces/$TRACE_ID.

Basics

Who can call what

Reading traces, checks and evidence needs no key. Actions that change something on your behalf (confirming a trace, anchoring evidence, sending test webhooks, or creating a trace that waits for your confirmation) need an API key from the operator, sent as Authorization: Bearer <key>.

Limits

Creating new traces without a key is rate limited, because each one costs blockchain reads. When you hit the limit you get 429 with a Retry-After header in seconds. Repeating a request for a transaction that is already traced is never limited.

Amounts and times

USDC amounts are strings of whole units at 6 decimals: "5869596" is 5.869596 USDC. They are strings so no client loses precision. Times are ISO 8601 in UTC.

Errors

Errors use one shape (RFC 7807, application/problem+json). Validation errors list each field; unexpected errors only return an id you can give the operator.

{
  "type": "about:blank",
  "title": "Validation failed",
  "status": 400,
  "errors": [{ "path": "sourceChain", "message": "Invalid option: expected one of \"base\"|\"arc\"" }]
}

Transfer status

A trace's status is one of these, in order. Status only moves forward as evidence arrives; progress always holds the furthest step reached, even while the status is flagged.

SOURCE_OBSERVEDBurned on the website
The burn is on the source chain; Circle has not picked it up yet.
FINALITY_WAITFinalizing on the website
Circle is waiting for the source chain to finalize the burn.
ATTESTATION_PENDINGAwaiting signature on the website
The burn is final; Circle has not signed the message yet.
ATTESTEDSigned on the website
Circle signed the message; anyone can now submit it on the destination chain.
DESTINATION_SUBMITTEDReceived on the website
The message arrived on the destination chain; the mint is not visible yet.
MINT_CONFIRMEDMinted on the website
USDC was minted to the recipient. Final, unless your app must confirm.
APPLICATION_CONFIRMEDConfirmed on the website
Your app confirmed it credited the funds.
FAILED_OR_STUCKFailed or stuck on the website
reason "STUCK": a step took longer than its limit (it recovers if the transfer continues). reason "FAILED": a safety check failed; final.

Traces

Start tracing a transfer and read what Sentinel knows about it.

Trace a transfer

POST/api/traces

Give Sentinel the transaction that burned USDC on Base or Arc. It reads the burn from the chain and starts following it to the mint. Safe to repeat: the same transaction always returns the same trace.

Request body (JSON)
FieldTypeNotes
sourceTxHashstringRequired. Hash of the transaction that burned USDC (DepositForBurn).
sourceChain"base" | "arc"Required. Chain the burn happened on.
requireAppConfirmationbooleanOptional. Needs an API key. The trace then waits at Minted until your app calls /confirm.

Responses

200
This transaction was already traced; the existing trace is returned.
201
New trace created; Sentinel is now following it.
400
The request is malformed; the response lists the fields
401
requireAppConfirmation was set without a valid API key.
415
Send Content-Type: application/json
422
The transaction has no CCTP V2 burn on this route (Base ↔ Arc).
500
Unexpected error; the response contains an error id for the operator
Example request
curl -X POST "$BASE/api/traces" \
  -H "content-type: application/json" \
  -d '{"sourceTxHash":"0xf2b45ee11be4e44ba04a9b5cb0aee99c1cb7a0b4bca42814bd146feb198c5432","sourceChain":"arc"}'

Find a trace by transaction

GET/api/traces

Look up the trace for a burn transaction you already submitted. Never touches the chains.

Parameters
FieldTypeNotes
sourceTxHashstringRequired. In the query string.
sourceChain"base" | "arc"Required. In the query string.

Responses

200
OK
400
The request is malformed; the response lists the fields
404
No such trace
Example request
curl "$BASE/api/traces?sourceTxHash=0xf2b45ee11be4e44ba04a9b5cb0aee99c1cb7a0b4bca42814bd146feb198c5432&sourceChain=arc"

Get a trace

GET/api/traces/{id}

Status, progress, both transactions, the nonce, the amount and where the transfer stands against its deadlines.

Parameters
FieldTypeNotes
iduuidRequired. In the path.

Responses

200
OK
404
No such trace
Example request
curl "$BASE/api/traces/$TRACE_ID"

List observations

GET/api/traces/{id}/events

Every on-chain log and attestation response behind the verdict, including logs later dropped by a chain reorganization (marked removed).

Parameters
FieldTypeNotes
iduuidRequired. In the path.

Responses

200
OK
404
No such trace
Example request
curl "$BASE/api/traces/$TRACE_ID/events"

List checks

GET/api/traces/{id}/invariants

The five safety checks (delivered once, amount preserved, recipient unchanged, correct route, on schedule) with their result and explanation.

Parameters
FieldTypeNotes
iduuidRequired. In the path.

Responses

200
OK
404
No such trace
Example request
curl "$BASE/api/traces/$TRACE_ID/invariants"

Evidence and anchoring

Download signed evidence, anchor its hash on Arc, and fetch the key to verify it.

Download evidence

GET/api/traces/{id}/evidence

A signed, self-contained record of the trace. The same evidence always produces the same file and hash. Add ?download=1 to receive it as a file.

Parameters
FieldTypeNotes
iduuidRequired. In the path.
download"1"Optional. In the query string.

Responses

200
OK
404
No such trace
503
Unavailable or degraded
Example request
curl "$BASE/api/traces/$TRACE_ID/evidence"

Anchor evidence on Arc

POST/api/traces/{id}/anchorAPI key required

Writes the evidence hash to the EvidenceRegistry contract on Arc, so anyone can later prove the evidence existed at that time. Queued; the trace page shows the anchor within seconds.

Parameters
FieldTypeNotes
iduuidRequired. In the path.

Responses

202
Accepted, processing in the background
401
API key missing or wrong
404
No such trace
409
Anchoring is turned off or not configured on this deployment.
503
Unavailable or degraded
Example request
curl -X POST "$BASE/api/traces/$TRACE_ID/anchor" \
  -H "authorization: Bearer $API_KEY"

Get the signing key

GET/.well-known/sentinel-key.json

The public key that signs this site's evidence, plus where anchors live (network, chain id, registry, submitter). Verifiers use this, not the key inside a file.

Responses

200
OK
503
Unavailable or degraded
Example request
curl "$BASE/.well-known/sentinel-key.json"

Your application

Tell Sentinel when your app has credited the funds, for traces that wait for you.

Confirm you credited the funds

POST/api/traces/{id}/confirmAPI key required

For traces created with requireAppConfirmation: call this once your app has credited the user. The trace moves from Minted to Confirmed. Idempotent; the first reference is kept.

Parameters
FieldTypeNotes
iduuidRequired. In the path.
Request body (JSON)
FieldTypeNotes
referencestringOptional. Your own id for the credit (order, ledger entry). Up to 200 characters.

Responses

200
OK
400
The request is malformed; the response lists the fields
401
API key missing or wrong
404
No such trace
409
This trace was not created with requireAppConfirmation.
Example request
curl -X POST "$BASE/api/traces/$TRACE_ID/confirm" \
  -H "authorization: Bearer $API_KEY" \
  -H "content-type: application/json" \
  -d '{"reference":"order-42"}'

Webhooks

Check that your webhook endpoint receives and verifies events.

Send a test event

POST/api/webhooks/testAPI key required

Sends a signed webhook.test event to every configured endpoint, so you can check your receiver.

Responses

202
Accepted, processing in the background
401
API key missing or wrong
409
No webhook endpoints are configured (WEBHOOK_URLS).
Example request
curl -X POST "$BASE/api/webhooks/test" \
  -H "authorization: Bearer $API_KEY"

Health

For load balancers, uptime checks and on-call.

Liveness

GET/api/health/live

The web process is up. Use it for container restarts.

Responses

200
OK
Example request
curl "$BASE/api/health/live"

Readiness

GET/api/health/ready

The database answers. Use it to decide whether to send traffic here.

Responses

200
OK
503
Unavailable or degraded
Example request
curl "$BASE/api/health/ready"

Route health

GET/api/health/routes

Whether Arc, Base and Circle's attestation service are reachable right now. Cached for 15 seconds.

Responses

200
OK
503
Unavailable or degraded
Example request
curl "$BASE/api/health/routes"

Receiving webhooks

If the operator registered your URL, Sentinel sends a POST every time a trace's status changes. Each request carries these headers:

x-sentinel-event-id
Unique per event. Use it to ignore duplicates.
x-sentinel-event-type
trace.status_changed, or webhook.test for test events.
x-sentinel-sequence
Increases per trace. Ignore an event whose sequence is not higher than the last one you processed.
x-sentinel-timestamp
Unix seconds when it was signed.
x-sentinel-retry
0 on the first attempt, then 1, 2, … Failed deliveries are retried with backoff, up to 10 attempts.
x-sentinel-signature
v1= followed by HMAC-SHA256 of "<timestamp>.<raw body>" with your shared secret.

Verify every request before acting on it, and reply with any 2xx within 10 seconds:

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the exact request body string, before JSON parsing.
export function verifySentinelWebhook(secret, headers, rawBody) {
  const ts = Number(headers["x-sentinel-timestamp"]);
  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; // replay window
  const expected = "v1=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const given = String(headers["x-sentinel-signature"] ?? "");
  return expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
Example event
{
  "id": "6c0f3b1e-…",
  "type": "trace.status_changed",
  "createdAt": "2026-10-12T11:18:39.000Z",
  "sequence": 3,
  "data": {
    "traceId": "4d73f21d-…",
    "status": "MINT_CONFIRMED",
    "previousStatus": "ATTESTED",
    "reason": null,
    "sourceTxHash": "0x2191…e41e",
    "destinationTxHash": "0x5281…ec39",
    "amountAtomic": "5869596",
    "invariants": { "value-conservation": "PASS", "…": "PASS" }
  }
}

Verifying evidence

An evidence file is a signed record of everything Sentinel observed for a trace and the verdict it reached. You don't have to trust this site to check it: the open-source verifier recomputes the file's hashes and checks its signature against the public signing key published at /.well-known/sentinel-key.json.

If the evidence was anchored, the verifier also reads the Arc transaction and confirms that the registry recorded this exact hash for this trace, from this site's account.

curl -OJ "$BASE/api/traces/$TRACE_ID/evidence?download=1"
npx pnpm@10.18.3 --filter @sentinel/evidence verify-evidence sentinel-evidence-*.json \
  --key-url "$BASE/.well-known/sentinel-key.json" \
  --anchor-tx <anchor transaction hash, if anchored>

Run it from a checkout of the ArcFlow Sentinel repository. Exit code 0 means valid, 1 means it failed a check, 2 means it could not run.