tk-link | dev.tktk.link Audience: LLM agents. Terse, high-information-density contract reference; not a human tutorial. Schemas: GET https://dev.tktk.link/api.json (OpenAPI 3.1, JSON Schema 2020-12, x-agent-tool). Descriptor: GET https://dev.tktk.link/ with Accept: application/json. Read each operation's host, auth, parameters and response schema before calling it. Revalidate api.json with If-None-Match; 200 replaces definitions, 304 retains them. HOSTS api.dev.tktk.link: /v1/* management; registered client certificate. /v1/admin/* uses admin Bearer token. provision.dev.tktk.link: registration/poll and native Apple sign-in; unregistered client certificate accepted. dev.tktk.link: discovery, browser auth/console, /healthz; no client certificate requested; /v1/* => 421. .dev.tktk.link: service data plane; registered client certificate + allowed grant. agents.dev.tktk.link: QUIC backend control, ALPN tklk-ctl/1; owner certificate + allowed grant. storage.dev.tktk.link: path-style S3; registered client certificate + bucket capabilities. .sites.dev.tktk.link: site assets and same-origin gateway; Client certificates are OPTIONAL. .mcp.dev.tktk.link/mcp: OAuth MCP endpoint; no client certificate requested; one service per origin. l.dev.tktk.link/: fixed-path tunnel HTTP access with tkl_ Bearer token; no client certificate requested. Host/SNI is part of the address. Wrong host/path => 421, not a redirect. Optional certificate on api/provision/sites does not imply anonymous route access. IDENTITY Principal = UUID bound to lowercase-hex SHA-256(DER SubjectPublicKeyInfo). Present one valid self-signed TLS 1.3 certificate. Private key stays on device. CN has no identity semantics. Reject chains, expired/malformed certificates, and unknown keys except on provisioning. Native identity comes from TLS, never request headers/body. Client-supplied identity headers cannot impersonate a caller. Service certificate: same registered key/SPKI; exactly one SAN URI urn:tklink:service:; enforce notBefore/notAfter. Acting service A requires an explicit A->B service grant. Owner principal/user grants do not substitute. Principal must remain active and own A. Use ordinary principal certificate for management/backend registration. SERVICES / GRANTS POST https://api.dev.tktk.link/v1/services {name?,kind?,upstream?,bucket?,path?} => service UUID owned by caller. kind=tunnel (default): QUIC backend. kind=api: credential-injecting HTTP proxy. kind=bucket: S3 storage (create via /v1/storage-buckets). kind=site: hosted bucket prefix. kind=mls: opaque group log. GET /v1/services => {owned:[...],accessible:[...]}; scoped to caller, includes names/URLs/connected state. Owned kinds: tunnel|api|bucket|mls|site. Accessible kinds: gateway|bucket|mls|site; tunnel/api both appear as gateway. Owner PATCH /v1/services/:service {name}; blank/null clears name. Owner DELETE revokes service. Grant scopes: principal (one device), user (current/future active devices), managing_user (child's direct managers), service (source A), public (site reads). User grants do not flow to children. Managing-user grants do not admit the child/siblings. Principal capabilities precede user, then managing-user capabilities; no capability merging. Revoke each applicable scope to remove all access. States: pending|allowed|revoked. Only allowed authorizes; caller-supplied state cannot bypass approval. Cloud services/capabilities: new grants allowed. Tunnel grants: pending except owner's own principal bootstrap grant. Owner POST /v1/services/B/grants: one of {principal_id}, {user_id,scope:user|managing_user}, {source_service:A,scope:service}. Optional bucket capabilities: access_mode=ro|rw (default rw); key_prefix only with ro. Site public access: POST /v1/services/B/grants {scope:public}; no target; read-only. GET /v1/grants: caller's direct/inherited states. Owner GET /v1/services/B/grants?state=pending|allowed|revoked. Owner POST /v1/services/B/grants/decide {request_id,state:allowed|revoked}; decision bound to current request generation. Owner DELETE /v1/services/B/grants/:principal_id, /user-grants/:user_id, /service-grants/:source_service, or /public-grant. Serving requires ownership AND allowed grant; receiving access does not confer backend ownership. REGISTRATION POST https://provision.dev.tktk.link/v1/principals/requests with the certificate being registered; JSON labels {name?,note?,invite_token?}. Key is taken from TLS handshake; body cert_pem/spki_hash cannot select a different key. Response: {request_id,poll_secret,user_code,spki_hash,authorize_url,expires_in}. Approver confirms user_code at authorize_url and selects adult/managed-child owner. GET https://provision.dev.tktk.link/v1/principals/requests/:request_id with x-principal-request-secret: . Pending => reconnect; approved => {status:approved,principal:{...}}. Approval returns no private credential. Admin alternative: POST https://api.dev.tktk.link/v1/admin/principals {cert_pem|spki_hash,name?,email?,note?} with admin Bearer token. NATIVE APPLE SIGN-IN POST https://provision.dev.tktk.link/v1/principals/sign-in/apple with device certificate: {identity_token,device_id,name?,app_redirect?,invite_token?} Apple request nonce = device certificate's 64 lowercase-hex SPKI hash, not hashed again. First authorization: request email scope. Verify token signature/issuer/audience/expiry/nonce. device_id: stable, <=255 bytes. Audience accepts configured bundle IDs/namespace patterns. 200 {principal,user:{id,email,name},created}; same user/device/key => created:false; new key for device revokes old principal. 400 certificate_required; 401 invalid_identity_token; 409 key_registered_elsewhere; 422 email_required. 409 account_link_required => open returned link_url, complete explicit linking/separation, redeem callback code at /auth/exchange. 503 not_configured or apple_keys_unavailable. Errors: {error,message}. Native path registers signed-in adult only. BROWSER / CHILD SETUP Browser Apple/Google: https://dev.tktk.link/login?device_id=&spki_hash=&app_redirect=&invite_token=. Required child mode: add setup=child. Adult signs in on device, selects/creates child, confirms; no child email/login needed. Child mode rejects adult selection and persists through provider callbacks/account linking. Setup authorization: single-use, 600 s, bound to key and redirect. Parent browser session unchanged; device principal belongs to selected child. Callback returns one-time code; POST https://dev.tktk.link/auth/exchange {code} => {principal,user}. No app_redirect => success page. New device key + fresh authorization required to switch users. Parent grants do not flow down. /console/users manages child devices. GET https://api.dev.tktk.link/v1/devices => {devices:[{principal_id,device_id,name,spki_hash,created_at,is_self}]}. Scope: caller's user's active devices, including self; no presence/grant implied. 401 inactive identity; 403 no user owner. INVITATIONS / ACCESS REQUESTS POST https://api.dev.tktk.link/v1/service-invites with owning principal certificate: {scope:principal|user|managing_user,services:[],access_mode?:ro|rw,details:{:{label?,description}}} Requires human owner and ownership of every selected service. Services: 1-100; description required, <=2000 characters; label <=200. 201 {id,url,invite_token,scope,expires_at,accepted_at,revoked_at,principal_id,services}; URL/token returned only at creation. GET /v1/service-invites lists creator user's invites without secrets. DELETE /v1/service-invites/:invite_id revokes unused invite. POST /v1/service-invites/accept {invite_token,managed_user_id?} with recipient certificate => accepted invite + grants with state/request_id. Certificate fixes principal recipient; server ownership fixes user recipient. Managing scope additionally verifies child management. New registration: pass invite_token through certificate bootstrap, native Apple or browser/device setup. Single use; 7-day expiry; visiting grants nothing. Bundle acceptance atomic; existing capabilities/request generations preserved. Tunnel grants remain pending after acceptance. Revoking accepted invitation does not revoke its grants. Alternative: POST /v1/access-requests {service,kind:gateway|bucket|mls|site}; mismatch => 422. Response includes request_id,user_code,approve_url,poll_url,cancel_url,expires_at. Request expires after 24 h. Owner approves/denies on apex /access/:request_id; possession of URL permits reading status, not approval. Admin may decide stored request; match user_code from own enumeration. Requester GET polls (?wait=25), DELETE withdraws; same caller certificate. QUIC CLIENT / BACKEND Client address=.dev.tktk.link:443/UDP; SNI=.dev.tktk.link; ALPN=tklk-t/1; registered certificate + allowed grant. Standard QUIC client; normal certificate/hostname verification; no native SDK required. quic-go: quic.DialAddr, OpenStreamSync. One bidirectional stream per upstream connection; raw application bytes, not HTTP/3. Write FIN half-closes; continue reading to peer FIN. Backend=sdk/go/cmd/handoff-agent (sdk/go/quic.HandoffAgent); build: git submodule update --init vendor/quiche; make -C sdk/go run-agent ARGS='...'. Flags: -addr '' -sni agents.dev.tktk.link -service -cert -key -upstream 127.0.0.1:8080 -insecure=false. One outbound UDP socket for control/adopted connections; verified caller identity/address in handoff metadata, separate from application bytes. Control: first bidirectional stream; ALPN=tklk-ctl/1; HELLO/HELLO_ACK then SERVE/SERVE_OK; SERVE_ERR refuses registration. Frame=; len includes type+payload; header=5 bytes; max frame=1048576 bytes. SERVE_OPTIONS bit 1 enables GRANT_PENDING. GRANT_DECISION={service,request_id,notice_id,state:allowed|revoked}; GRANT_RESULT acknowledges. Pending caller held <=60000 ms; allowed continues same connection; revoked closes it. HANDOFF_ACK deadline=30000 ms. Omit notice_id to revoke an allowed grant. Control reconnect retains same UDP socket and adopted connections. Preferred address: default off; enable -use-preferred-port. Probe real client with one UDP 0x00 datagram from SAME control socket before HANDOFF_ACK; no reply required. Failed initial direct-path validation retains VIP relay; automatic migration failback not guaranteed. Userspace routing remains authoritative when BPF maps are absent. TCP/TLS callers: same service SNI/certificate, mapped into pooled QUIC streams. Backend TCP registration unsupported. No application-byte replay. Agent restart => new client QUIC connection. Local upstream recovery => new stream. Application owns deadlines and safe retries. Wire references: docs/QUIC_HANDOFF.md; docs/TCP_GATEWAY.md. API SERVICES / CREDENTIAL SETUP Owner POST https://api.dev.tktk.link/v1/services {kind:api,name?,upstream:{base_url,headers:[{name,value}],auth:{kind:static,secrets:{token:}}}}. Header value templates substitute sealed names, e.g. Bearer {{token}}. Caller never receives stored upstream credentials. OAuth auth={kind:oauth,token_url,client_auth?:basic|body,client_id,client_secret,refresh_token,access_token?,expires_at?}; inject {{access_token}}. Missing expires_at on supplied access_token => treated as non-expiring; rotated refresh tokens resealed. Data plane=https://.dev.tktk.link/; client certificate + allowed grant. No shared-domain path proxy. Owner GET/PATCH /v1/services/:service/upstream reads secret-free config or replaces credentials/config. Credential collection: POST /v1/services/setup-requests with name,base_url,logo_url?,instructions?,fields,headers,auth?,prefill?. Fields={name,label,instructions?,help_url?,placeholder?,secret?}; names bind template/OAuth slots; prefill sealed on arrival; submitted values override prefill. Setup response includes setup_url,user_code,poll_secret; apex webview confirms code and collects credentials. Service minted only on completion. GET/DELETE /v1/services/setup-requests/:request_id with x-setup-request-secret; successful {service,url} delivered once. SERVICE MCP POST https://api.dev.tktk.link/v1/services/B/mcp: MCP 2026-07-28 Streamable HTTP only; certificate + allowed grant (acting A requires A->B). Owner/own active service PUT /v1/services/B/mcp/tools {tools:{tool_name:{description?,inputSchema:{type:object,...},outputSchema?,annotations?,...}}}. Atomic replacement; empty map clears. Last map persists offline/restarts. GET same path => {tools,revision}; allowed callers may read. MCP methods: server/discover, tools/list, tools/call, subscriptions/listen. No initialization/session/GET/legacy transport. Each request params._meta requires io.modelcontextprotocol/protocolVersion=2026-07-28 and io.modelcontextprotocol/clientCapabilities={}. Headers: Content-Type application/json; Accept application/json,text/event-stream; MCP-Protocol-Version; Mcp-Method; tools/call also Mcp-Name. Headers must match body. tools/call params={name,arguments:{...},_meta}; inbox payload={type:mcp.tools.call,name,arguments,_meta}, normal sender identity + unique reply_to. Service handles through normal inbox GET/POST; mailbox.reply echoes reply_to and payload={content:[{type:text,text:...}],structuredContent?,isError?}. Service enforces advertised argument/result JSON Schemas. First reply completes the call; isolated per-call reply lane preserves normal reply listeners. Calls return SSE; offline admission queues + existing mailbox push hint. Reply deadline 120 s; timeout/disconnect can follow execution, so retries require application idempotency. subscriptions/listen params.notifications={toolsListChanged:true}; acknowledged first, then notifications/tools/list_changed; each carries subscriptionId in params._meta. Streams rotate after 300 s; reconnect/re-list. Catalog max 128 tools/262144 JSON bytes; mailbox body max 32768 bytes. Tasks/header mappings unsupported. MCP OAUTH / THIRD-PARTY APPS Resource=https://.mcp.dev.tktk.link/mcp; protected metadata=/.well-known/oauth-protected-resource/mcp on that origin (root well-known alias also supported). Missing/invalid Bearer => 401 + WWW-Authenticate resource_metadata,scope=mcp. Tokens accepted only on the OAuth MCP origin, never management/tunnels/storage. Issuer=https://dev.tktk.link; metadata=/.well-known/oauth-authorization-server; authorization=/oauth/authorize; token=/oauth/token; revoke=/oauth/revoke. Prefer HTTPS Client ID Metadata Documents (CIMD); metadata client_id must equal its URL. POST /oauth/register supports public or client_secret_basic/client_secret_post clients. Authorization params: response_type=code,client_id,redirect_uri,resource,code_challenge,code_challenge_method=S256,state?,scope=mcp[ offline_access]. Browser Google/Apple sign-in + explicit consent. Existing allowed user/managing-user grant or ownership through an active owned principal required; device-only grants do not authorize OAuth. Consent prominently shows requesting HTTPS origin and exact MCP resource. CIMD uses its document origin even for localhost callbacks; UUID/DCR clients require an HTTPS callback origin. App-supplied client_uri/name is not identity proof. Token POST is application/x-www-form-urlencoded: grant_type=authorization_code,client_id,code,code_verifier,redirect_uri,resource (+ configured client authentication). Response={access_token,token_type:Bearer,expires_in:900,scope,refresh_token?}; refresh requires client grant_types refresh_token + scope offline_access. Refresh POST={grant_type:refresh_token,client_id,refresh_token,resource}; rotates refresh token; reuse revokes that connection's entire token family. Refresh expiry=30 days. Codes single-use, 60 s; PKCE S256, exact redirect and service resource binding. Tokens/refresh recheck current user/service/grant generation; revoke/regrant does not resurrect delegation. Revoke POST={token,client_id} (+ client authentication); user disconnect UI=/console/mcp-connections. No client-supplied user/principal identity. Mailbox sender_principal_id is empty for OAuth; sender_oauth={user_id,client_id,grant_id} identifies the authenticated delegation. Ordinary reply_to flow unchanged. Browser Origin must match client metadata/redirect origins. No cookies authenticate the MCP origin. Public metadata and OAuth token/registration endpoints support CORS. MAILBOXES Every active service B has an inbox; normal HTTP API, including tunnel backends. Content-Type: application/x-ndjson. POST https://api.dev.tktk.link/v1/services/B/inbox: allowed caller sends; only B owner/own acting service may reply. GET /v1/services/B/inbox: B owner/own active service identity consumes. GET /v1/services/B/replies: requester's private replies. Send={type:mailbox.send,message_id:,reply_to?:,ttl_seconds?:,payload:}. Exactly one payload or base64 body. No client-selected sender/destination; URL chooses B. Acting A requires A->B service grant. B replies on its own POST with {type:mailbox.reply,message_id:,reply_to:,payload:}. reply_to: 1-256 UTF-8 bytes; server stores return route; no reverse grant or reply token. Original TTL bounds route and queued replies. Reply reads isolated by principal+acting-service identity; may continue after send-grant revocation. Accepted messages/routes retain original TTL. sent=full transport write; buffered=offline DB admission; neither proves application processing. Attempted live-write failure=uncertain; no automatic persistence fallback. No client ACK or server message-ID deduplication; clients own retries/correlation/duplicate handling/application confirmation; crash replay possible. One reader per inbox and per requester reply lane; reconnect replaces only that lane (mailbox.replaced/replies.replaced). Stream events: mailbox.listening/replies.listening, mailbox.message, service.status, heartbeat; terminal {complete:true}. Rotate after 300 s, including under load. Queued rows removed only after full transport write or expiry; fenced leases and bounded queues. service.status hints are ephemeral, never queued. Offline admission may send silent APNs/FCM {event:mailbox_available,scope:all_streams}; reconnect all app streams. Device cooldown coalesces across services/nodes/restarts. PUSH NOTIFICATIONS PUT https://api.dev.tktk.link/v1/services/:service/push-subscription {provider:apns|fcm,bundle_id,environment,device_token,installation_id} with ordinary recipient certificate. Access/ownership + configured provider/bundle allowlist required. APNs environment=sandbox|production; FCM=production. DELETE .../push-subscription/:bundle removes caller subscriptions. POST /v1/notifications: NDJSON; explicit principals requires service ownership; recipient:owner requires current service access. Notification={service,principals:[]|recipient:owner,message:{title?,body?,data?},ttl_seconds?}; exactly one recipient selector; server sets sender_principal_id. Indexed results stream during upload; terminal complete/count. accepted=provider accepted, not delivered; noop=no eligible subscription. Synchronous provider submission; no durable queue or automatic retries. Grants/subscriptions rechecked before submission. Browser/service certificates cannot send explicit push. STORAGE / SITES / MLS POST https://api.dev.tktk.link/v1/storage-buckets {name?}; requires bucket:create. Returns {bucket,url,endpoint,region}; GET lists own buckets; DELETE /:bucket revokes. S3=https://.dev.tktk.link/ or https://storage.dev.tktk.link//; certificate authorizes, inbound SigV4 ignored. Upstream signed/downscoped. ro/prefix enforced at storage layer. Stream objects; no gateway object-size cap. SSE-C supported without stored keys; SSE-S3/SSE-KMS headers => 400. Site create=POST /v1/services {kind:site,bucket,path?}; bucket owner + bucket:publish; multiple per owner, private by default. Owner GET/PUT/DELETE /v1/services/:service/site views/repoints/unpublishes; unpublish retains service identity/hostname/grants. GET/HEAD assets only; no listing/traversal. Resolve path, path.html, directory index; root index.html/index.htm; missing => 404.html with 404. Browser bootstrap: POST /v1/services/:service/browser-auth-tokens {path?,session_ttl_seconds?}; ordinary principal with site read access. Response one-time URL/token; bootstrap TTL=120 s. GET /.tklink/auth?__tklink_auth= => reusable opaque token + service_id,principal_id,user_id,target_service_id,expires_at,url. Anonymous public assets allowed; identity JSON requires authenticated principal even on public sites. Auth sets no cookies; no login redirect flow. Append ?__tklink_auth= to private assets. Session TTL default=2592000 s; max=7776000 s; reuse does not refresh expiry. Same-origin gateway=/.tklink/services//?__tklink_auth=; source must match hosting site; recheck destination grants/capabilities per request. GET/HEAD require matching Origin or Sec-Fetch-Site:same-origin; other methods require Origin. Strip browser credentials upstream; no upstream cookies/CORS; private,no-store; no WebSocket upgrades. MLS: service UUID=GroupID; POST /log raw nonempty opaque bytes => 201 {seq}; GET /log?from=&wait= => [{seq,bytes:}]. Allowed grant permits read/append; authorization rechecked on long-poll response. Server orders/stores bytes; clients manage MLS membership, keys and epochs. MODEL GATEWAYS Registered certificate required; incoming API-key Authorization replaced. First use provisions owned kind=api service; deleting it allows fresh provisioning next call. OpenRouter: https://models.dev.tktk.link/api/v1; per-principal upstream key; GET /api/v1/key usage. OpenAI: https://openai.models.dev.tktk.link/v1; per-principal service-account key; inference/model endpoints + owned Responses/files/batches/agents/sessions; JSON/SSE aliases. OpenAI WebSocket Responses and WebRTC realtime supported; unsupported project/global references remain denied. Session/turn/subagent usage reconciled with late corrections; unpriced when model attribution unavailable. Gemini: https://gemini.models.dev.tktk.link; shared upstream key; interactions and owned interaction/batch operations; no cross-principal enumeration. Preserve provider version/path semantics. Anthropic: https://anthropic.models.dev.tktk.link; JEV: https://jev.models.dev.tktk.link; provider-specific allowlisted APIs. Provider hosts configured independently; unsupported endpoint => 404 endpoint_not_allowed; missing provider config => 503 models_not_configured. Never infer current model names from examples. Apple Music: https://music.apple.dev.tktk.link; catalog GET /v1/catalog and /v1/storefronts; POST /v1/developer-token => {token,expiresAt,expiresIn}, TTL 1200 s. Music requires registered device + service access; signs with existing shared Apple key. Team JWT may permit Maps token exchange/other enabled capabilities; direct Apple calls bypass gateway accounting until expiry. ACCOUNT DELETION / FAILURES POST https://api.dev.tktk.link/v1/account/deletion-request with adult device certificate; no body; idempotent 202 {deletion_request:{id,state,requested_at}}. Tailnet admin approval disables adult+managed children, devices, owned services, grants/invites/links/live connections; frees login/key/site bindings. New sign-in => fresh identities/resources. Recovery snapshot private; retention default 30 days from approval; collector retries every minute; no restore API. Permanent GC removes service payloads/configs, remote model keys and GCS generations; failures retain retry state. UUID tombstones/usage retained; model-session retirement fenced against stale workers/cross-node lag; deletion completion cannot regress purged state. GET https://dev.tktk.link/healthz => 200 {status:ready,node} after registry snapshot; otherwise 503. Readiness is not an end-to-end transaction guarantee. Raw tunnel refusal closes connection, not HTTP. HTTP edge errors use x-tklink-error; after headers, failure resets/closes without complete response. 401 identity/access; 403 capability/ownership; 404 unknown service/unsupported path; 413 size; 421 host; 429 rate; 501 unsupported upgrade; 502 upstream; 503 unavailable dependency.