Autosave a campaign draft (admin, or manage_org_settings)
/api/marketing/campaigns/{id}Accepts a PARTIAL object: only the keys present are validated and written, so one keystroke never re-sends every field. .eq('status','draft') is a second WHERE clause, not a separate read-then-write: a campaign that started sending between page load and this autosave firing must not have its content rewritten out from under the send in flight. 409 is deliberately ambiguous between "no such campaign for this company" and "it already left draft"; the compose page's own status banner is what actually tells the operator which one happened.
Authorization
sessionCookie The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across
numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.
Every request re-validates it against the Auth server (getUser()), never by decoding the cookie
locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified
app_metadata.company_id claim and enforced by row-level security; it is never read from request
input, on any route, ever.
role (admin / staff) is deliberately not in RLS. It gates specific actions in route code,
the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's
own check is the enforcement.
In: cookie
Path Parameters
Campaign id.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X PATCH "https://example.com/api/marketing/campaigns/string" \ -H "Content-Type: application/json" \ -d '{}'{ "ok": true}Start a new campaign draft (admin, or manage_org_settings) POST
Inserts an empty `book_campaigns` row (every column already carries its own DB default, see migration 0144) and returns its id. The client navigates to /dashboard/marketing/campaigns/{id} on success; deliberately not a page-level redirect, since a GET-rendered page performing this insert would fire on Next.js Link prefetch.
Delete a campaign draft (admin, or manage_org_settings) DELETE
Hard delete, and ONLY of a draft: a sent (or sending) campaign's book_campaign_recipients rows are the audit record of a consent-gated send, and `on delete cascade` (migration 0144) would take them with it. Same 409-is-ambiguous reasoning as the PATCH on this path.