signafAIde
Docs menu

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 pathauthpurpose
POST /v1/sandboxnone (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/projectkey{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/agentskey{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/recordskey{records:[signed record...]} (at most 1000) returns {receipts:[...], heads:[{head, head_sig}]}
GET /v1/heads/{agent}keyreturns 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}keyreturns {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=keyreturns 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=nonepublic list of anchors: {anchors:[{anchor, anchor_sig}]} with seq > after, oldest first, at most 100
GET /v1/usagekey{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/eventskey{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.jsonnone§5
GET /healthznoneok

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:

codestatuswhen
chain_conflict409the 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_signature400
unknown_agent400
content_mismatch400
quota_exceeded429the Free plan's monthly allowance (or the sandbox's) is used up; paid plans never get it for live records
plan_required402the 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_key401
invalid_record400
rate_limited429
not_found404the record, agent head or route does not exist
bad_request400a 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:

JSON
{"keys":[{"key_id":"...","public_key":"...","valid_from":"...","valid_to":null}]}

Old keys stay listed for as long as any record they signed may exist.