Commit an import (admin)
/api/data/importsStores the file in the private data-imports bucket, records the mapping the operator CONFIRMED, and either does the work inline (50 rows or fewer) or enqueues a data_job for the worker.
The mapping is stored rather than re-derived, because the auto-mapper is a heuristic that will change: a job re-run six months from now must apply what a human approved, not what this build would guess today.
The file is parsed here as well as in the runner. That is not waste: it is the only way to know the row count before choosing inline-vs-queued, and it means an unreadable file is a 400 the operator sees rather than a queued job failing somewhere they have to go looking for.
Not gated on billing status, same reasoning as /api/data/preview.
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
multipart/form-data
The file and the confirmed mapping. multipart/form-data.
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/data/imports" \ -F file="string" \ -F entity="clients" \ -F mapping="string"{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "status": "queued", "queued": true, "rows": 0, "summary": {}}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.
Ask for free migration help (admin) POST
The button on the proactive support banner. Stamps `support_requested_at` on one of this org's data jobs and pages us (console line plus `SUPPORT_WEBHOOK_URL`, if set). Deliberately small: it does not open a ticket in another system, email the customer, or promise a response time. The durable record is the row, the superadmin queue reads it, and a human replies. Scoped with an explicit `.eq('company_id')` on top of the id, because this route holds a service-role client and so has to spell out the tenancy that RLS would otherwise enforce. A zero-row update is a PostgREST 204 rather than an error, so the returned row (not the absence of an error) is what tells a real request from one naming somebody else's job.