# API surface

Every route in the application, grouped by who calls it. Generated from `php artisan route:list`
on **2026-09-18** (284 endpoints); regenerate rather than hand-edit when it drifts.

## How to read it

**Two auth models, and they are not interchangeable.**

- **Panels** are session + cookie. Three of them, one bundle each, separated by HOST rather than by
  path prefix: `ADMIN_DOMAIN`, `ORG_DOMAIN`, `USER_DOMAIN` (see `.env`). A panel route therefore
  looks unprefixed — `/flow/list` is the org panel's, `/booking/create` is the user's — and the
  host is what decides which. Guards: `auth:admin`, `auth:org`, `auth:app_user`.
- **`/api/admin/*` and `/api/org/*`** are Sanctum TOKEN endpoints for machines. Guards:
  `sanctum-admin`, `sanctum-org`. Several org panel routes are deliberate session-guarded *shims*
  over the same controllers, so the panel and an API client share one implementation.

There is no anonymous surface for a PERSON. The `contact` identity, its `sanctum-contact` guard and
the whole `/api/v1/*` widget were removed on 2026-08-06; registration is invitation-only.

- **`/hook/*` is the one unauthenticated write surface**, and it is for MACHINES: an organization's
  own systems pushing a result back into a conversation that is waiting for it. No session, no
  cookie, no CSRF. Authentication is an opaque per-tenant token in the URL plus an HMAC over the
  body — see **Webhooks** at the foot of this file, which is the published contract rather than a
  list of paths.

**Conventions** (`CLAUDE.md`, `docs/coding-style.md`):

- Verb-suffix actions, not REST resources: `/user/info/get`, `/org/flow/publish`.
- Responses are `{ data, message, status }` via `Controller::ok()` / `fail()`.
- Identifiers in payloads are `snake_case`; Mongo ids are 24-char strings and never ints.
- Platform-admin routes are additionally gated per feature by `check_right:<code>`.

**Not every endpoint has a UI caller, and that is not a defect.** Some are API-only by design;
others are UI that has not been built. The audit of which is which is T16 work and is not repeated
here — this file answers "what exists", not "what is reached".

### Platform admin panel — 70 endpoints

```
GET    /
POST   /admin/add
PUT    /admin/edit
GET    /admin/get
GET    /admin/list
GET    /admin_login_log/list
PUT    /admin_right/edit
GET    /admin_right/get
GET    /admin_right/list
POST   /admin_role/add
PUT    /admin_role/edit
GET    /admin_role/list
GET    /admin_setting/get
PUT    /admin_setting/update
POST   /check
GET    /conversation/get
GET    /conversation/list
GET    /csrf-refresh
GET    /dashboard/summary
GET    /flow/get
GET    /flow/list
GET    /flow_version/get
GET    /home
POST   /login
POST   /login/by-code
POST   /login/request-code
POST   /logout
GET    /media/get
POST   /media/upload
POST   /organization/create
PUT    /organization/edit
GET    /organization/get
GET    /organization/list
POST   /organization_detail_config/add
POST   /organization_detail_config/delete
PUT    /organization_detail_config/edit
GET    /organization_detail_config/list
GET    /organization_invitation/list
POST   /organization_invitation/resend
POST   /organization_invitation/revoke
POST   /organization_invitation/send
GET    /organization_login_log/list
POST   /platform_join_link/create
POST   /platform_join_link/delete
GET    /platform_join_link/list
POST   /platform_join_link/regenerate
POST   /platform_join_link/set-expiry
POST   /platform_join_link/toggle
PUT    /profile/change_password
GET    /profile/me
PUT    /profile/save
POST   /score/bump
GET    /score/history
GET    /score/leaderboard
GET    /score/today
GET    /service/get
GET    /service/list
POST   /service_detail_config/add
POST   /service_detail_config/delete
PUT    /service_detail_config/edit
GET    /service_detail_config/list
GET    /timer/list
PUT    /user/edit
GET    /user/get
GET    /user/list
POST   /user_detail_config/add
POST   /user_detail_config/delete
PUT    /user_detail_config/edit
GET    /user_detail_config/list
GET    /user_login_log/list
```

### Org panel — 129 endpoints

```
GET    /
POST   /appointment/cancel
GET    /appointment/list
POST   /appointment/reschedule
POST   /appointment/status
POST   /booking/cancel
GET    /booking/list
GET    /calculation/get
GET    /calculation/list
POST   /check
POST   /conversation/assign-member
GET    /conversation/get
GET    /conversation/list
POST   /conversation/messages/{id}
GET    /conversation/participants
POST   /conversation/reject-payment
POST   /conversation/remove-member
POST   /conversation/reply
POST   /conversation/send-choice
POST   /conversation/send-invoice
POST   /conversation/send-reschedule
POST   /conversation/verify
POST   /conversation/verify-payment
GET    /csrf-refresh
GET    /dashboard/summary
GET    /detail/get
PUT    /detail/save
POST   /document_template/delete
GET    /document_template/list
GET    /document_template/produced
POST   /document_template/save
POST   /external_form/attach
GET    /external_form/connector
POST   /external_form/dismiss
GET    /external_form/get
POST   /external_form/retry
POST   /external_form/rotate
GET    /external_form/stuck
GET    /external_form/submissions
POST   /external_form/toggle
GET    /external_form/waiting
POST   /external_wait/answer
GET    /external_wait/list
DELETE /flow/archive/{id}
POST   /flow/create
GET    /flow/get/{id}
GET    /flow/list
POST   /flow/publish/{id}
POST   /flow/restore/{id}
POST   /flow/set-kind/{id}
PUT    /flow/update/{id}
POST   /form_template/create
POST   /form_template/delete
PUT    /form_template/edit
GET    /form_template/list
GET    /home
POST   /invitation/lookup
POST   /join_link/create
POST   /join_link/delete
GET    /join_link/list
POST   /join_link/regenerate
POST   /join_link/set-expiry
POST   /join_link/toggle
POST   /login
POST   /login/by-code
POST   /login/request-code
POST   /logout
POST   /matching/choose
GET    /matching/list
GET    /matching/records
GET    /member/list
GET    /member_detail/get
PUT    /member_detail/save
POST   /message_template/create
POST   /message_template/delete
PUT    /message_template/edit
GET    /message_template/list
GET    /message_template/placeholders/list
GET    /notification/list
POST   /notification/read
POST   /notification/read-all
POST   /notification/read-by
GET    /notification/unread-count
GET    /org/me
GET    /payment/list
GET    /payment_info/get
POST   /payment_info/qr/upload
PUT    /payment_info/save
POST   /register
GET    /render/health
PUT    /resource/availability
POST   /resource/create
POST   /resource/delete
PUT    /resource/edit
GET    /resource/list
PUT    /resource/special_dates
POST   /service/create
PUT    /service/edit
GET    /service/get
GET    /service/list
POST   /service/participant/remove
GET    /service/participants
POST   /service/status/set
GET    /service_detail/get
PUT    /service_detail/save
POST   /template_category/adopt
POST   /template_category/create
POST   /template_category/delete
GET    /template_category/list
PUT    /template_category/rename
POST   /user_detail_config/create
POST   /user_detail_config/delete
PUT    /user_detail_config/edit
GET    /user_detail_config/list
POST   /user_type/assign-user
GET    /user_type/assigned-users
POST   /user_type/create
PUT    /user_type/edit
GET    /user_type/list
GET    /user_type/members
POST   /user_type/toggle
POST   /user_type/unassign-user
GET    /welcome/get
POST   /welcome/seen
POST   /workbook_template/create
POST   /workbook_template/delete
PUT    /workbook_template/edit
GET    /workbook_template/get
GET    /workbook_template/list
```

### User panel — 60 endpoints

```
GET    /
GET    /appointment/list
GET    /availability/list
POST   /booking/create
GET    /booking/list
POST   /booking/reschedule
POST   /check
GET    /conversation/flows
GET    /conversation/get
GET    /conversation/list
POST   /conversation/start
POST   /conversation/submit
POST   /conversation/upload
GET    /csrf-refresh
GET    /dashboard/summary
GET    /detail/get
PUT    /detail/save
GET    /home
POST   /invitation/accept
POST   /invitation/check-email
POST   /invitation/login-and-attach
POST   /invitation/lookup
POST   /invitation/request-code
POST   /invitation/verify-and-register
POST   /login
POST   /login/by-code
POST   /login/request-code
POST   /logout
GET    /matching/candidates
POST   /matching/choose
GET    /matching/list
POST   /matching/take
GET    /notification/list
POST   /notification/read
POST   /notification/read-all
POST   /notification/read-by
GET    /notification/unread-count
GET    /org_detail/get
GET    /org_detail/list
PUT    /org_detail/save
GET    /payment/list
POST   /payment/upload-receipt
GET    /service/get
POST   /service/join
POST   /service/leave
GET    /service/list
GET    /service/resources
GET    /thread/get
GET    /thread/list
POST   /thread/pin
POST   /thread/respond-choice
POST   /thread/send
POST   /thread/unpin
GET    /timetable/get
PUT    /timetable/row/remove
PUT    /timetable/row/save
PUT    /timetable/save
GET    /user/me
GET    /welcome/get
POST   /welcome/seen
```

### Platform API (token) — 6 endpoints

```
GET    /api/admin/health
POST   /api/admin/organization/create
GET    /api/admin/organization/get
GET    /api/admin/organization/list
POST   /api/admin/organization/reactivate
POST   /api/admin/organization/suspend
```

### Org API (token) — 12 endpoints

```
GET    /api/org/conversation/get
GET    /api/org/conversation/list
POST   /api/org/conversation/send-message
POST   /api/org/conversation/verify
POST   /api/org/flow/create
POST   /api/org/flow/delete
POST   /api/org/flow/edit
GET    /api/org/flow/get
GET    /api/org/flow/list
POST   /api/org/flow/publish
POST   /api/org/flow/restore
POST   /api/org/flow/set-kind
```

### Shared / framework — 7 endpoints

```
GET    /api/common/{option}
GET    /api/health
POST   /hook/external-form/{token}
POST   /hook/external-form/{token}/document
GET    /sanctum/csrf-cookie
GET    /storage/{path}
GET    /up
```

---

## Webhooks — the inbound contract (FP-T2)

**Published, and therefore a promise.** Once an organization's script runs against this, its
versioning and back-compatibility are ours to carry. Everything below is the contract; the script
that speaks it is `resources/connector/plantoo-external-form.gs`, and we ship that rather than
asking each org to implement it.

**Plantoo never calls out.** Every hop is inbound, started by the org. We do not poll a form, read
a Drive, or fetch a document. `tests/Unit/NoOutboundHttpTest.php` is that rule, written as a test.

```
POST /hook/external-form/{integration_token}            a submission came back
POST /hook/external-form/{integration_token}/document    the document that followed it
```

Both are on **every host** — the caller is a script and has no idea which panel it belongs to —
and both are throttled (`throttle:webhook`, keyed on the token).

**Headers, all three required**

```
X-Plantoo-Timestamp   unix seconds; refused if more than 300s from ours
X-Plantoo-Signature   sha256=<hex hmac of "{timestamp}.{raw body}" keyed with the org's secret>
X-Plantoo-Connector   the connector script's version, recorded so the panel can say what is deployed
```

Signed over the timestamp AND the body together: a captured signature cannot be replayed onto a
different body, and a captured body cannot be replayed at a different time.

**Body — `/hook/external-form/{token}`**

```jsonc
{ "event_id":  "2hJ…",                 // THEIR id for this delivery; idempotency is on it
  "reference": "K7R4M-9WXNP-2F8HJ-QTVD3",
  "answers":   { "Final pay": "12400" } }
```

**The form brings ANSWERS and nothing else.** An optional `calculation` block — figures an org's own
Apps Script had worked out — was accepted here until 16/09/2026, alongside a per-service switch
deciding whose figure a customer could be told. Both went with the C1 → C2 → D2 ladder they were
built for: **a figure a chat quotes is worked out by a workbook kept here**, by a `form` step that
names it, run in our own worker. The shipped connector never sent a `calculation`, so no
organization's script has to change. Extra keys are ignored rather than refused, and every figure
still lands in `calculation_record` with its inputs, outputs and workbook version.

**Body — `…/document`**

```jsonc
{ "event_id": "2hJ…-doc", "reference": "K7R4M-…",
  "name": "statement.pdf", "mime": "application/pdf",
  "content": "<base64>", "sha256": "<hex>" }     // base64, not multipart: the signature is over the raw body
```

**What comes back**

| Status | When |
|---|---|
| 200 | Recorded. `data.result` is `resumed`, `unmatched`, `attached`, `received` or `failed`, with `data.reason` when it is not `resumed` |
| 401 | Unknown or disabled token, bad signature, or a timestamp outside the window. Nothing is recorded |
| 422 | The document did not match its checksum — a structured refusal their worker can retry |

**200 for a submission we could not place is deliberate.** Google's trigger does not retry and
Google disables triggers that keep failing, so a non-200 costs an org their integration. Anything
unplaceable becomes a queue item in the org panel under **External forms**, never a refusal.

**The reference is the whole security model.** Long, random, expiring, single-use, scoped to one
run and one step — the standard a booking code and a magic link already meet. A matched live
reference resumes the run on the spot; an author who wants a human to read the answers first puts a
`verify` step after it. Unknown, spent, expired, another tenant's, or pointing at a case that has
already moved on are five different sentences and all of them are the unmatched queue.
