Developer Guide

Sanctor API

Last updated Oct 6, 2026

01

The shape of the thing

Sanctor is an authority engine: a private, permissioned, append-only hash chain compiled from your agreements. You integrate against one chain per relationship, such as a company, a trust, or a joint venture. Every chain stands alone. Nothing is shared between chains, there is no public network, and there is no token.

Three facts organize the whole API.

Everything is replay. The chain stores one ordered list of signed events, and nothing else. The book of rules in force, every past edition, every office and who held it when, the task pool, the cap table: all of it is computed from the list, never stored. This is why the read surface is small, and why you can verify everything yourself (see Reconstructing everything).

The model routes; the rules decide. A natural-language layer sits in front of the engine. It reads a question, finds the rule or clause that speaks to it, and wraps the answer. It never decides anything. After adoption, no model is involved in any decision. Every entry is sealed by the signatures the written rule names. You can also call the engine with rule ids directly and skip the model. The record comes out the same either way.

Only people, and keys they control, start anything. There is no admin, no owner, and no operator path. The compiler, the matrix, and the pool sit in the middle of arrows, never at their start. Every change begins with a key signing something, and every key's power traces to an adopted text, or to the founding entry itself.

02

The surface at a glance

One birth call, five verbs. Everything a client (a UI, a script, an agent) can ever do is in this table. Everything else in the product is one of these calls wearing clothes.

CallWhat it isWhat it writes
genesisCall zero. Births a chain: founders, threshold, clock, ground rulesThe GEN entry, the only self-authorizing entry
proposeA document or drafted rule comes in: hashed, words extracted, compiled, queued at its doorA pool item (off the chain until it exits)
signA key answers a pool item: approve, refuse, or withdrawThe item's exit entry, once settled (C/R/L/W/X)
checkThe ask. What would happen, read from the book as it standsNothing; a pure reading
doThe act, sealed. Flavors: act, attest, bind, unbind, departOne entry, authorized on its face
readEntries, the book as of any date, trace, chain of title, the explorerNothing; pure replay

Two doors, one intake. check and sign form the authority door, answered by the signers the rule names. attest is the truth door, answered by those the fact concerns. propose is the intake: documents on their way to adoption. Nothing enters the chain any other way.

03

Keys and identity

A key is an identity on the chain: a person, an office holder, or an agent. There are no accounts, no usernames, and no admin who creates users. A key matters only when the chain says it does.

  • Controllers (CTL) are born in the genesis entry. They are the only identities not created by an adopted text, because something has to adopt the first text. CTL seats change only by an adopted succession rule, never by bind or unbind.
  • Office holders exist because an adopted rule names an office and a bind entry fills it. The bind needs two things: the appointment, at the required threshold, and the key's own acceptance signature. A key is never bound without signing its own binding.
  • Agents (AGT), such as capability services, AI agents, or your own automation, are keys like any other, bound to a defined role. The chain records an agent's binding the same way it records a person's.

Signing is the whole of authentication. Every call that writes carries the caller's key, and the entry face records it forever. A key the book gives no power to can still attest facts that concern it, and can propose through a door that accepts from it. It can never confirm anything the rules do not let it confirm.

Who a key belongs to is your business, not the chain's. The chain stores keys; you keep your own labels. Keys in the examples below: k:4f1e, k:8c02, and k:d91a are founders, k:77aa is a counterparty, and k:a9e1 is an agent.

04

genesis: call zero

Genesis births a chain, and it is deliberately small. It seals only what cannot be adopted: who gets to adopt, and the rules that make adopting mean something. No rules, no roles, no people beyond the founders. The book is empty at birth.

http
POST /chains
{
"controllers": ["k:4f1e", "k:8c02", "k:d91a"],
"threshold": 3,
"deadline_days": 45
}

Response, the GEN entry, sealed:

json
{
"seq": 1000,
"entry": "GEN",
"hash": "0xeed6421824bc",
"prev": "0x000000000000",
"control_fact": "3/3",
"constitutive": ["G1","G2","G3","G4","G5"]
}

What the one call fixes for the chain's whole life:

  • The control fact: the founding keys and the threshold. This is the CTL office. The GEN entry is the only entry not authorized by the book. The founders' signatures authorize it, and everything after answers to it.
  • G1 to G5, the constitutive rules: the ground layer no adoption can amend away. Entries are append-only and hash-chained (G1). The book changes only by adoption through a door (G2). Pool items carry deadlines and lapse (G3). Measures describe and never decide (G4). Facts are attested by those they concern (G5).
  • The clock: deadline_days, inherited by every pool item ever opened.

A workspace is a chain, so genesis is also how you multiply. A new entity, a new trust, or a new venture is one more call. Each chain stands alone.

05

propose: documents and rules in

Propose is the one intake. A whole founding text, a single-rule amendment, or a resolution drafted from a dropped document all take the same path.

  1. The document is hashed and its words extracted. The hash and the words ride the chain. The file is handed back and never kept. Sanctor stores words and hashes, not files.
  2. The compiler turns it into rule drafts and checks them at the clarity gate. Nothing ambiguous can be proposed. The compiler returns what it could not read, and why.
  3. The drafts are assigned to their door: the rule whose consent the change needs, or the founding door if none exists yet.
  4. The item waits in the pool, with a deadline.

A founding text, right after genesis:

http
POST /chains/{id}/proposals
{
"by": "k:4f1e",
"document": "<the operating agreement, any format>",
"cause": "T"
}

Response:

json
{
"pool_item": "0x0aa72b6507e8",
"instrument": "T:0x81b2045ec159",
"door": "founding (3/3 controllers)",
"rules_drafted": ["R-0001@1","R-0002@1","R-0003@1","R-0004@1","R-0005@1"],
"deadline": "2026-03-26"
}

A later single-rule change uses the same call with a drafted rule instead of a document. It lands at whatever door the book assigns. One rule, one door, resolved from the adopted texts, never chosen by the caller.

Causes. Every proposal carries why it exists, as one code:

  • T: an instrument, by its hash
  • L: a law. A law is never mandatory here; it only creates a proposal the parties can accept, change, or reject
  • A: a confirmed act being codified
  • R: a refused act prompting reform
  • F: a confirmed fact
  • P the parties' own choice

Cause is not source. Law and facts can cause a rule; only adoption makes it binding.

Guards worth knowing: constitutive rules cannot be claimed by any door, no door may claim a rule that does not exist yet, and a waiting item goes stale if an adoption moves its rule's door.

06

sign: answering the pool

A pool item waits for the signatures its door names. Nothing else can move it. Each signer answers once:

http
POST /chains/{id}/pool/0x0aa72b6507e8/signatures
{ "by": "k:8c02", "answer": "+" }

Answers are + (approve), - (refuse), or withdraw (the bringer pulls their own item). The item sits off the chain while it waits; the chain never carries half-decided things. It leaves the pool exactly once, through one of five exits, each written as one entry.

ExitCodeWhen
ConfirmedCthe threshold is met and the change seals; an adoption recompiles the book into a new edition
RefusedRthe threshold can no longer be reached
LapsedLthe deadline passes without an answer; written automatically
WithdrawnWthe bringer pulls it
SupersededSa sibling version of the same matter sealed first
Failed at sealXconfirmed, but the head moved and the item no longer reads the same; refused at the last check, no seal

The deadline comes from genesis (deadline_days) and starts at the item's birth. Lapse is not punishment; it is how the pool stays clean. Silence is an answer, written as L, and the proposal can always be brought again fresh.

Discussion happens on the item while it waits, and it is kept with the exit entry. Readable later from the record, never editable.

Versions and matters

A matter is a tag on pool items and entries. It describes and never decides. Every propose into a matter creates a new version. Versions never replace each other. They sit side by side as candidates, and each has its own id.

http
POST /chains/{id}/matters/M-2026-31/versions
{ "by": "k:8c02", "text": "Pay Harlow Vendor $18,400 for the warehouse rebuild", "kind": "decision" }

A version is private to its author until the first signature, which commits it to the pool. Each signer holds one live signature per matter. A signer may move it to another version or withdraw it until a version seals.

http
POST /chains/{id}/versions/{vid}/signatures { "by": "k:4f1e", "answer": "+" }
DELETE /chains/{id}/versions/{vid}/signatures/k:4f1e withdraw

The first version to reach its threshold seals. Its siblings exit as S. A matter is refused only when no version can still reach the threshold. A matter has one clock, set when it opens, and every version in it lapses together.

kind is decision or fact. A fact is answered by those it concerns, all of them, with confirm or dispute. The consult at send time proposes the kind and the caller may override it before sending.

Documents

intake hashes a file, extracts its words into the matter's stream, and returns the hash. The bytes are not kept. verify takes a file and answers whether its hash is on the chain, and where.

http
POST /chains/{id}/intake multipart file → { "hash": "0x81b2045ec159", "words": 4120, "matter": "M-2026-31" }
POST /chains/{id}/verify multipart file → { "hash": "0x81b2045ec159", "found": true, "entry": 1014 }

07

check: the ask

Check is a pure reading: what would happen if this act were done now, answered from the book as it stands. Nothing is recorded, signed, or reserved. Call it as often as you like.

With natural language in front (the routed form; this is where the model lives):

http
POST /chains/{id}/check
{ "by": "k:8c02", "ask": "May we pay Harlow Vendor $18,400 for the warehouse rebuild?" }

Or with the model skipped entirely (the bare form; the result is identical):

http
POST /chains/{id}/check
{ "by": "k:8c02", "act": "pay", "params": { "amount": 18400, "payee": "Harlow Vendor" } }

Response, the reading:

json
{
"answer": "yes, with signatures",
"rule": "R-0006@2",
"clause": "§4.3",
"requires": { "threshold": "2/3", "of": ["k:4f1e","k:8c02","k:d91a"] },
"edition": "0xc3969e07c822",
"trace": "matched §4.3: payment over $5,000 needs two officers"
}

Three answers are possible:

  • Allowed alone. The rule lets this key act by itself; do will seal in one step.
  • Yes, with signatures. do opens a pool item at the rule's signers.
  • The book is silent. No rule speaks. See the gap path under do.

The trace field is the routing made visible: which rule matched, and why. It is returned as explanation and never written. The reading is honest only for the edition it names. do reads again at the head, so a book that changed in between changes the answer. That is the X exit.

08

do: the act

Do commits what check read. It reads the book again at the head, at the moment of sealing. Authority is checked once, on the entry's face, and never re-derived afterward. If the rule lets the key act alone, the entry seals in one step. If the rule names signers, do opens a pool item, and the entry seals when they confirm.

http
POST /chains/{id}/do
{ "by": "k:8c02", "act": "pay", "params": { "amount": 18400, "payee": "Harlow Vendor" } }

The flavors share one verb; only the answerers change.

  • **act**: conduct under a rule, as above. The authority door: the rule's signers.
  • **attest**: a fact declared, such as "we signed the vendor agreement on Friday." The truth door: answered by those the fact concerns (G5), not by whoever the rules favor. Confirm or dispute, nothing in between. A fact is never authority. Admission is not cure; conduct outside the rules stays recorded as what it was.
  • **bind / unbind**: a key takes or leaves an office. Bind needs the appointment at the required threshold, plus the key's own acceptance signature. Unbind is resignation (the key alone) or removal (the threshold). CTL is never bound or unbound.
  • **depart**: conduct that departed from the book, recorded honestly: breach, discharge, or awaiting ratification. The record keeps what happened, not what should have happened.

The gap path. When check answers "the book is silent," the fact can still be recorded; the chain never refuses truth. The engine then drafts a proposal from the gap: the unmatched entry becomes a drafted rule, routed to its door like any other proposal. Codify what you confirmed (A:), reform what you refused (R:). The book grows at exactly the places life outran it.

09

read: everything back

Read is pure replay, grouped however the question needs. The useful forms:

http
GET /chains/{id}/book the rules in force now
GET /chains/{id}/book?as_of=2026-06-01 the same book, replay stopped earlier
GET /chains/{id}/entries?from=1000 the record, in order
GET /chains/{id}/entries/1014 one entry's full face
GET /chains/{id}/rules/R-0002/title chain of title: every version, rule to words
GET /chains/{id}/trace/0x7d41e2 everything that followed from an instrument
GET /chains/{id}/pool what waits now (off the chain, so only under the head)

One entry, resolved:

json
{
"seq": 1014,
"rule": "ADP-0003",
"out": "A",
"by": "k:4f1e",
"sig": "3/3",
"cause": "L:0xc4112db9d674",
"edition": "0x0dee5f36771e",
"signed": ["k:4f1e·CTL+","k:8c02·CTL+","k:d91a·CTL+"],
"prev": "0x2d06c7564900",
"hash": "0x0653003ce80f",
"instrument": { "hash": "0xc4112db9d674", "words": 2210 }
}

Every reference resolves at the moment asked about, not the moment of reading. The rule id resolves to the clause as it stood at that edition. The keys resolve to whoever held them that day. The instrument's words are on the chain under its hash. Supply the original file to verify and it is hashed again and matched. A changed file returns a mismatch, not a document.

Three separations live on every face, and none borrows from the others:

  • Integrity: each entry hashes the one before it, so the list cannot be rewritten.
  • Authority: the signatures on the face, checked once at seal, never re-derived.
  • Chain of title: each rule's words trace version by version to the instrument that brought them.

10

Reconstructing everything

You never have to believe the server about state. You only trust the list, and the list is hash-chained. Given the entries, everything else is arithmetic:

text
entries --replay--> book, every edition, offices, pool, registers, depth

An independent replayer is an afternoon's work:

python
state = State()
for e in get("/chains/{id}/entries"):
assert e["prev"] == state.head # integrity: the chain links
assert e["hash"] == H(canonical(e)) # integrity: the face is intact
state.apply(e) # deterministic: no model, no clock, no I/O
assert state.edition_hash == get("/chains/{id}/book")["edition"]

H() is truncated SHA-256 over canonical JSON. apply is specified move by move. An adoption recompiles the book and must land on the same edition hash. Bind and unbind move seats. Exits close pool items. If your replayer and ours disagree on any derived value, one of us has a bug, and the entries will say which. There is no state to argue about, only the list.

What this buys in practice:

  • Audit: an auditor runs the replayer, not an interview. Every register (minute book, resolutions, cap table) is reproducible from the list.
  • Exit: the entries export whole, words and hashes included. Nothing about your governance is locked in our storage format.
  • Dispute: "as of 1 June" is not a backup lookup. It is the same replay, stopped early. Every past state is first class.
  • Integrations: your systems can act on our readings and spot-check them against their own replay.

11

Roles, capabilities, and agents

Roles need no sixth call. There is no create-role endpoint. An office exists because an adopted rule names it, as actor or approver. A bind entry fills it: the appointment plus the key's own acceptance. A defined but vacant office is a real, visible state. When a text creates an office, someone must be asked to fill it, or it sits empty. To rename or dissolve an office, amend the texts that name it. The org chart is a shadow of the agreements. Roles arrive inside propose (a text naming an office) and do (a bind filling it), and come back out inside read.

Capabilities are clients with keys. The filings agent, document reading, reminders, and the routing model itself: none has a private path into the engine, a special entry type, or a footnote in replay. Each holds a key, bound to a defined role, acting through the same five verbs as a person. This means a replayer never needs to know capabilities exist. You can ignore ours and build your own on the same calls. Turning one off breaks nothing; its key goes quiet, or is unbound with one more entry. Billing for capabilities is metered on the account and never written to the chain.

Agents are governed by adoption, not configuration. An agent acts by a key, so the book governs it exactly as it governs a person. A rule can cap what the filings agent may do, and its every act answers to that rule at seal. You do not configure your AI's permissions in a settings panel. You adopt them, with signatures, on the record.

text
1028 key k:a9e1 bound to filings agent (KMS key) as AGT
1029 G5▸R-0011@1 F by k:a9e1 · actor k:4f1e · discharged

Two lines from the demo chain: an agent is bound like anyone, and its work lands as ordinary entries.

12

Usage and metering

Every call returns what it cost, as a usage object on the response:

json
{
"answer": "yes, with signatures",
"rule": "R-0006@2",
"usage": { "input_tokens": 211, "output_tokens": 64 }
}

The rules are simple:

  • One unit, one meter. Everything is billed in tokens: model work at its token count, and deterministic work (compiles, seals, replays, reads) normalized to token equivalents. A heavy replay costs more than a light read, and both show up the same way on the same bill. The bill is tokens plus the monthly subscription, nothing else.
  • Input and output, on every response. Input tokens cover what the call carried in; output tokens cover what came back. Streaming responses report input tokens when the request is read and output tokens when the response finishes.
  • Usage never touches the chain. It is metered on the account, returned on the response, and aggregated at GET /account/usage. A billing number has no place on a sealed entry face.

Capabilities meter the same way: whatever a capability's key does shows up as that account's usage, so turning one on means faster token burn and nothing else changes.

13

Errors, guards, and notation

Every refused call returns Invalid with a reason in plain words. Nothing is partially applied; a refused call changes nothing. The guards you will actually meet:

GuardRefusal
Threshold"appointment needs 3 of the controllers; got 2". Signatures short of the rule's bar
Doorthe rule's door does not accept from this key, or the rule is constitutive (G1 to G5; no door may claim those)
One rule, one doora second door claims a rule already claimed
No pre-claimsa door claims a rule that does not exist yet
Stalethe item's rule moved doors while it waited; bring it again at the new door
Deadlinethe item lapsed; answering after L is refused, so propose afresh
Seal checkconfirmed, but the head moved and the reading changed; written as X, no seal
CTLbinding or unbinding a controller; seats change only by adopted succession
Role undefinedbinding to an office no adopted text names
Claritythe compiler could not read the document without ambiguity; it returns what and why

Notation reference, what you will see on entry faces and in the explorer:

  • Outcomes: C confirmed, R refused, L lapsed, W withdrawn, S superseded, X failed at seal, F fact, D departure, A adoption, G genesis
  • Causes: T: instrument, L: law, A: confirmed act, R: refused act, F: confirmed fact, P parties' choice. One code, one meaning
  • Rules: R-0002@2 means rule 2, version 2. Adoptions write R-0002@1 → R-0002@2 · hash. Refused changes write R@v → — · hash
  • Signatures: k:4f1e·CTL ▸ k:8c02·CTL+ k:d91a·CTL+, with the opener before ▸ and the answers after. Offices show as ·CTL, ·OWN, ·AGT. A bare key is a personal attester
  • Hashes: truncated SHA-256 (12 hex) over canonical JSON. Entries, instruments, and editions all use the same H()

One discipline ties all of this together: codes, hashes, and keys only. One number per row, nothing derived on a face. Anything computable is computed at read time by replay, so the sealed record never carries a number that could disagree with its own arithmetic.