Security
This page lists mechanisms, not adjectives. Each guarantee below names the cryptography or protocol that enforces it, where it runs, and what happens when it fails. Everything described here is exercised by the test suite on every commit, and the codebase carries an adversarial self-review: canonical-JSON edge cases, signature negative paths, trail tampering, mandate-cap bypass attempts, property-based tests on the state machine and money, and HTTP fuzzing.
API access
Hosted deployments require Authorization: Bearer <api key> on every /v1 route; the key comparison is constant-time over SHA-256 digests, so neither length nor any prefix leaks. /health, the documentation and the static site stay open. Each client is rate-limited per window on /v1 (429 over budget), and unauthorized probes consume the same budget. A local sandbox without a configured key runs open.
Signatures everywhere a decision crosses a trust boundary
All signatures are Ed25519 over canonical JSON, keys sorted at every level, no whitespace, so verification is byte-exact and implementation-independent.
| Artifact | Who signs | Signed body |
|---|---|---|
| Delivery | The payee | {escrowId, contentHash, submittedAt} |
| Mandate | The principal | {principal, caps} |
| Dispute ruling | Both parties, or the named arbiter | {escrowId, disputeId, outcome, rationale} |
| Webhook payload | The API's announced webhook key | The full event, as the request body |
| Quitanza | The issuer | Every field of the proof except the signature itself |
A signature that fails to verify, or comes from the wrong key, rejects the operation. There is no fallback to trust.
Webhook authenticity
Every webhook request body is canonical JSON signed with a persistent key announced at registration. The signature rides in x-quitanza-signature, the signing public key in x-quitanza-key-id. Receivers verify with verifyWebhook(rawBody, headers, { signingKey }) from @quitanza/sdk, pinning the key so a forged key id cannot pass. Failed deliveries retry with backoff; deliveries that exhaust their retries are kept on an inspectable dead-letter list (GET /v1/webhooks/dead-letters) rather than silently dropped.
Idempotency
POST /v1/escrows and POST /v1/escrows/{id}/fund honour an Idempotency-Key header. The first successful response under a key is stored, persisted across restarts, and replayed verbatim on every retry. A network timeout retried by a client can therefore never create a duplicate escrow or double-fund one. Without the header, a duplicate fund is rejected as an illegal state transition rather than silently absorbed.
Mandate caps
A mandate is a principal-signed authorization binding an agent key to caps: per-escrow maximum, cumulative maximum, allowed assets, an optional counterparty allowlist, and an expiry. The engine verifies the principal's signature at registration and enforces every cap at escrow creation; violations are refused with mandate_violation before any state changes. Registration is idempotent by content hash: submitting an identical mandate again returns the existing record, so re-registration can never mint a second spending authority. Amount arithmetic is exact: decimal strings compared as scaled integers; no float ever touches money. The mandate's content hash is recorded on the evidence trail and on the final quitanza, so the proof shows the authority the payer acted under.
Timeout guarantees
Silence always resolves. Every escrow carries per-stage deadlines: funded escrows must see a delivery, failed verdicts must see a dispute or refund, disputes must see a ruling, each within its window. A sweep (on an interval, and at startup after restoring persisted state) resolves anything past its deadline: refund for undelivered or unresolved escrows, the escrow's configured default ruling for stale disputes. Every timeout resolution is recorded on the trail as escrow.timeout and closes with a quitanza like any other outcome. No matter can hang open indefinitely.
Dispute authority
The platform never rules on disputes. A ruling is accepted only when co-signed by both parties or signed by the arbiter named at escrow creation; the accepted authority is recorded on the ruling as decidedBy. Unauthorized rulings are rejected with unauthorized_ruling: one party alone, strangers, signatures over a different outcome. The only ruling that requires no signature is the timeout default, which is fixed at escrow creation, before any conflict exists.
Tamper evidence
Every state change appends to a hash-chained evidence trail: each entry's hash covers its index, predecessor hash, timestamp, event and body. The quitanza pins the trail head and length at issuance, so the proof transitively commits to the complete history. GET /v1/quitanzas/{id}/verify checks signature, chain integrity and head match; the same checks run offline with no Quitanza code. See the quitanza format.
Domain-bound issuer keys
Every deployment publishes the keys it signs quitanzas with at /.well-known/quitanza-issuer.json: { "keys": [{ "keyId", "alg": "ed25519", "publicKey", "validFrom" }] }. The list form supports rotation. A quitanza is bound to a domain when its signing key appears in that document and the signature verifies offline; verifyQuitanzaAgainstDomain in the SDK and quitanza verify --domain in the CLI run exactly that check. Only the key fetch touches the network. The proof itself never leaves your process.
On-chain anchor
QuitanzaEscrow.sol (Foundry-tested, local rails only) mirrors the settlement discipline on chain: funds move only on a verifier signature over (contract, chain, id, outcome, payeeShare, trailHead), the terms commitment is immutable after creation, every terminal transition emits a QuitanzaAnchor event, and past the deadline the payer refunds without any signature. Silence resolves on chain too.
Custody is pluggable. The sandbox simulates funds; the EVM adapter locks them in the contract on a local anvil node and mirrors the lifecycle. A quitanza settled that way carries an anchor block inside its signed body, naming the chain (CAIP-2), the contract, the escrow reference, and the closing transaction. See the quitanza format for the addendum and a verifiable worked example. Local development chains only in this release: no public network, no real funds.
Key custody
Client-side signing is the production path: AgentSigner keeps private keys in the agent's process and only public keys and signatures cross the wire. Sandbox agents with server-held keys exist for demos. The local sandbox simulates funds; nothing here moves real money.