Shared

List this org's recent imports and exports (admin)

get/api/data/jobs

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.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

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

Response Body

application/json

application/json

application/json

curl -X GET "https://example.com/api/data/jobs"
{  "jobs": [    {      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",      "kind": "import",      "entity": "clients",      "format": "csv",      "status": "queued",      "sourceName": "string",      "summary": {},      "error": "string",      "needsSupport": true,      "supportRequestedAt": "2019-08-24T14:15:22Z",      "createdAt": "2019-08-24T14:15:22Z",      "finishedAt": "2019-08-24T14:15:22Z",      "downloadUrl": "http://example.com"    }  ],  "unavailable": true,  "error": "string"}

Delete your own account DELETE

Danger zone. Unlike a one-account-per-company product, a Booking org can have several members, so deleting YOUR account does not always take the workspace with it: - **Last member standing**: nobody would be left who could ever sign in and manage or delete the org, so the whole workspace is deleted too. - **Teammates remain**: only this user's membership and personal data go; the workspace is shared, not owned. - **Sole admin with staff remaining**: refused with 400. Promote a teammate first. This mirrors the "cannot remove the last admin" rule on DELETE /api/members/{id}, so an admin cannot self-delete into an org nobody can administer. When the workspace does go, its live Stripe subscriptions are cancelled immediately, before the database cascade, and that cancellation has to succeed for the deletion to proceed (see the 502). The Stripe customer and its invoices are deliberately NOT deleted: Australian tax law requires seven years of sales records, so erasure covers the account and retention covers the invoices. This route is exempted from the onboarding redirect in middleware so a mid-onboarding account can still delete itself.

Start an export (admin, active subscription only) POST

Queues 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.