{"openapi":"3.1.0","info":{"title":"TLearth Public API","version":"1.0.0","description":"Public, permission-scoped endpoints exposed by TLearth. All state changes go through authenticated server functions; public routes are read-only unless documented otherwise."},"servers":[{"url":"/"}],"paths":{"/api/public/verify":{"get":{"summary":"Verify a signed ledger event","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Verification result with inclusion proof"}}}},"/api/public/search":{"get":{"summary":"Public hybrid search (BM25 + trigram)","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","maxLength":200}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50}},{"name":"kinds","in":"query","schema":{"type":"string","description":"Comma-separated kinds"}}],"responses":{"200":{"description":"Ranked hits"}}}},"/api/public/answer":{"post":{"summary":"Public RAG answer with numbered citations","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":3,"maxLength":500},"k":{"type":"integer","minimum":1,"maximum":12,"default":6},"kinds":{"type":"array","items":{"type":"string"},"maxItems":20}}}}}},"responses":{"200":{"description":"Answer + citations"},"402":{"description":"Workspace out of AI credits"},"429":{"description":"Rate limited"},"502":{"description":"Upstream AI error"}}}},"/api/public/quota":{"get":{"summary":"Anonymous rate-limit policy and window position","responses":{"200":{"description":"Policy JSON; X-RateLimit-* headers included"}}}},"/api/public/merkle":{"get":{"summary":"Latest Merkle roots for anchoring","responses":{"200":{"description":"Roots list"}}}},"/api/public/chat":{"post":{"summary":"Streaming chat (SSE) via the router","responses":{"200":{"description":"text/event-stream"}}}},"/api/public/anchors":{"get":{"summary":"External anchor receipts (signed root digests)","description":"Each receipt carries a canonical `document`, its SHA-256 `digest`, and a DETACHED Ed25519 `signature` (base64url) produced over the canonical JSON of `document`. Verify offline without trusting this server.","parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]}},{"name":"id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Receipts as JSON or CSV"}}}},"/api/public/revocations":{"get":{"summary":"Revocation channel (tombstones)","description":"Every erasure request and its decision. Approved entries form their own hash chain: tombstone_hash = SHA-256(prev_tombstone_hash || canonical(entry)). Original payload hashes are never altered, so anchored roots keep verifying.","parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"Revocation entries"}}}},"/api/public/mcp":{"get":{"summary":"MCP tool manifest for these endpoints","responses":{"200":{"description":"Manifest with tool schemas and SDK snippets"}}}},"/api/public/webhooks/dispatch":{"post":{"summary":"Webhook delivery worker tick (cron only)","description":"Drains due webhook deliveries, performs one signed HTTP attempt each, then either marks them delivered, schedules an exponential-backoff retry, or dead-letters them. Requires the project apikey header.","responses":{"200":{"description":"Drain summary"},"401":{"description":"Missing or wrong apikey"}}}},"/api/public/sdk/{language}":{"get":{"summary":"Versioned client SDK generated from this spec","description":"Deterministically generated from the live OpenAPI document. Returns the SDK source; `?meta=1` returns version, spec hash, artifact hash, signed request examples, and runnable tests.","parameters":[{"name":"language","in":"path","required":true,"schema":{"type":"string","enum":["typescript","python","curl"]}},{"name":"meta","in":"query","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"SDK source or metadata"},"404":{"description":"Unsupported language"}}}},"/api/public/digest/run":{"post":{"summary":"Digest scheduler tick (cron only)","description":"Processes due digest schedules and writes a hashed delivery attestation per channel. Requires the project apikey header.","responses":{"200":{"description":"Per-job delivery report"},"401":{"description":"Missing or wrong apikey"}}}}},"x-signed-requests":{"description":"Outbound webhooks carry `X-TLearth-Signature: t=<unix>,v1=<hex>` where the HMAC-SHA256 message is `${t}.${rawBody}` keyed by the endpoint signing secret, plus `X-TLearth-Event`, `X-TLearth-Delivery`, and `X-TLearth-Attempt`. Anchor receipts use detached Ed25519 over canonical JSON.","verify_webhook_node":"import { createHmac, timingSafeEqual } from 'crypto';\nconst [tPart, vPart] = req.headers['x-aios-signature'].split(',');\nconst t = tPart.slice(2), v1 = vPart.slice(3);\nif (Math.abs(Date.now() / 1000 - Number(t)) > 300) throw new Error('stale timestamp');\nconst expected = createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');\nif (!timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) throw new Error('bad signature');","verify_anchor_web_crypto":"const key = await crypto.subtle.importKey('raw', b64u(receipt.signer_pubkey), { name: 'Ed25519' }, false, ['verify']);\nconst ok = await crypto.subtle.verify({ name: 'Ed25519' }, key, b64u(receipt.signature), new TextEncoder().encode(canonicalJSON(receipt.document)));"},"x-sdk-snippets":{"curl":"curl -s \"/api/public/search?q=agents&limit=5\"","typescript":"const res = await fetch('/api/public/answer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ question: 'What is TLearth?' }) });\nconst { answer, citations } = await res.json();","python":"import requests\nprint(requests.post(BASE + '/api/public/answer', json={'question': 'What is TLearth?'}).json())","mcp":"Point any MCP-capable assistant at /api/public/mcp to auto-discover these tools."}}