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.
| Field | Type | Notes |
|---|---|---|
| sourceTxHash | string | Required. Hash of the transaction that burned USDC (DepositForBurn). |
| sourceChain | "base" | "arc" | Required. Chain the burn happened on. |
| requireAppConfirmation | boolean | Optional. 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
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.
| Field | Type | Notes |
|---|---|---|
| sourceTxHash | string | Required. 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
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.
| Field | Type | Notes |
|---|---|---|
| id | uuid | Required. In the path. |
Responses
- 200
- OK
- 404
- No such trace
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).
| Field | Type | Notes |
|---|---|---|
| id | uuid | Required. In the path. |
Responses
- 200
- OK
- 404
- No such trace
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.
| Field | Type | Notes |
|---|---|---|
| id | uuid | Required. In the path. |
Responses
- 200
- OK
- 404
- No such trace
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.
| Field | Type | Notes |
|---|---|---|
| id | uuid | Required. In the path. |
| download | "1" | Optional. In the query string. |
Responses
- 200
- OK
- 404
- No such trace
- 503
- Unavailable or degraded
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.
| Field | Type | Notes |
|---|---|---|
| id | uuid | Required. 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
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
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.
| Field | Type | Notes |
|---|---|---|
| id | uuid | Required. In the path. |
| Field | Type | Notes |
|---|---|---|
| reference | string | Optional. 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.
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).
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
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
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
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:
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));
}{
"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.