Resend a receipt or tax invoice (admin)
/api/payments/receipts/{id}/resendWhat the Resend button on the Transactions tab calls. A receipt is minted once, automatically, the moment a deposit is confirmed paid (the Stripe webhook's issueReceipt, migration 0126); this route re-sends the SAME numbered document rather than generating a new one, so the receipt number never changes and the email never gets a second entry in the venue's own sequence.
Synchronous, not queued. The automatic send at issue time goes through the background worker (receipt_send, off the webhook's response path), but a caller who clicks this button wants an answer now, the same distinction POST /api/payments/refunds already draws between the two.
Always actually re-sends, even if one already went out: the idempotency key used here is a fresh one per call rather than the stable per-receipt key the automatic path uses, because reusing that key would make Resend's own request-level dedupe silently swallow the second send. Clicking "Resend" and mailing nobody is the one failure mode this route cannot have.
Admin-only. No Idempotency-Key header requirement, unlike refunds: a duplicate resend costs a nuisance email, not a second charge out of the business's balance, so there is nothing here worth making a caller retry a whole request over.
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
book_receipts row id.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/payments/receipts/string/resend"{ "ok": true}Charge a card-on-file no-show fee (admin) POST
What the "Charge fee" action on `/payments`, a booking's own detail dialog, and the reservations/bookings lists all call. The one MANUAL step in the whole card-on-file mechanism (migration 0129): every route upstream of this one only ever SAVES a card at zero charge (`createSetupIntent`, the guest checkout routes), and nothing is ever charged until a human clicks the button that POSTs here, on a no-show or a late cancellation staff decided is chargeable. There is no cron, no trigger, nothing on a timer. **A direct charge on the connected account**, off-session against the card saved at booking. The fee amount snapshotted into `deposit_amount_cents` at booking time (the same column a real deposit uses) is the CEILING; a later policy change never moves it. **`amountCents` (migration 0137) is optional and, when sent, is clamped server-side** against that same `deposit_amount_cents` before it is trusted for anything: strictly greater than 0 and no larger than the deposit. Omitted or null keeps the pre-0137 behaviour exactly: the full frozen deposit, charged unconditionally. This is what lets the dialog charge less than the disclosed ceiling for a booking whose own cancellation notice earned a lighter tier (see `GET` above), while a suggested figure or any custom amount a staff member types is never trusted on the client's word alone. **An `Idempotency-Key` header is REQUIRED**, identical in shape to `/api/payments/refunds`: `withIdempotency` runs the route at most once per key and replays the first response, the same key goes to Stripe as its own idempotency option, and the ledger row carries the charged PaymentIntent id in `stripe_event_id` (this charge's own webhook delivery is a deliberate no-op, so there is no separate `evt_...` to dedupe against). **A fourth layer this route needs that refunds does not**: a compare-and-set claim (`card_on_file_status` → `'charging'`) *before* Stripe is ever called. A refund is safe from two racing keys because Stripe itself is the ceiling (a second refund attempt fails outright); a charge has no such ceiling: two off-session PaymentIntents against the same saved card, carrying two different keys, would both succeed at Stripe and charge the guest twice. The claim closes that window; a losing request never reaches Stripe at all. Admin-only, and that gate is the whole authorization: the button is hidden from a `staff` login, but hiding a button is cosmetic (see `src/lib/auth/require-member.ts`). Not engine-gated: `card_on_file_enabled` lives on `book_companies` for either vertical, and both booking tables carry the 0129 columns this route reads and writes.
Begin transferring ownership of the organization POST
Moves nothing. Creates an `awaiting_confirmation` row and emails a one-time link to the **current owner's own address**: that link is the second factor, so an attacker holding a hijacked session can reach this route but cannot read the owner's inbox. **Why an emailed link and not a password re-entry:** the product ships Google sign-in, and a Google account has no password to re-enter (the primary owner account on this deployment is one), so a password step would permanently lock out exactly the people most likely to own something. It would also be hazardous to implement; verifying a password on the cookie-bound server client mints a fresh session and writes new auth cookies onto the response, silently rotating the caller's session. Owner only, not any admin. The target must ALREADY be an admin of the organization; "no such account" and "not an admin here" return the same message deliberately, so this cannot be used to probe which addresses have Gaplessly accounts.