{
  "status": 200,
  "authority": "theisen-api",
  "url": "https://tiesapi.com/docs",
  "sha256": "sha256:9f44f2a04318598f417e08c0a6a6b1ff8fa75ee318234048d15c2768e5f7854f",
  "bytes": "# TIES API reference\n\nhttps://tiesapi.com · this text is served byte-exact at https://tiesapi.com/docs with its sha256 · source https://github.com/tiesapi/ties\nAuthority: theisen-api, Dr. Theisen · Problem? doctortheisen@gmail.com, with the X-Request-Id of the reply\n\n## Basics\n- HTTPS only. https://www.tiesapi.com answers the same.\n- A browser (Accept contains text/html) gets an HTML page; anything else gets JSON.\n- Every JSON reply has \"status\", equal to the HTTP status. A refusal has \"error\": the reason, in words.\n- Every reply has the header X-Request-Id. The same id is on the reply's log line (see Logging).\n- Stores nothing. A request is checked and answered; nothing in it is kept, and the log line holds no request content.\n- Body: one JSON object, at most 16,384 characters (65,536 for POST /sop/check).\n- The gateway allows 10 requests a second, bursts of 20; over that the gateway answers 429 itself, without \"status\".\n- CORS: Access-Control-Allow-Origin: *. HEAD answers like GET, without the body.\n- Status: 200 ok · 201 receipt issued · 400 refused · 404 not found · 413 body too large · 500 internal error (with request_id).\n\n## Routes\nGET  /                        the routes, the docs URL, and code_sha256: the sha256 of the running lambda_function.py\nGET  /docs                    this reference: {url, sha256, bytes}\nGET  /catalog                 {keywords, entries: [{entry, url, sha256, title, parts}], objects: [{id, url, title, parts}]}\nGET  /catalog/<entry>         one named entry: {entry, url, sha256, bytes}; 404 if there is none\nGET  /catalog/sha256:<hex>    one object by content address: {id, url, sha256, bytes, title, parts}; 404 if no bytes are\n                              here (the address is still a valid label)\nPOST /receipt                 {federated_id, line} -> 201 {receipt, issued_at, hash, label, label_sha256, inside, ...}\nPOST /verify                  {federated_id, hash, issued_at, receipt, label_sha256} -> 200 {valid}\nPOST /sop/check               {federated_id, sop, lines, receipts} -> 200 {valid, complete, steps, done, timed, error}\nGET  /api                     the contracts, one page per route: {form, handlers, contracts: [{name, url, route, title, head,\n                              current}]}\nGET  /api/<contract>          one contract page: {route, input, processing, output, graph, example, ...}; 404 if there is none\nGET  /robots.txt, GET /favicon.ico\n\n## The register line (SOP-001, https://tiesapi.com/catalog/sop-001)\nOne JSON object per line, in a file that only grows. The fields, in the order bin/ties writes them:\n  n        line number, from 1\n  when     ISO 8601 time with offset (not checked by the endpoint)\n  who      the federated id, theisen-api:<16 hex>; must equal the federated_id of the request\n  label    https://tiesapi.com/catalog/<entry> (must be in the catalog), or\n           https://tiesapi.com/catalog/sha256:<64 hex> (a content address: an id before any name; nothing is looked up)\n  inside   sha256:<64 hex> of the exact bytes of the content; the content stays with the holder\n  parents  list of sha256:<64 hex>: the hashes of earlier lines this one ties to ([] for none)\n  memo     public text (may be left out)\n  by       human | agent | both\n  prev     the hash of the line before; null on line 1\n  hash     sha256:<64 hex> of the line without hash, as compact JSON: keys in the order written, separators \",\" and \":\",\n           UTF-8, non-ASCII characters as they are (no \\u escapes)\nLine 1 is the registration: label .../catalog/registration or .../catalog/<its own inside>, prev null.\nOther fields are covered by the hash and not checked, except the run fields below.\nbin/ties derives the federated id from line 1: \"theisen-api:\" + the first 16 hex of sha256(\"theisen-api|\" + the inside of\nline 1). The endpoint checks only its form, and that who equals the federated_id sent.\nA v0 tie line (SPEC.md: frame \"tie\", field, value) is also accepted.\n\nHash a line yourself:\n  python3 -c 'import hashlib,json,sys; d=json.loads(sys.argv[1]); d.pop(\"hash\",None); print(\"sha256:\"+hashlib.sha256(json.dumps(d,separators=(\",\",\":\"),ensure_ascii=False).encode()).hexdigest())' '<line>'\n\n## A run of an SOP (https://tiesapi.com/catalog/sop-nonce-chain)\nThe SOP is a text. Its steps are the lines \"<k> sha256:<64 hex>\" (the prefix https://tiesapi.com/catalog/ is allowed), k = 1..N\nin order. Other lines are free text, but a line that starts with a digit and is not a step is refused.\nThe SOP's id is sha256:<hex> of its exact UTF-8 bytes.\nA run is N+1 register lines, each with four more fields:\n  sop         the SOP's id\n  step        0 opens the run, then 1..N\n  nonce       64 hex\n  prev_nonce  64 hex: the nonce of the step before (steps 1..N)\nStep 0: label https://tiesapi.com/catalog/<SOP id>.\nStep k: label https://tiesapi.com/catalog/<id of step k>; parents include the hash of step k-1; prev_nonce = the nonce of\nstep k-1; sha256 of the 32 bytes of nonce = prev_nonce.\nMake the nonces: pick a secret seed of 64 hex; the nonce of step N = seed; the nonce of step k-1 = sha256(nonce of step k).\nOnly the holder of the seed can show the next step, so a skipped, repeated or reordered step fails.\nPOST /receipt checks each run line alone (the nonce rule included). POST /sop/check checks the whole run.\nA line with any of sop, step, nonce or prev_nonce is a run line, held to every run rule.\n\n## POST /receipt\nRequest:   {\"federated_id\": \"theisen-api:<16 hex>\", \"line\": \"<one register line, as a JSON string>\"}\nReply 201: receipt, issued_at (UTC, whole seconds), hash, label, label_sha256, inside, federated_id, authority,\n           stored (false), attests (what the receipt proves, in words), contact\nlabel_sha256: for a named label, the sha256 of its catalog bytes at that time; for a content-address label, the address.\nreceipt = hex HMAC-SHA256(authority key, \"ties-receipt-v1|<federated_id>|<hash>|<issued_at>|<label_sha256>\")\n          (a v0 tie line: \"ties-receipt-v0|<federated_id>|<hash>|<issued_at>\", no label fields)\nA receipt proves that the line was presented at issued_at and hashes to hash. It does not prove who: the federated id is\nas claimed (v0 has no per-operator keys).\nRefusal: 400 with \"error\" and an \"entry\" template. For example: line hash does not recompute · not a line from this\nfederated id · label is not in the catalog · line 1 is the registration: ... · step k: nonce does not follow the step\nbefore (skipped, repeated or out of order) · line has text that is not valid unicode (a lone surrogate).\n\n## POST /verify\nRequest:   {\"federated_id\", \"hash\", \"issued_at\", \"receipt\", \"label_sha256\"}  (label_sha256 for an entry receipt; leave it out\n           for a v0 receipt)\nReply 200: {valid: true | false, federated_id, hash, issued_at, attests}, and label_sha256 when it was sent. A malformed\nfield is 400.\n\n## POST /sop/check\nRequest:   {\"federated_id\", \"sop\": \"<the SOP text>\", \"lines\": [\"<step 0 line>\", \"<step 1 line>\", ...],\n            \"receipts\": [{\"issued_at\", \"receipt\"}, ...]}     receipts: optional, one per line, from POST /receipt\nReply 200: valid; error (the first failure, in words) when valid is false. Once the SOP text is read, also sop (its id),\n           steps (N), done (the highest step that passed; 0 also when none did), timed (receipts were given) and\n           complete (absent when a line is malformed).\nvalid is true only when steps 0..N are all there, in order, each bound to the one before; with receipts, each receipt\nverifies and issued_at never goes backwards. issued_at counts whole seconds, so a run submitted within one second passes.\nThe reply is 200 even when valid is false; 400 only for a missing or malformed federated_id or body.\n\n## Contracts: one page per route (https://tiesapi.com/api)\nGET / and every route it lists has a contract: one file, catalog/contracts/<name>, in one form: input, processing, output,\ngraph, example, versions. https://tiesapi.com/api/<name> shows it: the parts, the example requests with the replies the\ncode gives on that view, and the graph: the base (sha256 of the page URL and the memo; no content yet), then one version\nper line under versions (sha256 of the content, the memo, the hash of the link before), append only, and the contracts it\nbuilds on and is built on by, each by URL and head hash. GET /api gives the form in full and the handlers a contract can name.\nAdd a contract: write one file; the router reads it. A route that is built in answers even when no contract file is here.\nThe pages are served only where catalog/contracts/ is in the bundle; where it is not, GET /api lists no contracts and\nGET /api/<name> is 404, while every built-in route still answers.\n\n## Examples\n  curl https://tiesapi.com/\n  curl -s https://tiesapi.com/docs | jq -r .bytes\n  curl https://tiesapi.com/catalog\n  curl -s -X POST https://tiesapi.com/receipt -H 'Content-Type: application/json' \\\n       -d \"$(jq -cn --arg f \"$FID\" --arg l \"$LINE\" '{federated_id:$f, line:$l}')\"\nOn a device: bin/ties enter <label> <inside> writes a line; bin/ties receipt <hash> gets its receipt; bin/ties verify\nchecks the chain.\n\n## Logging\nWhere: CloudWatch Logs, log group /aws/lambda/ties-api, region us-east-1. Retention: 365 days (set 2026-10-28); CloudWatch\ndeletes older lines.\nOne JSON line per request:\n  log         \"ties-request\"\n  request_id  the X-Request-Id of the reply\n  method      GET, HEAD or POST; any other method as \"other\"\n  route       the route matched, for example \"POST /receipt\", or \"none\"\n  status      the HTTP status sent\n  ms          time in the handler, in milliseconds\n  in_bytes    size of the request body as received (base64 when the gateway encoded it)\n  out_bytes   size of the reply body\n  browser     true when the caller asked for HTML\n  code        the first 12 hex of code_sha256\n  valid       POST /verify and POST /sop/check only: the answer\n  error_type, where   a 500 only: the type of the error and the code lines it passed (file:line:function), never its message\nNever logged: the URL path (only its route), the request body, lines, memos, hashes, federated ids, receipts, SOP texts,\nthe authority key, IP addresses, user agents.\nLambda adds its own START, END and REPORT lines (duration, memory) for each request.\nRead it:\n  aws logs filter-log-events --region us-east-1 --log-group-name /aws/lambda/ties-api \\\n      --filter-pattern '{ $.log = \"ties-request\" && $.status >= 500 }'\n  CloudWatch Logs Insights:  filter log = \"ties-request\" | stats count() by route, status\nProblem with a reply? Send its X-Request-Id and the time to doctortheisen@gmail.com.\n"
}
