Production checklist
A pre-flight checklist for any integrator about to enable Satsignal in production. Goes beyond the per-path tutorials — covers the cross-cutting operational concerns that don't fit any single chooser card: which events you anchor, the fields you must persist, the retry / backoff matrix, the key-rotation runbook, and the recovery path for stuck broadcasts. Walk through this list before you ship. Revisit it when you onboard a new team or change an integration's volume.
Companion docs: API reference · Compatibility map · What to hash
1. The 60-second framing
Satsignal anchors hash commitments in BSV OP_RETURN outputs. Your integration calls POST /api/v1/anchors, gets back a proof_id + txid + bundle_url, persists those fields, and later can prove that a given byte string existed in bit-identical form at or before a specific block time. The per-path guides (Files, Webhooks, Agents, etc.) walk you through the wire shape for each integration shape. This page covers everything else: the decisions and runbooks that apply to every shape.
Use it as a literal pre-flight checklist before turning anchoring on for real traffic. Section 11 has the copy-paste version your team can paste into a launch ticket.
2. Decisions to make before any anchor goes out
These are the irreversible choices. Get them wrong and you'll be retrofitting your data model later — or worse, anchoring the wrong thing for months and discovering it at audit time.
- Which events / artifacts get anchored? Pick deliberately; retro-deciding what to anchor is hard. Common starting points: every outbound contract, every model-evaluation report, every customer support decision, every release artifact, every immutable-publish webhook from a partner platform.
- Which mode? Standard (a single file or canonical doc), Sealed (HMAC-blinded commitment when even the hash is sensitive), Manifest (Merkle root over up to 10,000 items in one anchor). The mode is set per-call — you don't have to choose globally.
- Are you OK with the issuer chain-tag? Every anchor's on-chain payload carries a 4-byte
issuer_idTLV (/spec-mbnt §11). On the hosted tier it is one constant shared by all workspaces — a chain observer sees "a Satsignal-issued anchor at time T", not which workspace — but anchoring through Satsignal at all is publicly observable. A self-run deployment with its own issuer DID makes its anchors enumerable as a class on-chain (and per-customer DIDs make each customer's anchors enumerable). There is no per-request opt-out on the hosted API today; self-run pipelines can suppress the TLV via the pipeline'spublishconfiguration (issuerflag). Details: /whats-on-chain §4. - What bytes get hashed? This is the most common verification-time mistake. JSON serializers reorder keys; CSVs differ by line ending; PDFs have timestamps. Decide on a canonical normalization once, then persist the canonical bytes (not just the source). See What to hash.
- What categorical labels do you use? The
categoryfield is a closed enum:commitment,policy_snapshot,evidence_bundle,memory_checkpoint,document,output. Free-text tagging belongs in thelabelfield instead. Pick which enum values you use early and document it; mixing taxonomies after launch is painful. - Where do anchors live in your data model? Single table? A column on each event row? A foreign-key sidecar table? The "store these fields" section below is the minimum row shape — your data model decides where that row lives.
- Who owns the key? A single shared key for the integration, or a scoped key per partner / tenant / team? Scoped keys limit blast radius on rotation. See §6.
3. The "store these fields" checklist
The single most important integration rule. For every anchor your code POSTs, persist these fields together — alongside the original canonical artifact you hashed. The proof is only meaningful when paired with the bytes it commits to.
| Field | Why | Source |
|---|---|---|
proof_id | Stable handle for later lookup, dashboard links, audit logs. | Response body. |
txid | The BSV transaction id that carries the OP_RETURN commitment. The only thing strictly needed to re-derive the proof from a block explorer. | Response body. |
bundle_url | Absolute URL of the portable .mbnt bundle. Persist it; downloading once and caching locally is the standard pattern. | Response body. |
proof_url | Dashboard page for humans. A counterparty with the link can verify without an account. | Response body. |
sha256_hex | The hash you submitted. Lets you re-verify without recomputing from the artifact every time. | Request body. |
folder_slug | Grouping namespace. Useful for inventory queries via GET /api/v1/anchors?folder_slug=.... | Request body. |
label | Free-text tag (if you sent one). | Request body. |
category | Your taxonomy bucket. Useful for filtering at audit time. | Request body. |
| The original canonical artifact | The exact bytes whose SHA-256 you submitted. Without these, the chain side still verifies but you cannot tie the on-chain commitment to your specific record. | Your storage. |
A minimal SQL-ish row:
CREATE TABLE anchored_records (
proof_id TEXT PRIMARY KEY,
txid TEXT NOT NULL,
sha256_hex TEXT NOT NULL,
folder_slug TEXT NOT NULL,
category TEXT NOT NULL,
label TEXT,
bundle_url TEXT NOT NULL,
proof_url TEXT NOT NULL,
artifact_path TEXT NOT NULL, -- where the canonical bytes live in YOUR storage
anchored_at_utc TIMESTAMP NOT NULL
);
The on-chain transaction is the source of truth for the proof itself; the row above is your local index into it. Lose the row and you can still find the proof on chain — lose the canonical artifact and you have a hash with nothing to compare it against.
4. Idempotency wiring
Every retry-prone POST gets an Idempotency-Key header. This is the single most important reliability guard in the API.
- The key is a stable client-side identifier. A deterministic suffix on your event id works (
evt_2026-05-26_abc123); a random UUID per retry does NOT — it defeats the cache. - Same key + identical body within 24h → the notary replays the verbatim cached response with
X-Idempotent-Replayed: true. No re-broadcast. No quota tick. Free retry. - Same key + different body →
409 idempotency_key_reuse_body_mismatch. Treat this as a programming error in your retry logic — your client is mutating the body between attempts. Alert; don't auto-retry. - The 24h cache is per
(workspace, key). Multi-key workspaces won't collide on the same key string. - The notary writes the idempotency record BEFORE touching the wallet ("records-before-broadcast"). Under burst load a request can return 504 before its broadcast lands; a retry with the same key returns the original response when the broadcast catches up. This reframes burst-load 504s from "regression" to "recoverable latency-tail".
Concrete shape:
curl -X POST https://app.satsignal.cloud/api/v1/anchors \
-H "Authorization: Bearer $SATSIGNAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-events-evt_demo_123" \
-d '{"folder_slug":"acme-events","sha256_hex":"...","file_size":256,"category":"commitment"}'
On a retry — same key, same body — the response carries X-Idempotent-Replayed: true and you can short-circuit the rest of your post-anchor work (the cached row already has everything).
5. Rate limits and quotas
Every 2xx and every 429 response from /api/v1/anchors (and the sibling read endpoints) carry the rate-limit headers.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Anchors allowed in the plan's quota window. |
X-RateLimit-Remaining | Remaining in the current window. |
X-RateLimit-Reset | UTC epoch seconds when the window resets. |
X-RateLimit-Window | month (free/starter/pro/scale) or day (legacy trial/paid). |
The plan quota window is the anchor API's only key-level throttle — there is no separate per-minute or per-hour burst limiter on POST /api/v1/anchors today. (Auth-free endpoints carry their own hour-windowed per-IP limits — e.g. /lookup_hash at 120/hour, or 5,000/hour per workspace with a bearer key.)
For a quota check without firing an anchor:
curl -H "Authorization: Bearer $SATSIGNAL_API_KEY" \
https://app.satsignal.cloud/api/v1/usage
This returns the current month's count, plan ceiling, and reset epoch — the same numbers the headers carry. Wire it into your monitoring; surprise quota exhaustion is the single most common cause of "why did anchors stop landing?".
A 429 quota_exceeded from the anchor API carries the same X-RateLimit-* headers but no Retry-After, and the window is a fixed calendar window (not a sliding bucket) — a backoff-and-retry loop cannot clear it. Alert on it, wait for X-RateLimit-Reset, or email hello@satsignal.cloud for a cap lift. Hour-windowed endpoints like /lookup_hash do return Retry-After on 429 — honor it there.
Plan tiers (rough shape; subject to change — confirm at satsignal.cloud/pricing):
- Free — 100/month. Hobby / evaluation. Live now.
- Starter — 10K/month. Planned tier; not in self-serve checkout.
- Pro — 100K/month. Planned tier; not in self-serve checkout.
- Scale / design partner — Custom. The main offer for production volume today — email
hello@satsignal.cloud.
Self-serve billing is not yet enabled. Sustained production volume onboards directly today: email hello@satsignal.cloud with your expected anchor volume and we'll lift the cap (or open a design-partner slot if you need invoice billing). Sign up early if you expect to exceed the free tier in your first month.
6. Key rotation policy
API keys are minted at app.satsignal.cloud/w/{workspace}/keys/. The format is sk_<8-char prefix>_<32-char secret> — the prefix is shown in the dashboard for fast identification; the secret is shown ONCE at mint time and is unrecoverable thereafter.
When to rotate
- A key leaks (committed to git, posted in a bug report, sent to the wrong Slack channel).
- A team member with key access leaves.
- Quarterly by policy. Suggested cadence: 90 days.
How to rotate without downtime
- Mint the new key first. Same scopes, same folder restrictions.
- Update every consumer. CI variables, secrets managers, environment files, deployed services. Don't forget local dev keys if they share scope.
- Verify the new key works end-to-end. Fire a real anchor through each consumer; confirm the 2xx + the
proof_urlresolves. - Then revoke the old key. Once revocation is in, every request using the old key returns
401.
The Authorization header alone identifies the workspace + scope; no separate workspace id needs to be sent in requests, so rotation is a single header swap on the client side.
Scoped keys
A key can be scoped to specific folders. Use scoped keys for partner / customer integrations to limit blast radius:
- A key scoped to
folder_slug = acme-eventscannot anchor intofolder_slug = beta-corp-events. - Out-of-scope folder access returns
404 folder_not_found— by design, not403, to avoid enumeration of folder names. - A revoked key returns
401 invalid_keyimmediately.
Document who can mint and revoke keys in your runbook. The most common failure mode is "the key works, but we don't know which engineer owns it" — keep a keys.md in your ops repo listing every active key prefix, its purpose, and its owner.
7. Retry and error handling
Decision matrix. The retry policy depends on which class of error you get back, NOT on the HTTP code alone.
| Code | Class | Retry? | How |
|---|---|---|---|
| 200 / 201 | success | — | Persist response fields. |
400 invalid_type / unknown_field / mode_conflict | programming error | NO | Fix your code. |
400 rejected_field | input | NO (until input is normalized) | Sanitize the offending field. |
400 invalid_sha256 | input | NO | Re-hash; ensure 64 lowercase hex chars. |
400 conflicting_alias | programming error | NO | Send only folder_slug, not both folder_slug and matter_slug with different values. |
401 missing_bearer / invalid_key | auth | NO (until key fixed) | Refresh credentials. |
403 insufficient_scope | auth | NO (until key fixed) | Mint a key with the required scope. |
404 folder_not_found | input | NO | Create the folder first via POST /api/v1/folders. |
409 idempotency_key_reuse_body_mismatch | programming error | NO | Fix your retry logic — body must be byte-identical on replay. |
409 proof_set_requires_force_new | by design | conditional | Re-submit with force_new: true IF you intentionally want a new anchor for the same sha. |
429 quota_exceeded | rate | NO — fixed calendar window | Alert and wait for X-RateLimit-Reset, or request a cap lift. No Retry-After is sent (per-IP 429s like /lookup_hash do send one — honor it there). |
| 500 / 502 / 503 / 504 | transient | YES, with Idempotency-Key | The cache returns your in-flight or completed result. |
Two rules of thumb that cover ~95% of cases:
- Programming errors do not become transient by being retried. If you get
idempotency_key_reuse_body_mismatch, alert — don't loop. - Always retry transient errors with the same Idempotency-Key. The record is written before the broadcast, so the original anchor may already exist on chain even though your client got a 504.
8. Broadcast failure and stuck-anchor recovery
Satsignal's broadcast path is 3-tier failover across independent broadcast services. Most broadcast failures self-recover via fallback, invisible to your client.
Symptoms of a stuck anchor
POST /api/v1/anchorsreturned 200, but thetxidfield is missing or null in the response.bundle_urlreturns 404 hours later when you try to fetch the.mbnt.- Your local row has a
proof_idbut never got atxid.
Diagnosis path
curl -H "Authorization: Bearer $SATSIGNAL_API_KEY" \
https://app.satsignal.cloud/api/v1/anchors/<proof_id>
Inspect the response:
- If
txidis now present → the broadcast caught up. Persist the txid; you're done. - If
txidis still null after 30 minutes → contact support with theproof_id. Don't reanchor.
Idempotency safety
Re-POSTing with the same Idempotency-Key is safe — the cached row will return without re-broadcasting. So if you're unsure whether your original POST landed, retry with the same key and inspect the response.
What NOT to do
DO NOT re-POST without Idempotency-Key to "force" a fresh broadcast. That's how you double-anchor and double-bill — the de-dup gate is on the hash, but a separate proof_id will be created if a different body shape (e.g. an additional force_new: true) is sent. Always retry through the idempotency layer.
9. Verification UX
If your users or auditors will ever want to verify a proof later, surface the verification path in your UI. The verification flow does not require a Satsignal account — anyone with the bundle + the original bytes can verify.
Minimum surface:
- A "Verify this proof" button → opens
proof.satsignal.cloud/verifywith the bundle pre-loaded, or shows the publicproof_url. - The
txiditself, with a link to a block explorer (any BSV explorer works — the chain side is provider-independent). - A way to access the canonical artifact bytes (a downloader, a presigned URL, or copy-paste of the canonical JSON).
- A one-line explanation of what a Satsignal proof DOES prove (tamper-evidence + timing — these exact bytes existed at or before the block time) and what it does NOT (authorship, that the content existed before the anchor was made, or that the content's claims are true).
The web verifier at https://proof.satsignal.cloud/verify accepts the .mbnt + the original artifact and returns pass/fail in ~3s. For a scriptable check today, the unzip + sha256sum recipe hashes the bundle's canonical.json, compares it to doc_hash_expected, and resolves the txid against any public BSV node — fully offline except that last chain-resolution step. (A standalone satsignal verify CLI, with --spv / --min-confirmations, is planned but not yet shipped.)
10. Support flow
Lightweight, by design — most issues are diagnosable from the proof_id.
- Status page: incidents and degraded-broadcast windows are posted on
satsignal.cloud/status.html(when applicable). - Bundle questions / verification disputes: email
hello@satsignal.cloud(the dashboard's Contact support link); include theproof_idand the artifact bytes you're trying to verify against. - Stuck broadcasts: see §8. Include the
proof_idand the approximate POST time. - Quota questions:
GET /api/v1/usagefirst; if the headers disagree with the dashboard, include both. - Free tier: best-effort. There is no formal SLA during early access (see
sla.html); design-partner customers get a direct contact channel.
Don't include API keys in support tickets. The proof_id + the txid (if you have one) are enough for us to pull the workspace audit trail.
11. Pre-flight checklist (the literal checklist)
Copy-paste markdown for your launch ticket. Tick each box before flipping anchoring on for production traffic.
- [ ] We know which events / artifacts we anchor.
- [ ] We know our anchor shape on both axes: single-file vs
items[]manifest batch, and plain SHA-256 vs sealed (HMAC) leaves. (These are independent — a "sealed manifest" is both — not one three-waystandard/sealed/manifestchoice.) - [ ] We store the canonical artifact we hashed (not just the source).
- [ ] We store
proof_id,txid,bundle_url,proof_url,sha256_hex,folder_slug,category,label. - [ ] We use
Idempotency-Keyon every anchor POST. - [ ] We handle 429
quota_exceededby alerting and waiting forX-RateLimit-Reset(noRetry-Afteris sent; retrying cannot clear the fixed monthly window); we honorRetry-Afteronly on per-IP 429s like/lookup_hash. - [ ] We handle 409
idempotency_key_reuse_body_mismatchas a programming error — alert, don't retry. - [ ] We have a verification UX or a documented support flow for auditor / counterparty access.
- [ ] We know who can mint and revoke API keys; rotation cadence is documented.
- [ ] We tested bundle verification at
proof.satsignal.cloud/verifywithout being logged in. - [ ] We tested a retry (Idempotency-Key replay) and confirmed
X-Idempotent-Replayed: true. - [ ] We tested a stuck-broadcast scenario (e.g. a 30-min sweep) and confirmed
GET /api/v1/anchors/<proof_id>returns the eventualtxid. - [ ] We documented for our team / users what a Satsignal proof does and does not prove.
- [ ] We have a key-rotation runbook.
- [ ] Quota visibility (
/api/v1/usageor the X-RateLimit headers) is wired into our monitoring.
12. Where this fits
- Path guides: Files · Webhooks · Agents · CI/CD · Sealed · Manifest
- Reference: What to hash · Compatibility map
- Machine-readable: OpenAPI spec · Postman collection · API reference
Questions about this specification? Email hello@satsignal.cloud.