{"openapi":"3.1.0","info":{"title":"DrillFlow API","version":"0.1.0"},"paths":{"/api/auth/register":{"post":{"summary":"Register","operationId":"register_api_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/login":{"post":{"summary":"Login","operationId":"login_api_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/logout":{"post":{"summary":"Logout","operationId":"logout_api_auth_logout_post","responses":{"204":{"description":"Successful Response"}}}},"/api/auth/me":{"get":{"summary":"Me","operationId":"me_api_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}}},"/api/auth/verify-email":{"post":{"summary":"Verify Email","operationId":"verify_email_api_auth_verify_email_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/resend-verification":{"post":{"summary":"Resend Verification","operationId":"resend_verification_api_auth_resend_verification_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Resend Verification Api Auth Resend Verification Post"}}}}}}},"/api/auth/request-password-reset":{"post":{"summary":"Request Password Reset","operationId":"request_password_reset_api_auth_request_password_reset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Request Password Reset Api Auth Request Password Reset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/reset-password":{"post":{"summary":"Reset Password","operationId":"reset_password_api_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/change-password":{"post":{"summary":"Change Password","description":"A new password while signed in. Cookie-only like delete-account: an API\ntoken lives in an agent's config and must not be able to take the account\nover. The current password is required, every OTHER session ends (the\nepoch moves), this one gets a fresh cookie, and the owner gets a mail.","operationId":"change_password_api_auth_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/sign-out-others":{"post":{"summary":"Sign Out Others","description":"End every session but this one — the lost-phone button. API tokens are\nnot sessions and stay valid; they have their own expiry and revoke.","operationId":"sign_out_others_api_auth_sign_out_others_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}}},"/api/my/links":{"get":{"summary":"My Links","operationId":"my_links_api_my_links_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/LinkInfo"},"type":"array","title":"Response My Links Api My Links Get"}}}}}}},"/api/my/links/{slug}":{"delete":{"summary":"Revoke Link","operationId":"revoke_link_api_my_links__slug__delete","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Update Link","description":"Managed-link knobs (paid B3 + C1/C1b, split for free accounts in C1d).\n\nTwo rules stack here.\n\nWHICH KNOB (C1d): expiry and \"viewers may copy\" are free for any signed-in\nowner. An expiry is hygiene — anonymous links carry one anyway — and the\ncopy switch is documented as a request rather than a barrier, so charging\nfor it would be selling something we ourselves call unenforceable. A\npassword IS enforced, a readable name claims a public namespace, and the\nview count grows with use: those three stay paid.\n\nSETTING vs REMOVING (C1c): taking a paid knob off never needs the plan.\nOtherwise an account whose subscription lapses could not remove the password\nit set itself — one support case per cancellation.","operationId":"update_link_api_my_links__slug__patch","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/delete-account":{"post":{"summary":"Delete Account","description":"Right-to-erasure: password re-auth, then the account and its owned\nshares are gone. The session cookie is cleared in the same response.\n\nCookie-only (C2): an API token must not be able to erase the account it was\nminted from.","operationId":"delete_account_api_auth_delete_account_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountRequest"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/delete-account/request":{"post":{"summary":"Request Account Deletion","description":"The passwordless account's way to confirm deletion (C7): a one-time\nlink to the account's own address. It replaced a Google re-auth, which\nGoogle does not reliably enforce — it waved the confirmation through\nwithout asking for anything. Cookie-only like delete-account itself.","operationId":"request_account_deletion_api_auth_delete_account_request_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Request Account Deletion Api Auth Delete Account Request Post"}}}}}}},"/api/auth/delete-account/confirm":{"post":{"summary":"Confirm Account Deletion","description":"The link from that mail. No session needed — the mail may be opened on\nanother device, and holding the mailbox is the proof. Reached by a button\non the app's page, never by the GET a mail scanner prefetches.","operationId":"confirm_account_deletion_api_auth_delete_account_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/handle":{"patch":{"summary":"Update Handle","description":"Claim the account's handle, or move to another one (C1b).\n\nThere is deliberately no \"is this free?\" endpoint: it would be a perfect\nenumeration oracle for other people's handles. The answer comes from this\ncall, which sits behind the session and the rate limit.\n\nA handle the account gives up is remembered. It returns to the pool only if\nnothing was ever published under it — otherwise a stranger could take it and\nrevive its dead URLs with their own diagrams. Its former owner may always\ntake it back.\n\nCookie-only (C2): moving the handle kills every named URL under the old one,\nwhich is not something a token in an agent's config should be able to do.","operationId":"update_handle_api_my_handle_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandleUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/tokens":{"get":{"summary":"My Tokens","operationId":"my_tokens_api_my_tokens_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiTokenInfo"},"type":"array","title":"Response My Tokens Api My Tokens Get"}}}}}},"post":{"summary":"Create Api Token","operationId":"create_api_token_api_my_tokens_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiTokenCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/tokens/{token_id}":{"delete":{"summary":"Revoke Api Token","description":"No entitlement check on purpose (C1c): a lapsed plan must still be able\nto take its own tokens out of circulation.","operationId":"revoke_api_token_api_my_tokens__token_id__delete","parameters":[{"name":"token_id","in":"path","required":true,"schema":{"type":"integer","title":"Token Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/docs":{"get":{"summary":"My Cloud Docs","operationId":"my_cloud_docs_api_my_docs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CloudDocInfo"},"type":"array","title":"Response My Cloud Docs Api My Docs Get"}}}}}}},"/api/my/docs/usage":{"get":{"summary":"My Cloud Usage","description":"What is stored against what the plan allows — /account shows it, and the\nclient checks it before it starts pushing a big document.","operationId":"my_cloud_usage_api_my_docs_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloudUsage"}}}}}}},"/api/my/docs/search":{"get":{"summary":"Search Cloud Library","description":"Find a shape, a note or a level anywhere in the library (C5b).\n\nDeclared ABOVE `/api/my/docs/{doc_id}`, or the path parameter swallows the\nword \"search\" — the same ordering `my_cloud_usage` depends on.\n\nThis one route is the DELIBERATE exception to the rule two notes above:\neverything else that reads the library is free, and stays free, so\ncancelling never costs anybody access to their own work — the list, the\ndownload, the history and the removal all answer a lapsed account. Search\nis the part the archive is sold for, and the owner decided (2026-10-02)\nthat it is the paid surface. It takes nothing away that a free account\ncould do before: without the plan there is no way to put a document in\nthe library in the first place.","operationId":"search_cloud_library_api_my_docs_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":100,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":30,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CloudSearchHit"},"title":"Response Search Cloud Library Api My Docs Search Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/docs/{doc_id}":{"get":{"summary":"My Cloud Doc","operationId":"my_cloud_doc_api_my_docs__doc_id__get","parameters":[{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloudDocBody"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Save Cloud Doc","description":"Write a new state, or refuse it as stale.\n\nWriting is the half that needs the plan (see the note above). The refusal\nis a 409 carrying what the server actually holds, so the client can show\nthe human who wrote last instead of quietly picking a winner.","operationId":"save_cloud_doc_api_my_docs__doc_id__put","parameters":[{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloudDocWrite"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloudDocInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Cloud Doc","description":"A tombstone, not a DELETE: a device that was offline has to be able to\nlearn the document is gone, and a row that simply vanished looks exactly\nlike one it has never seen.","operationId":"delete_cloud_doc_api_my_docs__doc_id__delete","parameters":[{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/blobs/missing":{"post":{"summary":"My Missing Blobs","operationId":"my_missing_blobs_api_my_blobs_missing_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlobCheck"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Response My Missing Blobs Api My Blobs Missing Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/blobs/{digest}":{"put":{"summary":"My Put Blob","operationId":"my_put_blob_api_my_blobs__digest__put","parameters":[{"name":"digest","in":"path","required":true,"schema":{"type":"string","title":"Digest"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"My Blob","operationId":"my_blob_api_my_blobs__digest__get","parameters":[{"name":"digest","in":"path","required":true,"schema":{"type":"string","title":"Digest"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/docs/{doc_id}/history":{"get":{"summary":"My Doc History","operationId":"my_doc_history_api_my_docs__doc_id__history_get","parameters":[{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotInfo"},"title":"Response My Doc History Api My Docs  Doc Id  History Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/docs/{doc_id}/history/{snapshot_id}":{"get":{"summary":"My Doc Snapshot","operationId":"my_doc_snapshot_api_my_docs__doc_id__history__snapshot_id__get","parameters":[{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}},{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"integer","title":"Snapshot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotBody"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/my/prefs":{"get":{"summary":"My Prefs","operationId":"my_prefs_api_my_prefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefsResponse"}}}}}},"put":{"summary":"Save My Prefs","description":"Last write wins. A settings value is not worth a conflict dialogue —\nwhoever set one most recently meant it, and the client only sends after a\nreal change.","operationId":"save_my_prefs_api_my_prefs_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefsBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/checkout":{"post":{"summary":"Create Checkout","description":"Start a hosted Stripe Checkout for a plan.\n\nCookie-only (`require_session_user`), like delete-account and the handle\nmove: this attaches a payment method to the account, and an API token —\nwhich lives in an agent's config file — must not be able to buy anything.","operationId":"create_checkout_api_billing_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingSession"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/portal":{"post":{"summary":"Create Portal","description":"Stripe's own billing portal: cancel, switch interval, change card, get\ninvoices. Building any of that here would be rebuilding it worse.\n\n`flow` opens a specific part of it (C9): `cancel` lands on the\ncancellation of the live subscription, `payment` on changing the card.","operationId":"create_portal_api_billing_portal_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PortalRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingSession"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/summary":{"get":{"summary":"Billing Summary","description":"What the Plan section shows (C9 round 3): the subscription's state and\nthe last invoices, read live from Stripe. Cookie-only like the portal —\ninvoices carry a name and an address. An account that never checked out\ngets an empty summary, not an error.","operationId":"billing_summary_api_billing_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingSummary"}}}}}}},"/api/billing/webhook":{"post":{"summary":"Webhook","description":"Stripe's side of the conversation.\n\nNo rate limit on purpose: the limiter carries no default limits (see\n`api/rate.py`), and putting one here would turn a burst of renewals into\n429s, which Stripe reads as an outage and answers with more retries.\n\nThe signature IS the authentication — the raw body must be verified before\nanything in it is believed. An unknown event type, an unknown price and an\nunknown customer are all 200: a non-2xx makes Stripe retry for days, and\nnone of those three will ever succeed on a second try.","operationId":"webhook_api_billing_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Webhook Api Billing Webhook Post"}}}}}}},"/api/auth/providers":{"get":{"summary":"Providers","description":"Which sign-in buttons the app may show.","operationId":"providers_api_auth_providers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Providers Api Auth Providers Get"}}}}}}},"/api/auth/oauth/{provider}/start":{"get":{"summary":"Oauth Start","operationId":"oauth_start_api_auth_oauth__provider__start_get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"/","title":"Next"}},{"name":"purpose","in":"query","required":false,"schema":{"type":"string","default":"login","title":"Purpose"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/oauth/{provider}/callback":{"get":{"summary":"Oauth Callback","operationId":"oauth_callback_api_auth_oauth__provider__callback_get","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/oauth/{provider}":{"delete":{"summary":"Unlink","description":"Disconnect a provider — only while another way in remains: a password,\nor another connected provider. Otherwise the account would be locked.","operationId":"unlink_api_auth_oauth__provider__delete","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/doc":{"post":{"summary":"Create Doc","operationId":"create_doc_api_doc_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/doc/{slug}/preview.png":{"get":{"summary":"Get Shared Doc Preview","description":"The share's picture (B31b) — for mail, READMEs and wikis that refuse an\niframe. A snapshot: it shows the diagram as it was when the link was made,\nand nothing can change it afterwards, which is what makes it cacheable\nforever.","operationId":"get_shared_doc_preview_api_doc__slug__preview_png_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/doc/{slug}":{"get":{"summary":"Get Shared Doc","operationId":"get_shared_doc_api_doc__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Shared Doc Api Doc  Slug  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/doc/@{handle}/{name}":{"get":{"summary":"Get Named Doc","description":"A share under its readable name (C1b): /view/@maria/onboarding-flow.\n\nThe ONE named route. The payload carries the canonical `slug`, so unlock and\nthe preview picture keep working through the single-segment routes they\nalready have — no second set of routes, and no ordering traps. Declared\nAFTER `/api/doc/{slug}/preview.png` and `/api/doc/{slug}/unlock`: those match\nthe same number of segments, and a literal only wins when it is declared\nfirst (the same trap that made the preview a sub-path in the first place).\n\nThe slug is no secret here — both URLs address the same live share.","operationId":"get_named_doc_api_doc___handle___name__get","parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string","title":"Handle"}},{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Named Doc Api Doc   Handle   Name  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/doc/{slug}/unlock":{"post":{"summary":"Unlock Shared Doc","description":"Hand over a password-protected document (C1). Deliberately no token and\nno cookie: a reload asks again. Drill-down costs nothing extra — the whole\ndocument is already in the reader's memory — so the only thing a session\nwould buy is skipping the prompt after a refresh, which is not worth a\nsecond credential living in the browser.\n\nRate-limited per IP like the publish route, on top of argon2's own cost.","operationId":"unlock_shared_doc_api_doc__slug__unlock_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnlockRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Unlock Shared Doc Api Doc  Slug  Unlock Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/support":{"post":{"summary":"Support","operationId":"support_api_support_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Support Api Support Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/health":{"get":{"summary":"Health","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Health","operationId":"health_api_health_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"ApiTokenCreate":{"properties":{"label":{"type":"string","maxLength":48,"minLength":1,"title":"Label"},"expires_in_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires In Days"}},"additionalProperties":false,"type":"object","required":["label"],"title":"ApiTokenCreate","description":"Mint a long-lived token for an agent (C2). Pro, and cookie-only."},"ApiTokenCreated":{"properties":{"id":{"type":"integer","title":"Id"},"label":{"type":"string","title":"Label"},"hint":{"type":"string","title":"Hint"},"created_at":{"type":"integer","title":"Created At"},"last_used_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Used At"},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"},"token":{"type":"string","title":"Token"}},"type":"object","required":["id","label","hint","created_at","token"],"title":"ApiTokenCreated","description":"The only response that ever carries the raw token — it is not stored,\nonly its sha256 is, so this is the user's single chance to copy it."},"ApiTokenInfo":{"properties":{"id":{"type":"integer","title":"Id"},"label":{"type":"string","title":"Label"},"hint":{"type":"string","title":"Hint"},"created_at":{"type":"integer","title":"Created At"},"last_used_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Used At"},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["id","label","hint","created_at"],"title":"ApiTokenInfo"},"BillingSession":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"BillingSession","description":"A hosted Stripe page the browser is sent to — checkout or portal."},"BillingSummary":{"properties":{"subscription":{"anyOf":[{"$ref":"#/components/schemas/SubscriptionInfo"},{"type":"null"}]},"invoices":{"items":{"$ref":"#/components/schemas/InvoiceInfo"},"type":"array","title":"Invoices","default":[]}},"type":"object","title":"BillingSummary","description":"The account page's Plan section (C9 round 3), read live from Stripe —\nno copy is kept here, so it cannot disagree with what Stripe bills."},"BlobCheck":{"properties":{"hashes":{"items":{"type":"string"},"type":"array","maxItems":200,"title":"Hashes"}},"type":"object","title":"BlobCheck","description":"Which of these pictures does the account already hold? (C5c) — so a\nclient uploads one once and never again, even from a machine that has never\nseen it."},"ChangePasswordRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","maxLength":200,"minLength":8,"title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"ChangePasswordRequest","description":"The current password is the re-auth: a session left open on a shared\nmachine must not be enough to lock its owner out."},"CheckoutRequest":{"properties":{"plan":{"type":"string","maxLength":40,"minLength":1,"title":"Plan","default":"pro"},"interval":{"type":"string","enum":["month","year"],"title":"Interval","default":"month"}},"type":"object","title":"CheckoutRequest","description":"What to buy (C6a). The plan is checked against `plans.PLANS` and the\npair against the configured prices at the route — this schema only pins the\nshape, so adding a tier needs no change here."},"CloudDocBody":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"title":{"type":"string","title":"Title"},"version":{"type":"integer","title":"Version"},"updated_at":{"type":"integer","title":"Updated At"},"created_at":{"type":"integer","title":"Created At"},"size":{"type":"integer","title":"Size"},"deleted_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Deleted At"},"data":{"additionalProperties":true,"type":"object","title":"Data"}},"type":"object","required":["doc_id","title","version","updated_at","created_at","size","data"],"title":"CloudDocBody"},"CloudDocInfo":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"title":{"type":"string","title":"Title"},"version":{"type":"integer","title":"Version"},"updated_at":{"type":"integer","title":"Updated At"},"created_at":{"type":"integer","title":"Created At"},"size":{"type":"integer","title":"Size"},"deleted_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Deleted At"}},"type":"object","required":["doc_id","title","version","updated_at","created_at","size"],"title":"CloudDocInfo","description":"One row of the library listing: metadata, never the body. A listing is\nthe common call and the bodies are the expensive half."},"CloudDocWrite":{"properties":{"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"updated_at":{"type":"integer","title":"Updated At"},"base_version":{"type":"integer","minimum":0.0,"title":"Base Version"},"data":{"additionalProperties":true,"type":"object","title":"Data"},"blobs":{"items":{"type":"string"},"type":"array","maxItems":500,"title":"Blobs"}},"additionalProperties":false,"type":"object","required":["title","updated_at","base_version","data"],"title":"CloudDocWrite","description":"A new state for one document.\n\n`base_version` is the version the client started from — 0 means \"I believe\nthis document is not in the account yet\". The server compares before it\nwrites, so a device that has been offline cannot silently erase what\nanother one saved in the meantime."},"CloudSearchHit":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"doc_title":{"type":"string","title":"Doc Title"},"diagram_id":{"type":"string","title":"Diagram Id"},"node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Node Id"},"kind":{"type":"string","title":"Kind"},"text":{"type":"string","title":"Text"},"path":{"items":{"type":"string"},"type":"array","title":"Path"}},"type":"object","required":["doc_id","doc_title","diagram_id","kind","text","path"],"title":"CloudSearchHit","description":"One match in the library's search index (C5b).\n\nMetadata only, like `SnapshotInfo`: enough to show a row and to open the\nright document at the right level, never the document itself.\n`node_id` is absent when the hit is a document or a diagram title."},"CloudUsage":{"properties":{"documents":{"type":"integer","title":"Documents"},"bytes":{"type":"integer","title":"Bytes"},"max_documents":{"type":"integer","title":"Max Documents"},"max_bytes":{"type":"integer","title":"Max Bytes"},"max_document_bytes":{"type":"integer","title":"Max Document Bytes"}},"type":"object","required":["documents","bytes","max_documents","max_bytes","max_document_bytes"],"title":"CloudUsage","description":"What the account has stored, against what its plan allows."},"DeleteAccountRequest":{"properties":{"password":{"type":"string","title":"Password","default":""}},"type":"object","title":"DeleteAccountRequest","description":"Re-auth for the destructive op — a stolen session alone must not\nsuffice to erase an account. An account without a password re-confirms\nwith Google instead (C7) and sends no password."},"EmailRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"EmailRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HandleUpdate":{"properties":{"handle":{"type":"string","title":"Handle"}},"additionalProperties":false,"type":"object","required":["handle"],"title":"HandleUpdate","description":"Claim a handle, or move to another one (C1b)."},"InvoiceInfo":{"properties":{"id":{"type":"string","title":"Id"},"number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Number"},"created":{"type":"integer","title":"Created"},"total":{"type":"integer","title":"Total"},"currency":{"type":"string","title":"Currency"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"pdf_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pdf Url"},"hosted_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hosted Url"}},"type":"object","required":["id","created","total","currency"],"title":"InvoiceInfo"},"LinkInfo":{"properties":{"slug":{"type":"string","title":"Slug"},"url":{"type":"string","title":"Url"},"created_at":{"type":"integer","title":"Created At"},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"},"allow_copy":{"type":"boolean","title":"Allow Copy","default":true},"view_count":{"type":"integer","title":"View Count","default":0},"protected":{"type":"boolean","title":"Protected","default":false},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"named_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Named Url"},"title":{"type":"string","title":"Title","default":""},"has_preview":{"type":"boolean","title":"Has Preview","default":false}},"type":"object","required":["slug","url","created_at"],"title":"LinkInfo"},"LinkUpdate":{"properties":{"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"},"allow_copy":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Copy"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"}},"additionalProperties":false,"type":"object","title":"LinkUpdate","description":"Partial update; omitted fields stay untouched. expires_at=null clears\nthe expiry, password=null removes the password."},"LoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"PortalRequest":{"properties":{"flow":{"anyOf":[{"type":"string","enum":["cancel","payment"]},{"type":"null"}],"title":"Flow"}},"type":"object","title":"PortalRequest","description":"Which part of Stripe's portal to open (C9). None = its front page;\n`cancel` and `payment` go straight to that flow, so the button on the\naccount page does what it says instead of landing on a menu."},"PrefsBody":{"properties":{"data":{"additionalProperties":{"type":"string"},"type":"object","title":"Data"}},"additionalProperties":false,"type":"object","required":["data"],"title":"PrefsBody","description":"A flat map of storage key to stored string — what the browser keeps,\nminus the parts that describe a screen rather than a person."},"PrefsResponse":{"properties":{"data":{"additionalProperties":{"type":"string"},"type":"object","title":"Data"},"updated_at":{"type":"integer","title":"Updated At"}},"type":"object","required":["data","updated_at"],"title":"PrefsResponse"},"RegisterRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":200,"minLength":8,"title":"Password"}},"type":"object","required":["email","password"],"title":"RegisterRequest"},"ResetPasswordRequest":{"properties":{"token":{"type":"string","title":"Token"},"password":{"type":"string","maxLength":200,"minLength":8,"title":"Password"}},"type":"object","required":["token","password"],"title":"ResetPasswordRequest"},"ShareRequest":{"properties":{"data":{"additionalProperties":true,"type":"object","title":"Data"},"preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview"}},"type":"object","required":["data"],"title":"ShareRequest"},"ShareResponse":{"properties":{"slug":{"type":"string","title":"Slug"},"url":{"type":"string","title":"Url"},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings","default":[]},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["slug","url"],"title":"ShareResponse"},"SnapshotBody":{"properties":{"id":{"type":"integer","title":"Id"},"version":{"type":"integer","title":"Version"},"title":{"type":"string","title":"Title"},"size":{"type":"integer","title":"Size"},"nodes":{"type":"integer","title":"Nodes"},"diagrams":{"type":"integer","title":"Diagrams"},"created_at":{"type":"integer","title":"Created At"},"data":{"additionalProperties":true,"type":"object","title":"Data"}},"type":"object","required":["id","version","title","size","nodes","diagrams","created_at","data"],"title":"SnapshotBody"},"SnapshotInfo":{"properties":{"id":{"type":"integer","title":"Id"},"version":{"type":"integer","title":"Version"},"title":{"type":"string","title":"Title"},"size":{"type":"integer","title":"Size"},"nodes":{"type":"integer","title":"Nodes"},"diagrams":{"type":"integer","title":"Diagrams"},"created_at":{"type":"integer","title":"Created At"}},"type":"object","required":["id","version","title","size","nodes","diagrams","created_at"],"title":"SnapshotInfo","description":"One version in a document's history (C5) — metadata only, so a list\ncosts a row each rather than a document each."},"SubscriptionInfo":{"properties":{"plan":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan"},"status":{"type":"string","title":"Status"},"interval":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Interval"},"amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"current_period_end":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Period End"},"cancel_at_period_end":{"type":"boolean","title":"Cancel At Period End","default":false},"ends_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ends At"}},"type":"object","required":["status"],"title":"SubscriptionInfo"},"SupportRequest":{"properties":{"message":{"type":"string","maxLength":5000,"minLength":1,"title":"Message"},"reply_to":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Reply To"},"website":{"type":"string","title":"Website","default":""}},"type":"object","required":["message"],"title":"SupportRequest"},"TokenRequest":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"TokenRequest"},"UnlockRequest":{"properties":{"password":{"type":"string","title":"Password"}},"additionalProperties":false,"type":"object","required":["password"],"title":"UnlockRequest","description":"The password a reader types for a protected share (C1)."},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"verified":{"type":"boolean","title":"Verified"},"handle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Handle"},"entitlements":{"items":{"type":"string"},"type":"array","title":"Entitlements","default":[]},"has_password":{"type":"boolean","title":"Has Password","default":true},"google":{"type":"boolean","title":"Google","default":false},"providers":{"items":{"type":"string"},"type":"array","title":"Providers","default":[]}},"type":"object","required":["id","email","verified"],"title":"UserResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}