Begin transferring ownership of the organization
/api/organization/transferMoves 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.
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
application/json
application/json
curl -X POST "https://example.com/api/organization/transfer" \ -H "Content-Type: application/json" \ -d '{ "email": "string" }'{ "ok": true}Resend a receipt or tax invoice (admin) POST
What 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.
Confirm a pending ownership transfer (owner, via emailed link) POST
**POST, never GET, and that is a security decision rather than REST manners.** This URL arrives in an inbox, and inboxes are full of things that fetch URLs without a human: Outlook Safe Links, mail scanners, Slack unfurls, antivirus proxies. A mutating GET would let a scanner start the cooling-off clock AND burn the single-use token, so the real owner's click would report "already used" and they could not tell a scanner from an attacker. The emailed link lands on a page, which posts here on a real click. **Two factors are required: the token AND the owner's own session.** A link that leaked; forwarded, logged by a mail gateway, in a screenshot; is inert on its own. Single use is structural: confirming nulls the whole token triple, so there is no `used` flag anyone can forget to check. Comparison is constant-time against a real digest even on a miss, and every failure returns one identical message so a leaked link cannot be used to map out why it failed.