API reference
Generated from the protocol specification, sections 5 and 6. The SDKs call this API for you; you need it only to write a client of your own. Every error points at Errors.
Authentication and API keys #
API keys are sfa_test_<32 base62> for the sandbox and sfa_live_<32 base62> for live. The prefix sets the env. Keys are stored only as hashes.
Endpoints #
| method and path | auth | purpose |
|---|---|---|
POST /v1/sandbox | none (rate-limited per IP) | creates an anonymous sandbox project: no account, email or card. Returns {project, api_key, claim_url, free_quota, terms_url, privacy_url, notice} (free_quota in records per month; terms_url and privacy_url are the service's Terms and Privacy notice, which creating a sandbox accepts, and notice says so in one line that clients show the person). Clients call it only on an explicit human opt-in (signafaide init, the MCP signafaide_init tool); recording never calls it (SPEC §8) |
GET /v1/project | key | {project, env, claimed, name} of the key, so a client can sign envelopes (§2.2) before its first record is accepted; claimed says whether a sandbox now belongs to an organisation (its claim link can no longer be used); name is the project's name, which signafaide status shows |
POST /v1/agents | key | {name, public_key, valid_from, proof, rotation?} (§2.4) returns {agent, key_id, retired}. Idempotent. A missing or wrong proof or rotation is bad_signature; a key already registered to another agent, or a valid_from out of range, is invalid_record |
POST /v1/records | key | {records:[signed record...]} (at most 1000) returns {receipts:[...], heads:[{head, head_sig}]} |
GET /v1/heads/{agent} | key | returns the current signed head {head, head_sig}, or not_found when the agent has no records (registered or not). A client whose local chain is gone (an ephemeral disk) continues after it (SPEC §8); on not_found (or unknown_agent from an older witness) the chain starts at seq 1 |
GET /v1/records/{record_hash} | key | returns {record, receipt, anchor}: the signed record, its receipt, and {anchor, anchor_sig, path} (path as in §4) once anchored, else null |
GET /v1/export?agent=&from=&to= | key | returns a verification pack (SPEC §7) of the key's project and env: every agent by default, only agent when given, records with from <= envelope.time < to. The pack names the filter, holds the newest signed head of each agent that has records in it, and carries the service's signed export statement (SPEC §7); all of it is read in one database snapshot. At most 50,000 records; a larger export is bad_request and is split with agent, from and to |
GET /v1/anchors?after= | none | public list of anchors: {anchors:[{anchor, anchor_sig}]} with seq > after, oldest first, at most 100 |
GET /v1/usage | key | {period, used, quota, contract, plan}: quota is the plan's included records for live keys (a limit only on Free) and the sandbox's for test keys. On Enterprise it is the contract's allowance (contract: true), or null when the contract sets none |
POST /v1/events | key | {name} where name is verify or export: the SDK reports that the client ran one (for the service's adoption metrics). Returns 202 |
GET /.well-known/signafaide-keys.json | none | §5 |
GET /healthz | none | ok |
Client header #
SDKs, the CLI and the MCP server send X-Signafaide-Client: <sdk>/<version> (<language> <runtime version>; adapter=<name|none>; agent=<claude-code|codex|cursor|gemini-cli|copilot|windsurf|unknown>; via=<sdk|cli|mcp>), optionally with ; strict=1 when strict mode is on. The service parses it leniently and uses it only for usage metrics.
Errors #
Errors are {"error":{"code":"...","message":"one sentence that says how to fix it","docs":"<url>"}}. The codes are:
| code | status | when |
|---|---|---|
chain_conflict | 409 | the error object also holds the service's head for the conflicting agent: "head":{agent, seq, record_hash} (seq 0 and record_hash null when it has no records) |
bad_signature | 400 | |
unknown_agent | 400 | |
content_mismatch | 400 | |
quota_exceeded | 429 | the Free plan's monthly allowance (or the sandbox's) is used up; paid plans never get it for live records |
plan_required | 402 | the organization's plan does not include the feature, for example content on live records on Free. The message names the plan and the console billing URL |
invalid_key | 401 | |
invalid_record | 400 | |
rate_limited | 429 | |
not_found | 404 | the record, agent head or route does not exist |
bad_request | 400 | a query parameter or request body (other than a record) is invalid |
Client behaviour on errors. A client sends one request at a time per process. After a network, rate_limited or 5xx error it backs off (doubling, to at most 60 s). After an error retrying cannot fix (quota_exceeded, invalid_key, plan_required, bad_signature, invalid_record, content_mismatch, chain_conflict) it stops sending in the background until its configuration (API key, witness URL, agent keys) changes; an explicit signafaide flush always tries again. A malformed API key (§6 key format) is never sent. Outside strict mode, nothing in a recorded call waits for the network or raises because of it.
Idempotency and ingest rules #
re-sending an already accepted record returns its original receipt. In a request that mixes accepted and new records, only the new ones form the new batch.
Ingest rules (POST /v1/records, invalid_record unless noted): envelope.project and envelope.env are the key's; envelope.time is at most MAX_CLOCK_SKEW (300 s) after the service's clock and not earlier than the previous record's time in that agent's chain; the key is active at envelope.time and not revoked at the moment the service receives the record (bad_signature).
Service keys #
GET /.well-known/signafaide-keys.json returns:
{"keys":[{"key_id":"...","public_key":"...","valid_from":"...","valid_to":null}]}Old keys stay listed for as long as any record they signed may exist.