Upload a staff photo
/api/providers/{id}/avatarByte-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).
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.
Request Body
multipart/form-data
The staff photo. Sent as multipart/form-data under the field name file.
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/providers/string/avatar" \ -F file="string"{ "ok": true, "avatarUrl": "http://example.com"}Reactivate an archived staff member (admin, or manage_staff) POST
The 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.
Remove a staff photo DELETE
Deletes the stored object and nulls `avatar_url`.