{"openapi":"3.1.0","info":{"title":"fstack demo API","version":"0.1.0","description":"A throwaway demo product that proves the fstack template end to end, for the fstack owner."},"servers":[{"url":"https://preview.demo.meduza.digital"}],"paths":{"/api/health":{"get":{"operationId":"get_api_health","summary":"Check that the Worker is up","x-auth":"public","description":"Answers without touching D1 or any provider, so a smoke check can tell the Worker from its dependencies.","security":[],"responses":{"200":{"description":"The Worker is serving, and the target it was built for.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"target":{"type":"string","enum":["local","preview","production"]}},"required":["ok","target"],"additionalProperties":false}}}}}}},"/api/me":{"get":{"operationId":"get_api_me","summary":"Who is signed in","x-auth":"user","description":"The caller's Clerk ids and the subject that owns data and pays: the user in b2c, the active Organization in b2b.","security":[{"clerkSession":[]}],"responses":{"200":{"description":"The signed-in caller.","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"organizationId":{"type":["string","null"]},"organizationRole":{"type":["string","null"]},"subjectId":{"type":"string"}},"required":["userId","organizationId","organizationRole","subjectId"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured for this target (`auth_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/news":{"get":{"operationId":"get_api_news","summary":"Your Product news consent","x-auth":"user","description":"Whether the signed-in person gets Product news (marketing email): `off`, `pending` (asked, not confirmed) or `on`.","security":[{"clerkSession":[]}],"responses":{"200":{"description":"The consent.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["off","pending","on"],"description":"`pending` until the person clicks the link in the confirmation email."}},"required":["status"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured for this target (`auth_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}},"post":{"operationId":"post_api_news","summary":"Ask for Product news","x-auth":"user","description":"Double opt-in: queues a `news-confirm` email whose signed link (3 days, newest only) turns consent `on` once clicked. At most one email per five minutes; a repeat inside that window answers `pending` and sends none.","security":[{"clerkSession":[]}],"responses":{"200":{"description":"Already `on`; nothing sent.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"on"}},"required":["status"],"additionalProperties":false}}}},"202":{"description":"`pending`: the confirmation email is queued (or was, under five minutes ago).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"pending"}},"required":["status"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk or `NEWS_SIGNING_SECRET` isn't configured for this target (`news_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"delete_api_news","summary":"Stop Product news","x-auth":"user","description":"Turns consent `off` at once and queues the deletion of the person's Resend contact, which ends any Automation in flight.","security":[{"clerkSession":[]}],"responses":{"200":{"description":"Off.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"off"}},"required":["status"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured for this target (`auth_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/webhooks/clerk":{"post":{"operationId":"post_api_webhooks_clerk","summary":"Receive Clerk events","x-auth":"webhook","description":"Called by Clerk through Svix. Each delivery runs once: a redelivered `svix-id` answers 200 with `duplicate: true` without running again. `email.created` (with \"Delivered by Clerk\" off) queues Clerk's rendered email for sending through Resend; one Clerk already delivered is ignored.\n\nCalled by the provider; the request is verified by its signature.","security":[],"responses":{"200":{"description":"Handled, ignored (an event type the product doesn't use) or a duplicate delivery.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"duplicate":{"type":"boolean"},"ignored":{"type":"string"}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"The Svix signature is missing, invalid or stale.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"No signing secret is set, or handling failed (the `email` queue included); Svix retries.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/webhooks/resend":{"post":{"operationId":"post_api_webhooks_resend","summary":"Receive Resend events","x-auth":"webhook","description":"Called by Resend through Svix. Consent Resend took away goes back to D1 as `off`: `contact.updated` with `unsubscribed`, `contact.deleted`, `contact.topics.updated` opting out of Product news, and `suppression.added`. Each delivery runs once: a redelivered `svix-id` answers 200 with `duplicate: true`.\n\nCalled by the provider; the request is verified by its signature.","security":[],"responses":{"200":{"description":"Handled, ignored (an event type the product doesn't use) or a duplicate delivery.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"duplicate":{"type":"boolean"},"ignored":{"type":"string"}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"The Svix signature is missing, invalid or stale.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"No `RESEND_WEBHOOK_SECRET` is set, or handling failed; Svix retries.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/files/download":{"get":{"operationId":"get_api_files_download","summary":"Download a file through a signed link","x-auth":"public","description":"The link that a route such as `GET /api/notes/{id}/attachment` hands out. It needs no session and works for whoever holds it until it expires (5 minutes at most). The file always downloads (`Content-Disposition: attachment`, `nosniff`).","security":[],"parameters":[{"name":"key","in":"query","required":true,"schema":{"type":"string","description":"The file's storage key, as the link carries it."}},{"name":"expires","in":"query","required":true,"schema":{"type":"string","description":"Expiry, in Unix seconds."}},{"name":"signature","in":"query","required":true,"schema":{"type":"string","description":"HMAC-SHA256 of key and expiry."}}],"responses":{"200":{"description":"The file's bytes, with its content type."},"403":{"description":"The link is expired, or its key, expiry or signature was edited."},"404":{"description":"The file no longer exists."},"503":{"description":"`STORAGE_SIGNING_SECRET` isn't set for this target.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/billing/checkout":{"post":{"operationId":"post_api_billing_checkout","summary":"Start a Stripe Checkout","x-auth":"user","description":"Opens a Stripe Checkout Session (Managed Payments) for the signed-in subject: a plan from the catalog by its key, or a credit pack under the `credits` model. Prices come from the catalog by lookup key; the client never names a Stripe price. The plan is granted only once Stripe's signed webhook confirms it.","security":[{"clerkSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","minLength":1,"maxLength":64},"interval":{"type":"string","enum":["month","year"]}},"required":["plan"],"additionalProperties":false},{"type":"object","properties":{"pack":{"type":"string","minLength":1,"maxLength":64}},"required":["pack"],"additionalProperties":false}]}}}},"responses":{"201":{"description":"Redirect the browser to `url`, Stripe's hosted Checkout.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"],"additionalProperties":false}}}},"400":{"description":"The body is neither `{ plan, interval? }` nor `{ pack }` (`invalid_checkout`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Billing is off for this target: no Stripe key, or a key of the wrong mode (`billing_off`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"The subject already has a subscription that grants access; change it in the portal (`already_subscribed`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"No such plan, a plan this model can't sell, or packs under a model without credits (`unknown_plan`, `packs_not_sold`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured, Stripe has no active price for a lookup key (`price_not_found`), or Stripe failed (`billing_unavailable`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/billing/portal":{"post":{"operationId":"post_api_billing_portal","summary":"Open the Stripe customer portal","x-auth":"user","description":"A Stripe customer portal session on the subject's own Customer: payment methods, invoices, cancelling. It returns to `/app/billing`.","security":[{"clerkSession":[]}],"responses":{"201":{"description":"Redirect the browser to `url`.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Billing is off for this target (`billing_off`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"The subject has never bought, so it has no Stripe Customer (`no_billing_account`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured, or Stripe failed (`billing_unavailable`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/webhooks/stripe":{"post":{"operationId":"post_api_webhooks_stripe","summary":"Receive Stripe events","x-auth":"webhook","description":"Called by Stripe, signed with the endpoint's secret (`Stripe-Signature`, five minutes' tolerance). It is the only writer of billing state. Each event runs once: a redelivered event id answers 200 with `duplicate: true`, and an event older than the stored state never overwrites it.\n\nCalled by the provider; the request is verified by its signature.","security":[],"responses":{"200":{"description":"Handled, ignored (an event type or Customer billing doesn't act on) or a duplicate delivery.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"duplicate":{"type":"boolean"},"ignored":{"type":"string"}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"The signature is missing, invalid or stale (`invalid_signature`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Billing is off for this target (`billing_off`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"No signing secret is set, or handling failed or must wait for an earlier event; Stripe retries.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/notes":{"post":{"operationId":"post_api_notes","summary":"Submit a note","x-auth":"user","description":"Queues the note for processing and answers before it is stored; it appears in the list once the queue consumer has run. The note belongs to the signed-in user.","security":[{"clerkSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":1000}},"required":["body"]}}}},"responses":{"202":{"description":"The note is queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["queued"],"additionalProperties":false}}}},"400":{"description":"The note is empty or longer than 1000 characters."},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured for this target (`auth_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}},"get":{"operationId":"get_api_notes","summary":"List your notes","x-auth":"user","description":"The signed-in user's 50 most recent stored notes, newest first.","security":[{"clerkSession":[]}],"responses":{"200":{"description":"The stored notes.","content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string"},"body":{"type":"string"},"submittedAt":{"type":"string"},"processedAt":{"type":"string"},"attachmentKey":{"type":["string","null"]},"attachmentName":{"type":["string","null"]},"attachmentSize":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["id","userId","body","submittedAt","processedAt","attachmentKey","attachmentName","attachmentSize"],"additionalProperties":false}}},"required":["notes"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured for this target (`auth_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/api/notes/{id}/attachment":{"put":{"operationId":"put_api_notes_id_attachment","summary":"Attach a file to your note","x-auth":"user","description":"The request body is the file itself, with its `Content-Type` and a `Content-Length` of at most 10 MiB. A note holds one file; a new one replaces and deletes the old. The file is stored under the subject: the user in b2c, the active Organization in b2b.","security":[{"clerkSession":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The note's id."}},{"name":"name","in":"query","required":false,"schema":{"description":"The file name shown on the note and used for the download.","type":"string"}}],"responses":{"201":{"description":"The file is attached.","content":{"application/json":{"schema":{"type":"object","properties":{"attachment":{"type":"object","properties":{"name":{"type":"string"},"size":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["name","size"],"additionalProperties":false}},"required":["attachment"],"additionalProperties":false}}}},"400":{"description":"The file is empty, or the body doesn't match its Content-Length.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"No such note of yours, or its file belongs to another Organization.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"411":{"description":"No Content-Length.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"413":{"description":"The file is larger than 10 MiB.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk isn't configured for this target (`auth_not_configured`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}},"get":{"operationId":"get_api_notes_id_attachment","summary":"Get a download link for your note's file","x-auth":"user","description":"A signed `GET /api/files/download` link that works for `expiresIn` seconds (default and maximum 300).","security":[{"clerkSession":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The note's id."}},{"name":"expiresIn","in":"query","required":false,"schema":{"description":"Seconds the link works, 1 to 300.","type":"string"}}],"responses":{"200":{"description":"The link and when it stops working.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"expiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"}},"required":["url","expiresAt"],"additionalProperties":false}}}},"400":{"description":"`expiresIn` is outside 1-300.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"No valid session (`sign_in_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"b2b only: signed in with no active Organization (`organization_required`).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"No such note of yours, or it has no file.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Clerk or `STORAGE_SIGNING_SECRET` isn't configured for this target.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}}},"components":{"securitySchemes":{"clerkSession":{"type":"http","scheme":"bearer","description":"A Clerk session token. Same-origin browser calls send the `__session` cookie instead."}}}}