{"components":{"parameters":{"IDPath":{"in":"path","name":"id","required":true,"schema":{"$ref":"#/components/schemas/UUID"}},"ListIDPath":{"in":"path","name":"listID","required":true,"schema":{"$ref":"#/components/schemas/UUID"}},"TaskIDPath":{"in":"path","name":"taskID","required":true,"schema":{"$ref":"#/components/schemas/UUID"}},"UserIDPath":{"in":"path","name":"userID","required":true,"schema":{"$ref":"#/components/schemas/UUID"}},"VarSecretName":{"description":"Variable / secret name. Shell-env shape: uppercase letter\nfirst, then uppercase letters, digits, or underscores.\nUp to 64 chars.\n","in":"path","name":"name","required":true,"schema":{"pattern":"^[A-Z][A-Z0-9_]{0,63}$","type":"string"}}},"responses":{"BadGateway":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Upstream provider failure (mailer, Stripe, Slack, Favro)."},"BadRequest":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Bad request."},"Conflict":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"State conflict (e.g. project_ready, already-decided)."},"Forbidden":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Caller lacks the required role."},"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Row not found or hidden by RLS."},"PayloadTooLarge":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Request body exceeded the configured limit."},"PaymentRequired":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Subscription expired or read-only."},"RateLimited":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"retry_after":{"type":"integer"}},"type":"object"}]}}},"description":"Too many requests; check the `Retry-After` header.","headers":{"Retry-After":{"schema":{"type":"integer"}}}},"ServerError":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Internal server error."},"ServiceUnavailable":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Subsystem not configured / temporarily unavailable."},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Authentication required or failed."}},"schemas":{"APIKey":{"properties":{"created_at":{"$ref":"#/components/schemas/Timestamp"},"expires_at":{"$ref":"#/components/schemas/Timestamp"},"id":{"$ref":"#/components/schemas/UUID"},"last_used_at":{"$ref":"#/components/schemas/Timestamp"},"name":{"type":"string"},"prefix":{"type":"string"},"read_only":{"type":"boolean"}},"type":"object"},"APIKeyCreated":{"allOf":[{"$ref":"#/components/schemas/APIKey"},{"properties":{"key":{"description":"One-time plaintext. Never returned again.","type":"string"}},"type":"object"}]},"AnalyticsSnapshot":{"description":"Aggregated stats backing the per-user analytics dashboard.\nStatus counts are open-ended (per task status), so additional\nproperties are allowed.\n","properties":{"days":{"type":"integer"},"lists":{"items":{"properties":{"count":{"type":"integer"},"id":{"$ref":"#/components/schemas/UUID"},"name":{"type":"string"}},"type":"object"},"type":"array"},"series":{"items":{"properties":{"completed":{"type":"integer"},"date":{"description":"YYYY-MM-DD","type":"string"},"failed":{"type":"integer"}},"type":"object"},"type":"array"},"status":{"additionalProperties":{"type":"integer"},"type":"object"}},"type":"object"},"AuditEvent":{"properties":{"created_at":{"$ref":"#/components/schemas/Timestamp"},"event":{"type":"string"},"ip":{"type":"string"},"ok":{"type":"boolean"},"reason":{"type":"string"},"user_agent":{"type":"string"}},"type":"object"},"AuthConfig":{"properties":{"locales":{"items":{"type":"string"},"type":"array"},"signup_enabled":{"type":"boolean"},"sso":{"properties":{"button_label":{"type":"string"},"enabled":{"type":"boolean"},"hosted_domain":{"type":"string"},"login_url":{"type":"string"},"provider_name":{"type":"string"}},"type":"object"}},"type":"object"},"BillingMe":{"properties":{"billing_enabled":{"type":"boolean"},"current_period_end":{"$ref":"#/components/schemas/Timestamp"},"financial_aid":{"type":"boolean"},"in_trial":{"type":"boolean"},"is_active":{"type":"boolean"},"note":{"type":"string"},"status":{"type":"string"},"tier":{"type":"string"},"trial_ends_at":{"$ref":"#/components/schemas/Timestamp"}},"type":"object"},"BillingPlan":{"properties":{"description":{"type":"string"},"highlight":{"type":"boolean"},"id":{"type":"string"},"kind":{"type":"string"},"label":{"type":"string"},"tagline":{"type":"string"}},"type":"object"},"Charge":{"additionalProperties":true,"description":"Stripe charge snapshot; shape mirrors Stripe's `charges` object.","type":"object"},"Comment":{"properties":{"author":{"type":"string"},"author_id":{"$ref":"#/components/schemas/UUID"},"body":{"type":"string"},"created_at":{"$ref":"#/components/schemas/Timestamp"},"edited_at":{"$ref":"#/components/schemas/Timestamp"},"id":{"format":"int64","type":"integer"},"kind":{"enum":["user"],"type":"string"},"list_id":{"$ref":"#/components/schemas/UUID"},"task_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"},"CreateTaskRequest":{"properties":{"color":{"pattern":"^#[0-9a-fA-F]{6}$","type":"string"},"description":{"type":"string"},"due_at":{"$ref":"#/components/schemas/Timestamp"},"icon":{"maxLength":16,"type":"string"},"name":{"type":"string"}},"required":["name"],"type":"object"},"Error":{"description":"Structured error envelope. `code` follows `area.subkey`.","properties":{"code":{"type":"string"},"error":{"type":"string"}},"required":["error"],"type":"object"},"Invite":{"properties":{"created_at":{"$ref":"#/components/schemas/Timestamp"},"email":{"type":"string"},"expires_at":{"$ref":"#/components/schemas/Timestamp"},"id":{"$ref":"#/components/schemas/UUID"},"role":{"type":"string"}},"type":"object"},"InviteSummary":{"properties":{"accepted":{"type":"boolean"},"email":{"type":"string"},"expired":{"type":"boolean"},"expires_at":{"$ref":"#/components/schemas/Timestamp"},"project_id":{"$ref":"#/components/schemas/UUID"},"project_name":{"type":"string"},"role":{"type":"string"}},"type":"object"},"LoginRequest":{"properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"],"type":"object"},"LoginResponse":{"properties":{"must_change_password":{"type":"boolean"},"role":{"type":"string"},"token":{"type":"string"},"user_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"},"LoginTOTPRequired":{"properties":{"challenge":{"type":"string"},"requires_totp":{"type":"boolean"}},"type":"object"},"MeUser":{"properties":{"demo":{"type":"boolean"},"display_name":{"type":"string"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"locale":{"type":"string"},"must_change_password":{"type":"boolean"},"role":{"type":"string"},"user_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"},"Member":{"properties":{"created_at":{"$ref":"#/components/schemas/Timestamp"},"email":{"type":"string"},"role":{"enum":["editor","viewer"],"type":"string"},"user_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"},"PatchMeRequest":{"properties":{"display_name":{"maxLength":40,"type":"string"},"locale":{"type":"string"}},"type":"object"},"PatchTaskRequest":{"description":"All fields optional. `due_at` accepts JSON `null` to clear the\ncolumn; omitting it leaves the existing value.\n","properties":{"description":{"type":"string"},"due_at":{"oneOf":[{"$ref":"#/components/schemas/Timestamp"},{"type":"null"}]},"name":{"type":"string"},"outputs":{"additionalProperties":true,"type":"object"},"position":{"exclusiveMinimum":0,"format":"double","type":"number"},"status":{"enum":["pending","in_progress","completed","failed"],"type":"string"}},"type":"object"},"PortraitMeta":{"properties":{"bytes":{"type":"integer"},"mime":{"type":"string"},"sha256":{"type":"string"},"updated_at":{"$ref":"#/components/schemas/Timestamp"}},"type":"object"},"Project":{"description":"A list (URL alias \"list\"). MyRole is the caller's effective role.","properties":{"completed":{"type":"integer"},"created_at":{"$ref":"#/components/schemas/Timestamp"},"description":{"type":"string"},"failed":{"type":"integer"},"git_repo":{"description":"Associated git remote url; empty if none.","type":"string"},"icon":{"type":"string"},"id":{"$ref":"#/components/schemas/UUID"},"my_role":{"enum":["owner","editor","viewer"],"type":"string"},"name":{"type":"string"},"overdue":{"type":"integer"},"owner_id":{"$ref":"#/components/schemas/UUID"},"pending":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"PublicProjectView":{"properties":{"description":{"type":"string"},"icon":{"type":"string"},"name":{"type":"string"},"state":{"type":"string"},"tasks":{"items":{"properties":{"description":{"type":"string"},"icon":{"type":"string"},"id":{"$ref":"#/components/schemas/UUID"},"name":{"type":"string"},"position":{"format":"double","type":"number"},"status":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"RecoveryCodesResponse":{"properties":{"recovery_codes":{"items":{"type":"string"},"type":"array"}},"type":"object"},"RegisterResponse":{"properties":{"email":{"type":"string"},"email_verified":{"type":"boolean"},"role":{"type":"string"},"token":{"type":"string"},"user_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"},"SetupState":{"properties":{"setup_required":{"type":"boolean"}},"type":"object"},"SignupRequest":{"properties":{"email":{"type":"string"},"invite_token":{"type":"string"},"password":{"type":"string"},"terms_accepted":{"type":"boolean"}},"required":["email","password","terms_accepted"],"type":"object"},"TOTPSetupResponse":{"properties":{"otp_auth_url":{"type":"string"},"qr_png_data_url":{"type":"string"},"secret_base32":{"type":"string"}},"type":"object"},"TOTPStatus":{"properties":{"enabled":{"type":"boolean"},"enrolled_at":{"$ref":"#/components/schemas/Timestamp"}},"type":"object"},"TOTPVerifyRequest":{"properties":{"challenge":{"type":"string"},"code":{"type":"string"},"recovery_code":{"type":"string"},"remember":{"type":"boolean"}},"required":["challenge"],"type":"object"},"Task":{"properties":{"archived_at":{"$ref":"#/components/schemas/Timestamp"},"color":{"type":"string"},"comment_count":{"type":"integer"},"created_at":{"$ref":"#/components/schemas/Timestamp"},"description":{"type":"string"},"due_at":{"$ref":"#/components/schemas/Timestamp"},"icon":{"type":"string"},"id":{"$ref":"#/components/schemas/UUID"},"list_id":{"$ref":"#/components/schemas/UUID"},"name":{"type":"string"},"outputs":{"additionalProperties":true,"type":"object"},"position":{"format":"double","type":"number"},"status":{"type":"string"},"updated_at":{"$ref":"#/components/schemas/Timestamp"}},"type":"object"},"Timestamp":{"format":"date-time","type":"string"},"UUID":{"format":"uuid","type":"string"},"UserExport":{"properties":{"api_keys":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"auth_audit":{"items":{"$ref":"#/components/schemas/AuditEvent"},"type":"array"},"comments_authored":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"format":{"type":"string"},"generated_at":{"$ref":"#/components/schemas/Timestamp"},"profile":{"additionalProperties":true,"type":"object"},"project_memberships":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"sessions":{"items":{"additionalProperties":true,"type":"object"},"type":"array"},"two_factor":{"additionalProperties":true,"type":"object"}},"type":"object"},"Version":{"properties":{"build_time":{"type":"string"},"go_version":{"type":"string"},"modified":{"type":"boolean"},"revision":{"type":"string"},"version":{"type":"string"}},"type":"object"}},"securitySchemes":{"bearerAuth":{"bearerFormat":"JWT","description":"JWT issued by `POST /auth/login` or sent via the `hazy_jwt`\ncookie; for service callers, personal API keys created via\n`POST /users/me/api-keys` use the same\n`Authorization: Bearer …` header.\n","scheme":"bearer","type":"http"},"cookieAuth":{"description":"Browser callers use this in practice; the bearer header and\ncookie are interchangeable.\n","in":"cookie","name":"hazy_jwt","type":"apiKey"},"stripeSignature":{"description":"HMAC signature over the body — only on `POST /webhooks/stripe`.","in":"header","name":"Stripe-Signature","type":"apiKey"}}},"info":{"description":"HTTP/JSON API exposed by `hazydod` for Hazydo. Every endpoint lives\nunder `/api/v1/`; this document covers the non-admin surface used by\nthe SPA, CLI, and personal-API-key scripts. The admin-only endpoints\nunder `/admin` are out of scope here.\n\nAuthentication: most routes accept either a session cookie\n(`hazy_jwt`) or a `Authorization: Bearer \u003ctoken\u003e` header where the\ntoken is either a short-lived login JWT or a personal API key\nminted at `POST /users/me/api-keys`. The two schemes are\ninterchangeable; the SPA uses the cookie, scripts typically use a\nbearer key.\n\nErrors share the envelope `{ code: string, error: string }` —\n`code` follows `area.subkey` (`auth.invalid_credentials`,\n`billing.not_configured`, …). Some paths omit `code`; the `error`\nfield is always present.\n","license":{"identifier":"AGPL-3.0-or-later","name":"AGPL-3.0-or-later"},"title":"Hazydo API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/auth/config":{"get":{"description":"Reports whether self-service signup is enabled, what SSO option\n(if any) is wired up, and which i18n locales the server can\nrender mail in. Public.\n","operationId":"getAuthConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthConfig"}}},"description":"OK"},"4XX":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Any client-side error follows the shared Error envelope. In\npractice this endpoint has no 4xx mode today — it's declared\nhere so generated clients have a typed branch ready if one\never appears.\n"}},"security":[],"summary":"Pre-auth configuration the login screen needs","tags":["Auth"]}},"/auth/demo":{"post":{"description":"Public autologin into the demo user, when `HAZY_DEMO_ENABLED` is\nset on the server. 404 when disabled or unseeded. The session\ncookie is set on success.\n","operationId":"demoLogin","responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/LoginResponse"},{"properties":{"demo":{"type":"boolean"},"email":{"type":"string"},"email_verified":{"type":"boolean"}},"type":"object"}]}}},"description":"Demo session created."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Sign in to the shared demo account","tags":["Auth"]}},"/auth/login":{"post":{"description":"Verifies credentials, mints a session row (returned as the\n`hazy_jwt` cookie), and emits a JWT in the response body for\nBearer-using callers. When the user has TOTP enabled, returns a\nshort-lived challenge instead — call `POST /auth/totp/verify`\nwith the code to finish login.\n","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/LoginResponse"},{"$ref":"#/components/schemas/LoginTOTPRequired"}]}}},"description":"Signed in, or TOTP challenge pending."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"login_url":{"type":"string"}},"type":"object"}]}}},"description":"SSO required for this address (hosted-domain lock)."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Sign in with email and password","tags":["Auth"]}},"/auth/logout":{"post":{"description":"Idempotent. The cookie is always cleared in the response. If a\nsession row was actually deleted, an `auth.logout` audit entry\nlands.\n","operationId":"logout","responses":{"204":{"description":"Logged out."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Drop the caller's session row + cookie","tags":["Auth"]}},"/auth/oidc/callback":{"get":{"description":"Verifies state + nonce, exchanges the code, JIT-provisions or\nlinks the user, mints a session, and redirects to `/` (or `/#sso-error=\u003cslug\u003e` on failure).\n","operationId":"oidcCallback","parameters":[{"in":"query","name":"code","schema":{"type":"string"}},{"in":"query","name":"state","schema":{"type":"string"}}],"responses":{"200":{"description":"Reserved — current implementation always redirects, but the\nresponse key is declared so codegen tools that require a 2xx\nentry don't choke.\n"},"302":{"description":"Redirect back into the SPA."},"404":{"$ref":"#/components/responses/NotFound"}},"security":[],"summary":"Finish the SSO leg","tags":["Auth"]}},"/auth/oidc/login":{"get":{"description":"Mints state + nonce cookies and 302s to the configured OpenID\nConnect provider. 404 when SSO isn't enabled.\n","operationId":"oidcLogin","responses":{"200":{"description":"Reserved — current implementation always redirects, but the\nresponse key is declared so codegen tools that require a 2xx\nentry don't choke.\n"},"302":{"description":"Redirect to the IdP."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Start the SSO leg","tags":["Auth"]}},"/auth/password/forgot":{"post":{"description":"Always 204 — hit and miss are indistinguishable on the wire so\nthe endpoint can't be used to enumerate registered addresses.\nRate-limited per (IP, email).\n","operationId":"forgotPassword","requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"type":"string"}},"required":["email"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Token (maybe) issued."},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[],"summary":"Issue a password-reset link","tags":["Auth"]}},"/auth/password/reset":{"post":{"description":"On success: all prior sessions are dropped, a new session is\nminted, and the `hazy_jwt` cookie is updated.\n","operationId":"resetPassword","requestBody":{"content":{"application/json":{"schema":{"properties":{"password":{"type":"string"},"token":{"type":"string"}},"required":["token","password"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Password reset."},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Consume a reset token and set a new password","tags":["Auth"]}},"/auth/register":{"post":{"description":"Creates a user, mints a session, sends the verification mail.\nRefused with `auth.signup_disabled` when public signup is off\nunless `invite_token` resolves to a valid pending list\ninvite for the same email. Rate-limited per (IP, email).\n","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}},"description":"Account created."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Create an account","tags":["Auth"]}},"/auth/totp/verify":{"post":{"description":"Consumes the challenge token returned by `/auth/login`, accepts\neither a 6-digit code or a recovery code. Setting `remember=true`\nmints a 30-day trusted-device bypass cookie. Mints the session\ncookie + JWT on success.\n","operationId":"verifyTOTP","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TOTPVerifyRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}},"description":"Signed in."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"security":[],"summary":"Finish the TOTP step of login","tags":["TOTP"]}},"/auth/verification/resend":{"post":{"description":"Login-required so the recipient address comes from the\nsigned-in row (not request input). 409 when the user is already\nverified.\n","operationId":"resendVerification","responses":{"204":{"description":"Mail dispatched."},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/BadGateway"}},"summary":"Re-send the verification email","tags":["Auth"]}},"/auth/verify":{"get":{"description":"Single-use. Always redirects to the SPA with `#verify-ok` or\n`#verify-failed`; no JSON body is returned.\n","operationId":"verifyEmail","parameters":[{"in":"query","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reserved — current implementation always redirects, but the\nresponse key is declared so codegen tools that require a 2xx\nentry don't choke.\n"},"302":{"description":"Redirect to the SPA with a flash flag."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or malformed `token` query parameter."}},"security":[],"summary":"Consume an email-verification token (link in mail)","tags":["Auth"]}},"/billing/charges":{"get":{"description":"Returns the 25 most recent charges. Stripe outage degrades to\nan empty list + `charges_error` rather than a 5xx.\n","operationId":"listBillingCharges","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"charges":{"items":{"$ref":"#/components/schemas/Charge"},"type":"array"},"charges_error":{"type":"string"}},"type":"object"}}},"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Caller's recent Stripe charges","tags":["Billing"]}},"/billing/checkout":{"post":{"operationId":"startCheckout","requestBody":{"content":{"application/json":{"schema":{"properties":{"kind":{"description":"Legacy alias; resolves to the first plan whose Kind matches.","type":"string"},"plan_id":{"type":"string"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"url":{"type":"string"}},"type":"object"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Start a Stripe Checkout Session","tags":["Billing"]}},"/billing/me":{"get":{"operationId":"getBillingMe","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingMe"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Caller's subscription snapshot","tags":["Billing"]}},"/billing/plans":{"get":{"operationId":"listBillingPlans","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"plans":{"items":{"$ref":"#/components/schemas/BillingPlan"},"type":"array"}},"type":"object"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"security":[],"summary":"Public catalogue of operator-configured plans","tags":["Billing"]}},"/billing/portal":{"post":{"operationId":"openBillingPortal","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"url":{"type":"string"}},"type":"object"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Open the Stripe Customer Portal","tags":["Billing"]}},"/comments/{commentID}":{"delete":{"operationId":"deleteComment","parameters":[{"in":"path","name":"commentID","required":true,"schema":{"format":"int64","type":"integer"}}],"responses":{"204":{"description":"Deleted."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Delete your own comment","tags":["Comments"]},"patch":{"description":"Author-only at the SQL layer. 404 masks \"not yours\".","operationId":"patchComment","parameters":[{"in":"path","name":"commentID","required":true,"schema":{"format":"int64","type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"body":{"maxLength":10000,"type":"string"}},"required":["body"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"}}},"description":"Updated."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Edit your own comment","tags":["Comments"]}},"/invites/{token}":{"get":{"operationId":"showInvite","parameters":[{"in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteSummary"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Pre-auth invite preview (renders \"You've been invited\")","tags":["Invites"]}},"/invites/{token}/accept":{"post":{"description":"The caller's email must match the invite's target. Returns the\njoined list id.\n","operationId":"acceptInvite","parameters":[{"in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"project_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"}}},"description":"Joined."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Redeem a list invite for the signed-in user","tags":["Invites"]}},"/lists":{"get":{"description":"Returns every list the caller owns or is a member of, with\naggregate task-status counts per row so the drawer can render\nprogress without a per-list events subscription.\n","operationId":"listProjects","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Project"},"type":"array"}}},"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List the caller's visible lists","tags":["Lists"]},"post":{"operationId":"createProject","requestBody":{"content":{"application/json":{"schema":{"properties":{"description":{"maxLength":4096,"type":"string"},"git_repo":{"description":"Git remote url; empty if none.","maxLength":2048,"type":"string"},"name":{"type":"string"}},"required":["name"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}},"description":"Created."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Create a new list","tags":["Lists"]}},"/lists/{listID}":{"delete":{"description":"Deletes the list and cascades its tasks, comments, and members.\n","operationId":"deleteProject","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"204":{"description":"Deleted."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Drop a list and cascade its tasks","tags":["Lists"]},"patch":{"operationId":"patchProject","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"description":{"maxLength":4096,"type":"string"},"git_repo":{"description":"Git remote url, or empty string to clear.","maxLength":2048,"type":"string"},"icon":{"type":"string"},"name":{"type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}},"description":"Updated."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Rename, re-describe, re-icon, or set the git repo of a list","tags":["Lists"]}},"/lists/{listID}/events":{"get":{"description":"Replaces UI polling. Frames:\n\n* `event: task` — payload is the full task snapshot.\n* `event: task_deleted` — payload is `{ id: \u003ctask uuid\u003e }`.\n* `event: comment` / `event: comment_deleted` — discussion thread.\n* `event: members_changed` — drawer should refetch `/members`.\n* `event: synced` — baseline replay complete.\n* `: keepalive` comment every 15s.\n","operationId":"listProjectEvents","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string"}}},"description":"SSE stream."},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Server-sent event stream for a list","tags":["Events"]}},"/lists/{listID}/invites":{"get":{"operationId":"listInvites","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Invite"},"type":"array"}}},"description":"OK"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Pending invites on a list (owner-only)","tags":["Invites"]}},"/lists/{listID}/invites/{inviteID}":{"delete":{"operationId":"revokeInvite","parameters":[{"$ref":"#/components/parameters/ListIDPath"},{"in":"path","name":"inviteID","required":true,"schema":{"$ref":"#/components/schemas/UUID"}}],"responses":{"204":{"description":"Revoked."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Revoke a pending invite (owner-only)","tags":["Invites"]}},"/lists/{listID}/members":{"get":{"operationId":"listMembers","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Member"},"type":"array"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List members of a list","tags":["Members"]},"post":{"description":"If the email resolves to a known user, the membership is\ncreated immediately. Otherwise a pending invite is persisted\nand an email link is sent — the recipient redeems via\n`/invites/{token}/accept`.\n","operationId":"addMember","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"type":"string"},"role":{"enum":["editor","viewer"],"type":"string"}},"required":["email","role"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Member"},{"properties":{"email":{"type":"string"},"expires_at":{"$ref":"#/components/schemas/Timestamp"},"role":{"type":"string"},"status":{"type":"string"}},"type":"object"}]}}},"description":"Member added (or invite mailed)."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Invite a user as editor or viewer","tags":["Members"]}},"/lists/{listID}/members/{userID}":{"delete":{"description":"Owner-only when removing someone else. Members removing\nthemselves use the same endpoint with their own user id; owners\ncannot self-leave (delete the list instead).\n","operationId":"removeMember","parameters":[{"$ref":"#/components/parameters/ListIDPath"},{"$ref":"#/components/parameters/UserIDPath"}],"responses":{"204":{"description":"Removed."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Remove a member (kick or self-leave)","tags":["Members"]}},"/lists/{listID}/public":{"delete":{"description":"Idempotent — disabling a private list is a no-op.","operationId":"disablePublicLink","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"204":{"description":"Disabled."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Revoke the list's public-share token","tags":["PublicLink"]},"get":{"operationId":"getPublicLink","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"enabled":{"type":"boolean"},"token":{"type":"string"},"url":{"type":"string"}},"type":"object"}}},"description":"Public link state."},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Inspect / fetch the list's public-share token","tags":["PublicLink"]},"post":{"operationId":"enablePublicLink","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"token":{"type":"string"},"url":{"type":"string"}},"type":"object"}}},"description":"Token minted."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Enable (or rotate) the list's public-share token","tags":["PublicLink"]}},"/lists/{listID}/tasks":{"get":{"operationId":"listTasks","parameters":[{"$ref":"#/components/parameters/ListIDPath"},{"description":"When `true`, includes archived tasks.","in":"query","name":"archived","schema":{"enum":["true","false"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Task"},"type":"array"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List tasks in a list","tags":["Tasks"]},"post":{"description":"Position is auto-assigned as `max(position) + 1`. Refused with\n`project_ready` if the list is currently dispatching; move\nit to Draft first.\n","operationId":"createTask","parameters":[{"$ref":"#/components/parameters/ListIDPath"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}},"description":"Created."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Append a task to a list","tags":["Tasks"]}},"/public/lists/{token}":{"get":{"operationId":"viewPublicProject","parameters":[{"in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProjectView"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Read-only public list view (no auth)","tags":["PublicViewer"]}},"/public/lists/{token}/rss":{"get":{"description":"Reuses the same public token as the read-only viewer. Each completed (non-archived) task is one entry: the task name is the title and its completion note (outputs.result), rendered from markdown to sanitized HTML, is the body — ordered newest-completion first.\n","operationId":"viewPublicProjectRSS","parameters":[{"in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/rss+xml":{"schema":{"type":"string"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"RSS 2.0 feed of a shared list's completed tasks (no auth)","tags":["PublicViewer"]}},"/public/lists/{token}/subscribe":{"post":{"operationId":"subscribePublicProject","parameters":[{"in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"id":{"type":"string"}},"type":"object"}}},"description":"Subscribed."},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Convert public-link access into a persistent viewer membership","tags":["PublicViewer"]}},"/setup":{"post":{"description":"Refused with 409 if any admin user already exists. On success\nthe caller is logged in immediately (cookie + JWT).\n","operationId":"doSetup","requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"type":"string"},"password":{"minLength":8,"type":"string"}},"required":["email","password"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"role":{"type":"string"},"token":{"type":"string"},"user_id":{"$ref":"#/components/schemas/UUID"}},"type":"object"}}},"description":"Admin created."},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Create the first admin user on a blank install","tags":["Setup"]}},"/setup/state":{"get":{"operationId":"getSetupState","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupState"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}},"security":[],"summary":"Does this install still need first-run bootstrapping?","tags":["Setup"]}},"/tasks/{taskID}":{"delete":{"description":"Refused with `project_ready` if dispatching; with 409 if the task is running.","operationId":"deleteTask","parameters":[{"$ref":"#/components/parameters/TaskIDPath"}],"responses":{"204":{"description":"Deleted."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Remove a task","tags":["Tasks"]},"patch":{"description":"Position, name, description, due_at, outputs, and status (one of\n`pending`, `in_progress`, `completed`, `failed`). `due_at`\naccepts JSON `null` to clear.\n","operationId":"patchTask","parameters":[{"$ref":"#/components/parameters/TaskIDPath"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchTaskRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}},"description":"Updated."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Update mutable task fields","tags":["Tasks"]}},"/tasks/{taskID}/archive":{"post":{"description":"Stamps `archived_at`. The row stays addressable but drops out of\ndefault listings.\n","operationId":"archiveTask","parameters":[{"$ref":"#/components/parameters/TaskIDPath"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}},"description":"Archived."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Soft-hide a task","tags":["Tasks"]}},"/tasks/{taskID}/comments":{"get":{"operationId":"listComments","parameters":[{"$ref":"#/components/parameters/TaskIDPath"}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Comment"},"type":"array"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Read a task's comment thread","tags":["Comments"]},"post":{"description":"Adds a comment to the task's discussion thread.\n","operationId":"createComment","parameters":[{"$ref":"#/components/parameters/TaskIDPath"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"body":{"maxLength":10000,"type":"string"}},"required":["body"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"}}},"description":"Posted."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Post a new comment","tags":["Comments"]}},"/users/me":{"get":{"operationId":"getMe","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeUser"}}},"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Return the signed-in user's profile","tags":["Users"]},"patch":{"operationId":"patchMe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchMeRequest"}}},"required":true},"responses":{"204":{"description":"Updated."},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Update display name and/or locale","tags":["Users"]}},"/users/me/analytics":{"get":{"operationId":"getAnalytics","parameters":[{"in":"query","name":"days","schema":{"default":30,"maximum":365,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSnapshot"}}},"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Per-user analytics snapshot","tags":["Analytics"]}},"/users/me/api-keys":{"get":{"operationId":"listAPIKeys","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/APIKey"},"type":"array"}}},"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List the caller's active personal API keys","tags":["Users"]},"post":{"description":"Returns the plaintext exactly once. After this response the\nserver keeps only the sha256 — the plaintext is unrecoverable.\nLimited to 25 active keys per user.\n","operationId":"createAPIKey","requestBody":{"content":{"application/json":{"schema":{"properties":{"expires_at":{"format":"date-time","type":"string"},"name":{"maxLength":80,"type":"string"},"read_only":{"type":"boolean"}},"required":["name"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIKeyCreated"}}},"description":"Key minted."},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Mint a new personal API key","tags":["Users"]}},"/users/me/api-keys/{id}":{"delete":{"description":"Password-confirmed. Idempotent on a revoked key.","operationId":"revokeAPIKey","parameters":[{"$ref":"#/components/parameters/IDPath"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"password":{"type":"string"}},"required":["password"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Revoked."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Revoke a personal API key","tags":["Users"]}},"/users/me/audit":{"get":{"operationId":"listMyAudit","parameters":[{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AuditEvent"},"type":"array"}}},"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Recent sign-in / security events for the caller","tags":["Users"]}},"/users/me/delete":{"post":{"description":"Requires the current password and the exact confirmation phrase\n\"delete my account\" (case-insensitive). Refused with code\n`last_admin` (single-admin install) or `shared_project` (caller\nowns lists with other members; offending IDs returned in\n`blocked_project_ids`).\n","operationId":"deleteAccount","requestBody":{"content":{"application/json":{"schema":{"properties":{"confirmation":{"type":"string"},"current_password":{"type":"string"}},"required":["current_password","confirmation"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Account deleted; session cookie cleared."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"blocked_project_ids":{"items":{"$ref":"#/components/schemas/UUID"},"type":"array"}},"type":"object"}]}}},"description":"Refused — `last_admin` or `shared_project`."},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Self-service GDPR Art. 17 erasure","tags":["Users"]}},"/users/me/export":{"get":{"description":"Returns one JSON document covering the caller's profile,\nsessions, audit, API keys (metadata only — never plaintexts),\nmemberships, comments they authored, and 2FA presence.\n","operationId":"exportMe","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserExport"}}},"description":"Export payload."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"GDPR self-service data export","tags":["Users"]}},"/users/me/password":{"post":{"description":"Verifies the current password (bcrypt), writes the new hash,\ndrops every other session, and refreshes the cookie. Escape-\nhatch route: works even when the account is read-only.\n","operationId":"changePassword","requestBody":{"content":{"application/json":{"schema":{"properties":{"current_password":{"type":"string"},"new_password":{"minLength":8,"type":"string"}},"required":["current_password","new_password"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Password updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Change the signed-in user's password","tags":["Users"]}},"/users/me/portrait":{"delete":{"description":"Idempotent.","operationId":"deletePortrait","responses":{"204":{"description":"Removed."},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Clear the caller's portrait","tags":["Portraits"]},"put":{"description":"Multipart form upload (`file` field, JPEG or PNG, ≤ 1 MB).\nServer centre-crops to square, resizes to 256×256, re-encodes\nas JPEG (quality 82) before storing.\n","operationId":"putPortrait","requestBody":{"content":{"multipart/form-data":{"schema":{"properties":{"file":{"format":"binary","type":"string"}},"required":["file"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortraitMeta"}}},"description":"Saved."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Upload a portrait","tags":["Portraits"]}},"/users/me/totp":{"get":{"operationId":"getTOTPStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TOTPStatus"}}},"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Report 2FA enrolment state","tags":["TOTP"]}},"/users/me/totp/confirm":{"post":{"description":"Validates the first code and surfaces the freshly-minted\nrecovery codes ONCE. Save them off-server — the API never\nreturns the same codes again.\n","operationId":"confirmTOTP","requestBody":{"content":{"application/json":{"schema":{"properties":{"code":{"type":"string"}},"required":["code"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryCodesResponse"}}},"description":"Enrolment complete."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Finalise TOTP enrolment","tags":["TOTP"]}},"/users/me/totp/disable":{"post":{"description":"Requires the user's current password. Clears trusted-device\ncookies on success.\n","operationId":"disableTOTP","requestBody":{"content":{"application/json":{"schema":{"properties":{"password":{"type":"string"}},"required":["password"],"type":"object"}}},"required":true},"responses":{"204":{"description":"Disabled."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Turn off 2FA after re-auth","tags":["TOTP"]}},"/users/me/totp/recovery-codes/regenerate":{"post":{"description":"Invalidates the previous codes; shown once.","operationId":"regenerateRecoveryCodes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryCodesResponse"}}},"description":"Codes minted."},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Mint a fresh set of recovery codes","tags":["TOTP"]}},"/users/me/totp/setup":{"post":{"description":"Mints a fresh secret and returns its provisioning URL, QR PNG\ndata URL, and the manual base32 fallback. Re-calling before\nconfirm replaces the pending secret.\n","operationId":"setupTOTP","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TOTPSetupResponse"}}},"description":"Provisioning data."},"409":{"$ref":"#/components/responses/Conflict"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"summary":"Start TOTP enrolment","tags":["TOTP"]}},"/users/{userID}/portrait":{"get":{"description":"Cross-workspace by design (so comment authors / shared-list\nmembers render with the right face). Strong ETag from the\nstored sha256; supports `If-None-Match`.\n","operationId":"getPortrait","parameters":[{"$ref":"#/components/parameters/UserIDPath"}],"responses":{"200":{"content":{"image/jpeg":{"schema":{"format":"binary","type":"string"}}},"description":"JPEG bytes."},"304":{"description":"Not modified."},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Fetch any user's portrait as JPEG","tags":["Portraits"]}},"/version":{"get":{"operationId":"getVersion","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Version"}}},"description":"OK"},"4XX":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Reserved — the daemon never returns a 4xx from this route\n(it just serializes a static `version.Info`), but a typed\nerror branch keeps generated clients consistent with the\nrest of the API.\n"}},"security":[],"summary":"Build identity (unauthenticated)","tags":["Meta"]}},"/webhooks/stripe":{"post":{"description":"Unauthenticated by JWT; the `Stripe-Signature` header verifies\nthe body. Body is capped at 256 KiB.\n","operationId":"stripeWebhook","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object"}}},"required":true},"responses":{"200":{"description":"Accepted."},"400":{"$ref":"#/components/responses/BadRequest"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"security":[{"stripeSignature":[]}],"summary":"Inbound Stripe events","tags":["Billing"]}}},"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"servers":[{"url":"/api/v1"}],"tags":[{"description":"Sign-in, sign-up, password reset, OIDC, and session lifecycle.","name":"Auth"},{"description":"Two-factor authentication setup, verification, and recovery codes.","name":"TOTP"},{"description":"The caller's own profile, password, export, audit log, account deletion, and personal API keys.","name":"Users"},{"description":"Top-level work containers (\"lists\" in the URL space) — CRUD and per-list resources.","name":"Lists"},{"description":"Individual units of work inside a list — CRUD and archive.","name":"Tasks"},{"description":"Threaded discussion on a task.","name":"Comments"},{"description":"Membership and access control on a list.","name":"Members"},{"description":"Outbound and inbound list invitations.","name":"Invites"},{"description":"Per-user avatar uploads.","name":"Portraits"},{"description":"Stripe-backed subscription, plans, charges, checkout, and customer portal.","name":"Billing"},{"description":"Enable / disable the unguessable read-only token on a list.","name":"PublicLink"},{"description":"Token-scoped read of a list (no auth) and subscribe-into-drawer.","name":"PublicViewer"},{"description":"Per-user aggregate metrics over the caller's lists and tasks.","name":"Analytics"},{"description":"Per-list change-event stream (SSE).","name":"Events"},{"description":"First-run install bootstrap (admin user, base settings).","name":"Setup"},{"description":"Daemon build identity and similar unauthenticated metadata.","name":"Meta"}]}