Start an export (admin, active subscription only)
/api/data/jobsQueues or runs an export of this org's clients or bookings.
THE PLAN BOUNDARY LIVES HERE, and it is a SUBSCRIPTION-STATUS gate rather than a PlanFeature one, which no other route in this API does. exportAccess() in src/lib/plan.ts reads book_billing.subscription_status raw: active passes, trialing is refused, and everything else (past_due, canceled, or no billing row at all) is refused with different copy. A trial deliberately fails even though it grants every other paid feature, because taking the data out is not what a trial is for. The hub disables its own buttons for the same three states, but that is cosmetic in the sense require-member.ts means it.
Small exports run inline and are downloadable the moment this returns (queued: false); anything over 50 rows becomes a data_job on book_job_queue for the worker to drain.
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
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
curl -X POST "https://example.com/api/data/jobs" \ -H "Content-Type: application/json" \ -d '{ "entity": "clients" }'{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "queued", "queued": true}List this org's recent imports and exports (admin) GET
The Import & export hub's history, newest first, capped at 25. Reads `book_data_jobs` (migration 0100) through the service-role client, scoped explicitly by `company_id`. Every finished export carries a freshly minted `downloadUrl`: a signed URL into the PRIVATE `data-exports` bucket, valid for 15 minutes. It is generated per request and stored nowhere, because it is a bearer credential for a file containing the venue's entire client list. Sweeps stalled jobs as a side effect (`flagStalledJobs`), which is how a worker that died mid-import becomes visible without a cron of its own. Never 500s on a missing table: migrations here are applied by hand AFTER the deploy, so between the two this route answers 200 with `unavailable: true` rather than a stack trace nobody can act on.
Dry-run an import file (admin) POST
Reads an uploaded file, guesses the column alignment, validates every row against the real database, and **writes nothing at all**: not a client, not a booking, and not even the uploaded file. Storing nothing is the decision worth knowing about. The obvious design uploads once and re-validates against the stored copy each time the operator changes a column; the cost is a bucket slowly filling with the abandoned first attempts of every venue that opened this screen and thought better of it, each one somebody's entire client list. The browser already holds the file, so it re-sends it. **Not gated on billing status**, unlike export: importing is how a venue arrives, and refusing it during a trial would be refusing the customer. A `mapping` sent back with the file WINS over the auto-guess, so an operator's corrections survive touching a second column. It is filtered against this file's real headers and this entity's real fields before use.