Record a sale
/api/salesThe shared "Add item" cart BeNotely uses for both a standalone walk-in "New sale" (no bookingTable/bookingId) and a booking's close-out (both set, as a pair: CloseOutDialog's own POST). No hasPermission gate: recording a sale is ordinary front-of-house work, the same posture PATCH /api/appointments/{id} and /api/reservations/{id} already take for closing out a booking. Calls book_record_sale() (service_role), which atomically inserts the sale and its line items and, for each product line, decrements stock via book_product_stock_adjust(): either the whole sale and every stock delta commit, or none do.
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
curl -X POST "https://example.com/api/sales" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "kind": "service", "name": "string", "unitPriceCents": 0, "quantity": 1 } ] }'{ "sale": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "company_id": "b2e6a1c3-1a5e-44ae-a8fd-81f76fd715cf", "booking_table": "book_appointments", "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a", "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e", "subtotal_cents": 0, "discount_type": "percent", "discount_value": 0, "discount_cents": 0, "total_cents": 0, "gift_card_applied_cents": 0, "tip_cents": 0, "paid_in_person_cents": 0, "payment_method": "cash", "status": "completed", "void_reason": "string", "voided_at": "2019-08-24T14:15:22Z", "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4", "created_at": "2019-08-24T14:15:22Z", "sold_by_provider_id": "0aff90c0-8294-4110-8a4e-bbd4f49eb7ff" }}A customer answers a sent form (public, unauthenticated) PUT
The guest side of the standalone Forms feature. The token is the entire credential, same posture the manage-token routes take (0018): every write is scoped to the company_id resolved FROM the token, never from the request body. Replace semantics: a customer reopening the link edits their answers rather than stacking a second submission. No GET: src/app/forms/[token]/page.tsx loads the send directly on the server with its own service-role read.
Void a sale POST
Reverses the sale's stock ONLY (`book_void_sale()`, reason `sale_undo`) and marks it voided: it does not un-collect any payment or reverse a gift-card redemption, the same "no undo here, correct it the way any other mistake is corrected" posture GiftCardRedeemField's own redemption already takes. No `hasPermission` gate, matching POST /api/sales.