Reactivate an archived staff member (admin, or manage_staff)
/api/providers/{id}/reactivateThe exact inverse of POST /api/providers/{id}/archive, and it takes no request body at all: archiving kept everything except the photo, so book_reactivate_provider() only has to clear archived_at and, where a login was linked, that membership row's suspended_at.
The card is checked first (it exists here and is archived), so a request refused for its own reasons never reaches Stripe. Then the seat is reserved, before the person is restored, so an org that has since filled its plan refuses cleanly with the same 402 a brand-new hire would get rather than half-reactivating somebody the plan can no longer afford. The plan's limit is held under a per-company lock (migration 20261003170000), so several reactivations at once (Bulk Reactivate) never pass it, and the Stripe quantity is set from the real count afterwards.
Archiving ended the member's sessions and, when they had no other business to work in, blocked their sign-in; reactivation lifts that block, so they sign in again to get back in. A member who works in another business was moved there instead and can switch back once reactivated.
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
Provider id.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/providers/string/reactivate"{ "ok": true}Archive a staff member (admin, or manage_staff) POST
The third state alongside active and deleted, and the one to reach for when somebody leaves: unlike DELETE it works on a staff member who has bookings, and unlike the free `active` toggle it stops the seat being billed. Deliberately reversible. Only the photo is thrown away; bio, phone, service links and working hours all survive untouched, and a linked login is suspended rather than unlinked, so POST /api/providers/{id}/reactivate has nothing to re-enter or re-link. All of it happens inside one `book_archive_provider()` call (migration 0086), so a partial failure cannot leave a login suspended with the card still reading as active, or the reverse. The person leaves the seat count (`seatsUsed()` excludes them) and joins a separate archived count, free on every plan up to the plan's own limit (`SeatPlan.archivedLimit` in billing/config.ts: Free 3, Solo 5, Growth 10, Pro 20, Custom unlimited). At the limit this answers 402, and nothing already archived is ever removed to make room. The tier is re-read from the database here rather than trusted from the caller.
Upload a staff photo POST
Byte-for-byte the service-image route with the nouns swapped: bucket `staff-photos`, path `{companyId}/{providerId}`, same verify-then-upload ordering (the id is validated and the row confirmed to be the caller's before anything is written).