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

NameInRequiredSchema
keyqueryyes{"type":"string","description":"The file's storage key, as the link carries it."}
expiresqueryyes{"type":"string","description":"Expiry, in Unix seconds."}
signaturequeryyes{"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

NameInRequiredSchema
idpathyes{"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."}
namequeryno{"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

NameInRequiredSchema
idpathyes{"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."}
expiresInqueryno{"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
}