API reference
Base URL https://preview.demo.meduza.digital. Requests and responses are JSON. The same reference is available as openapi.json.
Endpoints marked signed in need a Clerk session: browsers on this site send it as a cookie; other clients send Authorization: Bearer <session token>.
GET/api/health
Check that the Worker is up. Public.
Answers without touching D1 or any provider, so a smoke check can tell the Worker from its dependencies.
Responses
200 The Worker is serving, and the target it was built for.
{
"type": "object",
"properties": {
"ok": {
"type": "boolean",
"const": true
},
"target": {
"type": "string",
"enum": [
"local",
"preview",
"production"
]
}
},
"required": [
"ok",
"target"
],
"additionalProperties": false
}GET/api/me
Who is signed in. Signed in.
The caller's Clerk ids and the subject that owns data and pays: the user in b2c, the active Organization in b2b.
Responses
200 The signed-in caller.
{
"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 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured for this target (`auth_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}GET/api/news
Your Product news consent. Signed in.
Whether the signed-in person gets Product news (marketing email): `off`, `pending` (asked, not confirmed) or `on`.
Responses
200 The consent.
{
"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 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured for this target (`auth_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/news
Ask for Product news. Signed in.
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.
Responses
200 Already `on`; nothing sent.
{
"type": "object",
"properties": {
"status": {
"type": "string",
"const": "on"
}
},
"required": [
"status"
],
"additionalProperties": false
}202 `pending`: the confirmation email is queued (or was, under five minutes ago).
{
"type": "object",
"properties": {
"status": {
"type": "string",
"const": "pending"
}
},
"required": [
"status"
],
"additionalProperties": false
}401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk or `NEWS_SIGNING_SECRET` isn't configured for this target (`news_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}DELETE/api/news
Stop Product news. Signed in.
Turns consent `off` at once and queues the deletion of the person's Resend contact, which ends any Automation in flight.
Responses
200 Off.
{
"type": "object",
"properties": {
"status": {
"type": "string",
"const": "off"
}
},
"required": [
"status"
],
"additionalProperties": false
}401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured for this target (`auth_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/webhooks/clerk
Receive Clerk events. Provider webhook.
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. Called by the provider; the request is verified by its signature.
Responses
200 Handled, ignored (an event type the product doesn't use) or a duplicate delivery.
{
"type": "object",
"properties": {
"ok": {
"type": "boolean",
"const": true
},
"duplicate": {
"type": "boolean"
},
"ignored": {
"type": "string"
}
},
"required": [
"ok"
],
"additionalProperties": false
}400 The Svix signature is missing, invalid or stale.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 No signing secret is set, or handling failed (the `email` queue included); Svix retries.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/webhooks/resend
Receive Resend events. Provider webhook.
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`. Called by the provider; the request is verified by its signature.
Responses
200 Handled, ignored (an event type the product doesn't use) or a duplicate delivery.
{
"type": "object",
"properties": {
"ok": {
"type": "boolean",
"const": true
},
"duplicate": {
"type": "boolean"
},
"ignored": {
"type": "string"
}
},
"required": [
"ok"
],
"additionalProperties": false
}400 The Svix signature is missing, invalid or stale.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 No `RESEND_WEBHOOK_SECRET` is set, or handling failed; Svix retries.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}GET/api/files/download
Download a file through a signed link. Public.
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`).
Parameters
| Name | In | Required | Schema |
|---|---|---|---|
key | query | yes | {"type":"string","description":"The file's storage key, as the link carries it."} |
expires | query | yes | {"type":"string","description":"Expiry, in Unix seconds."} |
signature | query | yes | {"type":"string","description":"HMAC-SHA256 of key and expiry."} |
Responses
200 The file's bytes, with its content type.
403 The link is expired, or its key, expiry or signature was edited.
404 The file no longer exists.
503 `STORAGE_SIGNING_SECRET` isn't set for this target.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/billing/checkout
Start a Stripe Checkout. Signed in.
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.
Request body
{
"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 Redirect the browser to `url`, Stripe's hosted Checkout.
{
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
],
"additionalProperties": false
}400 The body is neither `{ plan, interval? }` nor `{ pack }` (`invalid_checkout`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}404 Billing is off for this target: no Stripe key, or a key of the wrong mode (`billing_off`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}409 The subject already has a subscription that grants access; change it in the portal (`already_subscribed`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}422 No such plan, a plan this model can't sell, or packs under a model without credits (`unknown_plan`, `packs_not_sold`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured, Stripe has no active price for a lookup key (`price_not_found`), or Stripe failed (`billing_unavailable`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/billing/portal
Open the Stripe customer portal. Signed in.
A Stripe customer portal session on the subject's own Customer: payment methods, invoices, cancelling. It returns to `/app/billing`.
Responses
201 Redirect the browser to `url`.
{
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
],
"additionalProperties": false
}401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}404 Billing is off for this target (`billing_off`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}409 The subject has never bought, so it has no Stripe Customer (`no_billing_account`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured, or Stripe failed (`billing_unavailable`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/webhooks/stripe
Receive Stripe events. Provider webhook.
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. Called by the provider; the request is verified by its signature.
Responses
200 Handled, ignored (an event type or Customer billing doesn't act on) or a duplicate delivery.
{
"type": "object",
"properties": {
"ok": {
"type": "boolean",
"const": true
},
"duplicate": {
"type": "boolean"
},
"ignored": {
"type": "string"
}
},
"required": [
"ok"
],
"additionalProperties": false
}400 The signature is missing, invalid or stale (`invalid_signature`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}404 Billing is off for this target (`billing_off`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 No signing secret is set, or handling failed or must wait for an earlier event; Stripe retries.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}POST/api/notes
Submit a note. Signed in.
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.
Request body
{
"type": "object",
"properties": {
"body": {
"type": "string",
"minLength": 1,
"maxLength": 1000
}
},
"required": [
"body"
]
}Responses
202 The note is queued.
{
"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 The note is empty or longer than 1000 characters.
401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured for this target (`auth_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}GET/api/notes
List your notes. Signed in.
The signed-in user's 50 most recent stored notes, newest first.
Responses
200 The stored notes.
{
"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 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured for this target (`auth_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}PUT/api/notes/{id}/attachment
Attach a file to your note. Signed in.
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.
Parameters
| Name | In | Required | Schema |
|---|---|---|---|
id | path | yes | {"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 | query | no | {"description":"The file name shown on the note and used for the download.","type":"string"} |
Responses
201 The file is attached.
{
"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 The file is empty, or the body doesn't match its Content-Length.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}404 No such note of yours, or its file belongs to another Organization.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}411 No Content-Length.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}413 The file is larger than 10 MiB.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk isn't configured for this target (`auth_not_configured`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}GET/api/notes/{id}/attachment
Get a download link for your note's file. Signed in.
A signed `GET /api/files/download` link that works for `expiresIn` seconds (default and maximum 300).
Parameters
| Name | In | Required | Schema |
|---|---|---|---|
id | path | yes | {"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."} |
expiresIn | query | no | {"description":"Seconds the link works, 1 to 300.","type":"string"} |
Responses
200 The link and when it stops working.
{
"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 `expiresIn` is outside 1-300.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}401 No valid session (`sign_in_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}403 b2b only: signed in with no active Organization (`organization_required`).
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}404 No such note of yours, or it has no file.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}503 Clerk or `STORAGE_SIGNING_SECRET` isn't configured for this target.
{
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"additionalProperties": false
}