{"openapi":"3.1.0","info":{"title":"Tillpad","description":"Bounded storage and search for agent jobs. Metered KVP, memory conventions (prefs/facts/run), RAG, receive-only email inboxes, scheduled HTTPS jobs, run keys with wipe receipts. Authenticate with Bearer API key (tp_…) or session JWT.","version":"0.1.0"},"servers":[{"url":"https://tillpad.dev"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key (tp_…) or session JWT"}}},"security":[{"bearerAuth":[]}],"paths":{"/health":{"get":{"security":[],"summary":"Health check","responses":{"200":{"description":"OK"}}}},"/api/config":{"get":{"security":[],"summary":"Public client config","responses":{"200":{"description":"Config JSON"}}}},"/api/auth/oidc/start":{"post":{"security":[],"summary":"Start CorroborateMe OIDC sign-in","description":"Returns an authorize URL for the configured CorroborateMe OIDC client. Required for human login when ENVIRONMENT≠local.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"next":{"type":"string","description":"Post-login SPA path (must start with /)"}}}}}},"responses":{"200":{"description":"{ url, state }"},"503":{"description":"OIDC not configured"}}}},"/api/auth/oidc/callback":{"get":{"security":[],"summary":"OIDC redirect callback","description":"Exchanges the authorization code, verifies JWKS, links the Tillpad account, sets session cookie, redirects to /login?cm=1&exch=…","responses":{"302":{"description":"Redirect to SPA"}}}},"/api/auth/oidc/session":{"post":{"security":[],"summary":"Exchange one-shot OIDC code for Tillpad session JWT","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"}},"required":["code"]}}}},"responses":{"200":{"description":"{ token }"},"401":{"description":"Invalid or expired code"}}}},"/api/auth/request-otp":{"post":{"security":[],"summary":"Request email OTP (ENVIRONMENT=local only)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}}},"responses":{"200":{"description":"OTP (local returns devCode)"},"404":{"description":"Non-local — use OIDC"}}}},"/api/auth/verify-otp":{"post":{"security":[],"summary":"Verify OTP and issue session (ENVIRONMENT=local only)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"code":{"type":"string"}},"required":["email","code"]}}}},"responses":{"200":{"description":"Token + account"},"404":{"description":"Non-local — use OIDC"}}}},"/api/auth/logout":{"post":{"security":[],"summary":"Clear Tillpad session cookie; when OIDC was used, may return endSessionUrl for CorroborateMe RP-initiated logout","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"postLogoutRedirectUri":{"type":"string","format":"uri","description":"Optional same-origin return URL for CorroborateMe post_logout_redirect_uri. When omitted, defaults to {APP_PUBLIC_URL}/login. Non-same-origin values are ignored."}}}}}},"responses":{"200":{"description":"{ ok, endSessionUrl? } - browser should top-level navigate to endSessionUrl when present"}}}},"/api/agents/bootstrap":{"post":{"security":[],"summary":"Zero-human agent onboarding. Every email gets the same HTTP 202 verification_required answer and a 6-digit code sent to that inbox (10 minutes, single use). Repeat the call with email and code to get bootstrapToken. No account is created until the code is verified.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"label":{"type":"string"},"code":{"type":"string","description":"6-digit code from the verification email, after a 202 verification_required response"}},"required":["email"]}}}},"responses":{"200":{"description":"{ ok, bootstrapToken, accountId, expiresIn, next } - only with a valid code"},"202":{"description":"{ ok: false, verificationRequired: true, code: verification_required, expiresIn, next } - code emailed to the address; same answer for every email"},"400":{"description":"invalid_email"},"401":{"description":"invalid_verification_code (wrong, expired, or already used)"},"429":{"description":"rate_limited"},"503":{"description":"email_unavailable"}}}},"/api/me":{"get":{"summary":"Current account + usage","responses":{"200":{"description":"Account"}}}},"/api/usage":{"get":{"summary":"Usage snapshot + budget view","responses":{"200":{"description":"Usage"}}}},"/api/usage/periods":{"get":{"summary":"List stored usage periods (newest first)","description":"Returns meter totals per calendar month (YYYY-MM). Optional cursor is an exclusive YYYY-MM for pagination.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"description":"Return periods older than this YYYY-MM"}],"responses":{"200":{"description":"Period list"},"400":{"description":"invalid_cursor"}}}},"/api/usage/periods/{periodYm}":{"get":{"summary":"Usage snapshot for a specific calendar period","parameters":[{"name":"periodYm","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"}}],"responses":{"200":{"description":"Usage for period"},"400":{"description":"invalid_period"}}}},"/api/budget":{"get":{"summary":"Budget view (remaining quotas + checkout URL)","responses":{"200":{"description":"Budget"}}}},"/api/budget/estimate":{"post":{"summary":"Estimate whether an operation would 402/429","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["kvp_ops","storage_bytes","rag_index","rag_query","inbound_email","outbound_http"]},"amount":{"type":"number"},"textLength":{"type":"number"},"byteLength":{"type":"number"}},"required":["kind"]}}}},"responses":{"200":{"description":"Estimate"}}}},"/api/keys":{"get":{"summary":"List API keys (session or account key)","responses":{"200":{"description":"Keys"}}},"post":{"summary":"Create API key (account|run|sub) with optional ACL","description":"Session JWT or full-access account key required. Custom fields: name, kind, namespaces, tools, ttlSeconds, opBudget, wipeOnExpire. Omit namespaces on run keys to auto-assign a private folder. Omit tools/namespaces on sub keys for unrestricted access on that dimension.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Optional display label"},"kind":{"type":"string","enum":["account","run","sub"],"description":"account=full access; run=temporary session; sub=limited"},"ttlSeconds":{"type":"number","description":"Lifetime in seconds; run defaults to 24h if omitted"},"namespaces":{"type":"array","items":{"type":"string"},"description":"Folder allow-list; run with empty list gets an auto folder"},"tools":{"type":"array","items":{"type":"string"},"description":"Tool allow-list (MCP/REST tool names); omit for all tools"},"opBudget":{"type":"number","description":"Max metered ops for this key; omit for unlimited"},"wipeOnExpire":{"type":"boolean","description":"When true, clear namespaces and issue a wipe receipt on expiry (default true for run)"}}}}}},"responses":{"200":{"description":"Key + secret once"}}}},"/api/keys/{id}":{"delete":{"summary":"Revoke API key (session or account key)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"}}}},"/api/runs/{keyId}/finish":{"post":{"summary":"Wipe run-key namespaces and return signed wipe receipt","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Receipt"}}}},"/api/wipe-receipts/{id}":{"get":{"summary":"Fetch wipe receipt and verify signature","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Receipt"}}}},"/api/billing/checkout":{"post":{"summary":"Create Stripe Checkout URL (human recurring subscription)","responses":{"200":{"description":"{ url }"}}}},"/api/billing/machine-pay":{"post":{"summary":"Unlock Pro or purchase agent SKUs via Stripe MPP. Accepts session JWT, tp_ key, or bootstrap JWT. Optional sku (default pro_prepaid_30d). Returns 402 Payment challenge without credential; prepaid period + account tp_ secret on bootstrap settle.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","enum":["pro_prepaid_30d","pro_prepaid_90d","topup_kvp_10k","topup_storage_1gb"]}}}}}},"responses":{"200":{"description":"{ ok, planStatus, planPeriodEnd, secret? (bootstrap first settle) }"},"402":{"description":"MPP Payment challenge"},"503":{"description":"MPP not configured"}}}},"/api/billing/portal":{"post":{"summary":"Create Stripe Customer Portal URL","responses":{"200":{"description":"{ url }"}}}},"/api/billing/invoices":{"get":{"summary":"List Stripe invoices and charges for the account customer","parameters":[{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"starting_after","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ items, hasMore, stripeConfigured }"},"503":{"description":"Stripe not configured"}}}},"/api/billing/purchases":{"get":{"summary":"List local billing purchase log (MPP + webhook events)","parameters":[{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"cursor","in":"query","schema":{"type":"string"}},{"name":"include_stripe","in":"query","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"{ purchases, hasMore }"}}}},"/api/billing/webhook":{"post":{"security":[],"summary":"Stripe webhook (signature required)","responses":{"200":{"description":"Received"}}}},"/api/kvp/{namespace}":{"get":{"summary":"List KVP keys in namespace","parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Keys page"}}}},"/api/kvp/{namespace}/{key}":{"put":{"summary":"Put KVP value","parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Stored"}}},"get":{"summary":"Get KVP value","parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Value"}}},"delete":{"summary":"Delete KVP key","parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/files/types":{"get":{"summary":"Supported RAG upload file types","description":"Public capability catalog: extensions, MIME types, and extract notes for text/PDF/Excel.","responses":{"200":{"description":"Supported types catalog"}}}},"/api/files/{namespace}":{"post":{"summary":"Upload file for RAG indexing","description":"Accepts text, PDF, and Excel (.xls/.xlsx). Other binaries (images, Word, PPT, archives) return 415. PDF/Excel are text-extracted then embedded.","parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"201":{"description":"File row"},"415":{"description":"Unsupported media type"}}}},"/api/files":{"get":{"summary":"List files","parameters":[{"name":"namespace","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Files"}}}},"/api/files/{id}":{"get":{"summary":"Get file metadata","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"File"}}},"delete":{"summary":"Delete file + vectors","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/rag/search":{"post":{"summary":"Semantic RAG search","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"namespace":{"type":"string"},"topK":{"type":"integer"}},"required":["query"]}}}},"responses":{"200":{"description":"Hits"}}}},"/api/rag/notes":{"post":{"summary":"Index a plain-text note for RAG","description":"JSON body with text (and optional title). Same chunk/embed/Vectorize pipeline as multipart file upload; no FormData required.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"namespace":{"type":"string"},"text":{"type":"string"},"title":{"type":"string"}},"required":["namespace","text"]}}}},"responses":{"201":{"description":"File row + estimate"},"402":{"description":"Subscription required"},"415":{"description":"Unsupported / binary text"},"429":{"description":"rag_index hard cap"}}}},"/api/memory/{scope}/{key}":{"put":{"summary":"Store a memory convention value (KVP)","description":"Scopes: prefs, facts, run. Run scope maps to the run key's bound namespace when using a run key.","parameters":[{"name":"scope","in":"path","required":true,"schema":{"type":"string","enum":["prefs","facts","run"]}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"string"},{"type":"object","properties":{"value":{"type":"string"}}}]}},"text/plain":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"Stored"}}},"get":{"summary":"Read a memory convention value","parameters":[{"name":"scope","in":"path","required":true,"schema":{"type":"string","enum":["prefs","facts","run"]}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Value"}}}},"/api/memory/search":{"post":{"summary":"Semantic search with optional memory scope filter","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"scope":{"type":"string","enum":["prefs","facts","run"]},"topK":{"type":"integer"}},"required":["query"]}}}},"responses":{"200":{"description":"Hits"}}}},"/api/inspect/namespaces":{"get":{"summary":"Namespace inventory","responses":{"200":{"description":"Namespaces"}}}},"/api/namespaces/{name}":{"delete":{"summary":"Wipe a folder (KVP, files, vectors) — session or account key","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted counts"},"403":{"description":"Not allowed"},"404":{"description":"Unknown folder"}}}},"/api/inspect/reconcile-storage":{"post":{"summary":"Reconcile storage meter","responses":{"200":{"description":"Storage bytes + usage"}}}},"/api/support/contact":{"post":{"summary":"Contact Tillpad support (Pro)","description":"Sends a support message to the Tillpad team on behalf of an active Pro account. Reply-To is the account email. Limited to a small number of messages per account per UTC day. Does not consume metered quotas.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subject","message"],"properties":{"subject":{"type":"string","maxLength":200,"description":"Short subject line"},"message":{"type":"string","maxLength":10000,"description":"Support message body"}}}}}},"responses":{"200":{"description":"Message accepted"},"400":{"description":"Validation error"},"402":{"description":"Active Pro subscription required"},"429":{"description":"Daily support contact limit reached"}}}},"/api/inboxes":{"get":{"summary":"List receive-only inboxes","responses":{"200":{"description":"{ inboxes }"}}},"post":{"summary":"Create receive-only inbox","description":"Temporary or permanent address on INBOUND_EMAIL_DOMAIN. Meters inbound_email on receive.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["localPart","kind"],"properties":{"localPart":{"type":"string"},"kind":{"type":"string","enum":["temporary","permanent"]},"ttlSeconds":{"type":"integer"},"domain":{"type":"string"}}}}}},"responses":{"200":{"description":"{ inbox, address }"},"402":{"description":"subscription_required"}}}},"/api/inboxes/audit":{"get":{"summary":"Inbox audit log","parameters":[{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Audit entries"}}}},"/api/inboxes/blocklist":{"get":{"summary":"List blocked sender addresses and domains","responses":{"200":{"description":"{ entries }"}}},"post":{"summary":"Block a sender address or domain","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["kind","value"],"properties":{"kind":{"type":"string","enum":["address","domain"]},"value":{"type":"string"}}}}}},"responses":{"200":{"description":"{ entry }"}}}},"/api/inboxes/blocklist/{id}":{"delete":{"summary":"Remove blocklist entry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok: true }"}}}},"/api/inboxes/webhooks":{"get":{"summary":"List email webhooks","responses":{"200":{"description":"{ webhooks }"}}},"post":{"summary":"Register email.received webhook","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string"}}}}}},"responses":{"200":{"description":"{ webhook, secret }"}}}},"/api/inboxes/webhooks/failures":{"get":{"summary":"Recent webhook delivery failures","description":"Deliveries that failed after all retry attempts (max 3).","parameters":[{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"offset","in":"query","schema":{"type":"integer"}},{"name":"webhookId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ failures, limit, offset }"}}}},"/api/inboxes/webhooks/deliveries":{"get":{"summary":"Webhook delivery log","parameters":[{"name":"status","in":"query","schema":{"type":"string"}},{"name":"webhookId","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"{ deliveries, limit, offset }"}}}},"/api/inboxes/webhooks/{id}":{"delete":{"summary":"Disable email webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok: true }"}}}},"/api/inboxes/{id}":{"get":{"summary":"Get inbox","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ inbox }"}}},"delete":{"summary":"Delete inbox and purge messages","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok: true }"}}}},"/api/inboxes/{id}/messages":{"get":{"summary":"List inbox messages (metadata)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"{ messages }"}}}},"/api/inboxes/{id}/messages/{msgId}":{"get":{"summary":"Get message metadata and attachments","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"msgId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ message, attachments }"}}}},"/api/inboxes/{id}/messages/{msgId}/raw":{"get":{"summary":"Download raw MIME (meters 1 kvp_op)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"msgId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"message/rfc822 body"}}}},"/api/inboxes/{id}/messages/{msgId}/attachments/{attachmentId}":{"get":{"summary":"Download attachment (meters 1 kvp_op)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"msgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Attachment bytes"}}}},"/api/schedules":{"get":{"summary":"List account schedules","responses":{"200":{"description":"{ schedules }"}}},"post":{"summary":"Create HTTPS interval schedule","description":"Periodically GET/POST a URL. Contract: 10s timeout/attempt, up to 3 attempts with exponential backoff (cap 300s), success=2xx; meters outbound_http per attempt and per notify_webhook action; auto-disables after consecutive exhausted failures (default 5). Min interval 5 minutes. Optional onSuccess/onFailure: store_kvp, store_rag, notify_webhook.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["url","intervalMinutes"],"properties":{"name":{"type":"string"},"url":{"type":"string","format":"uri"},"method":{"type":"string","enum":["GET","POST"]},"bodyTemplate":{"type":"string"},"authMode":{"type":"string","enum":["none","bearer","header"]},"authHeaderName":{"type":"string"},"authSecret":{"type":"string"},"intervalMinutes":{"type":"integer","minimum":5},"timezone":{"type":"string"},"onSuccess":{"type":"array","items":{"type":"object","additionalProperties":true}},"onFailure":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"responses":{"200":{"description":"{ schedule, contract }"},"400":{"description":"Validation / schedule limit"},"402":{"description":"subscription_required"}}}},"/api/schedules/runs":{"get":{"summary":"List schedule run attempts","description":"Use status=failed for exhausted failures; check consecutiveFailures / disabledReason on the schedule.","parameters":[{"name":"scheduleId","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","running","retrying","failed","delivered","skipped"]}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"{ runs, limit, offset }"}}}},"/api/schedules/{id}":{"get":{"summary":"Get one schedule","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ schedule, contract }"},"404":{"description":"Not found"}}},"delete":{"summary":"Disable/delete a schedule","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok: true }"},"404":{"description":"Not found"}}}}}}