The onboarding chain, as a chain

Admin creates the merchant, merchant invites the staff, staff redeem
the code on a phone. Every endpoint for it already existed and was
already documented - scattered across four sections in the order the
server groups them, not the order a person meets them.

Now one section, in tier order, each step with the request that makes
it and the response it hands to the next tier: the owner password shown
once, the invitation code shown once, the session returned by register
so a new salesperson is never sent to a login form. The status codes
were checked against the handlers: all three creations are 201.

Three absences named rather than left to be found: a merchant cannot
create a staff login directly (invitation only, on purpose); there is
no mobile app in this repository, only the API it will call; and an
admin cannot reset an owner's password or suspend a merchant over HTTP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-11 11:54:47 +05:30
parent 0a423ed8cc
commit c50a74de47

132
API.md
View File

@@ -63,6 +63,138 @@ has no business learning that a resource exists.
---
## The onboarding chain — who creates whom
Three tiers. Each one creates the login for the next, and nobody ever creates
their own from nothing.
```
┌──────────────────┐ creates ┌──────────────────┐ invites ┌──────────────────┐
│ Platform admin │ ───────────▶ │ Merchant owner │ ───────────▶ │ Sales staff │
│ (web) │ company + │ (web) │ code, │ (mobile) │
│ │ owner login │ │ shown once │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
POST /api/admin/clients POST /api/team/invitations POST /api/auth/register
```
### Tier 1 — the platform admin registers a merchant
Signed in as an account with `role: "admin"` and no company.
```
POST /api/auth/login
{ "email": "admin@loyaly.ai", "password": "…", "device": "Admin console" }
POST /api/admin/clients
{ "company_name": "TeNext Retail", "owner_email": "suriya@tenext.in",
"owner_name": "Suriya" }
→ 201 { "client_id": "…", "slug": "tenext-retail",
"owner_email": "suriya@tenext.in", "password": "xK9…" }
```
`password` is generated and shown **once**. The admin hands the owner their
email and that password — that is the merchant login. Leave `password` out of
the request; a password an operator invents for someone else is weak and
travels over chat.
```
GET /api/admin/clients → every merchant, with site and user counts
```
### Tier 2 — the merchant owner registers sales staff
Signed in as the owner (or any manager).
```
POST /api/auth/login
{ "email": "suriya@tenext.in", "password": "xK9…", "device": "Head office" }
POST /api/team/invitations
{ "email": "priya@tenext.in", "full_name": "Priya R", "role": "staff",
"expires_in_days": 7 }
→ 201 { "id": "…", "email": "priya@tenext.in", "role": "staff",
"code": "LQOUHR-AYYTPE-7Q756N-PGAAN6", "expires_at": "…" }
```
`code` is shown **once** and is what the merchant gives the salesperson —
read aloud, WhatsApp, printed on a card. It is single-use and expires. The
salesperson chooses their own password when they redeem it (Tier 3), so the
merchant never knows or handles a staff password.
Managing the team afterwards:
```
GET /api/team → everyone: role, active, last login
GET /api/team/invitations → codes still unredeemed (without the code)
DELETE /api/team/invitations/{id} → withdraw one before it is used
PATCH /api/team/{id} { "role": "manager" } → promote
PATCH /api/team/{id} { "active": false } → they have left; signs them out now
```
The owner also sets the shop up from the same login — `POST
/api/sites/{site}/enrolment-code` for the shop PC, `POST
/api/sites/{site}/cameras` for cameras — see §8.
### Tier 3 — the salesperson gets their mobile login
Not signed in yet. They have the code.
```
GET /api/auth/invitation?code=LQOUHR-AYYTPE-7Q756N-PGAAN6 (no auth)
→ { "client_name": "TeNext Retail", "email": "priya@tenext.in",
"full_name": "Priya R", "role": "staff" }
```
Show *"Join TeNext Retail as Priya R"* and ask for a password. Then:
```
POST /api/auth/register (no auth)
{ "code": "LQOUHR-AYYTPE-7Q756N-PGAAN6",
"full_name": "Priya R", "password": "…", "device": "Pixel 8" }
→ 201 { "access_token": "…", "refresh_token": "…", "expires_at": "…",
"user": { …, "role": "staff", "client_name": "TeNext Retail" } }
```
**They are signed in.** Do not send them to a login form. From the next day:
```
POST /api/auth/login { "email": "priya@tenext.in", "password": "…", "device": "Pixel 8" }
POST /api/auth/refresh { "refresh_token": "…", "device": "Pixel 8" }
```
And the screens a salesperson uses — all `staff` may call:
```
GET /api/visits?limit=30 then ?cursor=… who just walked in
GET /api/visits/stream the same, pushed
GET /api/visitors?q=priya find a customer
GET /api/visitors/V-42/history their past visits
PUT /api/visitors/V-42/profile give them a name
POST /api/purchases record a sale
GET /api/visitors/V-42/image their photo, if any
```
Do **not** send `email` or `role` on register — they come from the code, and a
body naming either is refused. That is what stops a forwarded code becoming
somebody else's account.
### What does not exist, stated plainly
- **The merchant cannot create a staff login directly** with a password of
their choosing. The only path is an invitation code the staff member redeems
themselves. This is deliberate — it keeps staff passwords out of the
merchant's hands and off chat — but it means a salesperson without a phone in
hand cannot be set up *for* them. If that friction is real, a
`POST /api/team/members` that mirrors `POST /api/admin/clients` (generated
password, shown once) is a small addition.
- **There is no mobile app in this repository.** Tier 3 is a complete API with
no client yet. Everything above is what that app will call.
- **The admin cannot reset a merchant owner's password over HTTP**, nor suspend
or delete a merchant. Today that is `behavision-server provision` on the
server.
---
## Quick starts
### A mobile app for shop-floor staff