API surface
What this page is
A hand-curated reference of Ratiba's HTTP + WebSocket surface as of M10 close-out (2026-05-06). It covers the routes a pilot operator or integrator actually touches — webhooks from external providers, the admin REST surface backing the Next.js dashboard, and the two WebSocket endpoints (admin chat rail + customer web widget).
This is deliberately not auto-generated. The catalogue is small enough that a hand-written table communicates intent (auth model, rate-limit posture, purpose) more clearly than a 200-operation OpenAPI dump. Auto-generated OpenAPI reference is deferred to M13+, when the surface stabilises and the auto-gen step can drive the same tables as a build artifact.
For live exploration during development, the FastAPI app exposes
Swagger UI at http://localhost:8010/docs and ReDoc at
http://localhost:8010/redoc.
All paths are mounted under the /api/v1/ prefix per the M3 router
registration in app/main.py (create_app).
Webhooks (inbound from external providers)
These are unauthenticated public endpoints — the security boundary is either an HMAC signature header (Meta) or the absence of any signature combined with payload-shape validation + per-tenant idempotency keys (Daraja). PesaPal uses HTTP basic auth credentials configured per tenant.
| Method | Path | Purpose | Auth | Rate limit |
|---|---|---|---|---|
| GET | /api/v1/webhooks/whatsapp | Meta verification handshake (hub.challenge) | verify-token query param | none |
| POST | /api/v1/webhooks/whatsapp | Meta Cloud API inbound message | HMAC SHA256 (WHATSAPP_APP_SECRET) | none (Meta-managed) |
| GET | /api/v1/webhooks/instagram | Meta verification handshake | verify-token query param | none |
| POST | /api/v1/webhooks/instagram | Meta Graph IG DM inbound | HMAC SHA256 (INSTAGRAM_APP_SECRET) | none |
| GET | /api/v1/webhooks/messenger | Meta verification handshake | verify-token query param | none |
| POST | /api/v1/webhooks/messenger | Meta Graph Messenger DM inbound | HMAC SHA256 (MESSENGER_APP_SECRET) | none |
| POST | /api/v1/webhooks/daraja/stk | Daraja STK push callback (M-Pesa) | none (Daraja signs nothing — payload validation + correlation only) | none |
| POST | /api/v1/webhooks/pesapal/ipn | PesaPal payment IPN (cards) | HTTP basic | none |
The three Meta webhooks share a single project-level app-secret pattern
per ADR-0008 (WhatsApp) and ADR-0009 (Instagram + Messenger): one secret
per channel verifies signatures across all tenants, while per-tenant
credentials (<channel>_phone_number_id / <channel>_page_id +
<channel>_access_token) live on public.tenants.
Admin REST (authenticated)
The admin surface backs the Next.js dashboard and platform-admin tools. Authentication mode varies by router family:
- Platform admin (tenant lifecycle):
X-Admin-API-Keyheader validated against theADMIN_API_KEYenv var. Tenants themselves are not yet onboarded at this point, so a Keycloak session is not yet available. - Tenant admin (catalog, personality, chat): Keycloak-issued JWT
in the
Authorization: Bearer …header, decoded viaget_current_tenant_from_jwt.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /api/v1/admin/tenants | Atomic onboarding (registry + schema + Keycloak realm + admin user) | X-Admin-API-Key |
| POST | /api/v1/admin/tenants/{tenant_id}/suspend | Move trial/active tenant to suspended | X-Admin-API-Key |
| POST | /api/v1/admin/tenants/{tenant_id}/restore | Move suspended (or deleted within 7d) back to active | X-Admin-API-Key |
| DELETE | /api/v1/admin/tenants/{tenant_id} | Soft-delete (7-day cooling-off; worker drops schema/realm after) | X-Admin-API-Key |
| POST | /api/v1/admin/catalog/import | Upload + extract draft (image / multi_image / pdf / text / csv) | Keycloak JWT |
| POST | /api/v1/admin/catalog/commit | Commit reviewed draft to per-tenant DB | Keycloak JWT |
| GET | /api/v1/admin/catalog/imports | List recent imports (rollback list) | Keycloak JWT |
| GET | /api/v1/admin/personality | Read tenant personality dials | Keycloak JWT |
| PATCH | /api/v1/admin/personality | Update one or more dials | Keycloak JWT |
| POST | /api/v1/admin/personality/reset | Reset dial(s) to vertical default | Keycloak JWT |
| GET | /api/v1/admin/profile | Tenant display identity (business_name + assistant_name) — the dashboard chrome brands itself with the tenant's own name, not "Ratiba" | Keycloak JWT |
This table is a curated subset. Later milestones added more admin routers (
staff,commissions,onboarding,knowledge,appointments); the complete, always-current list is the auto-generated OpenAPI schema linked at the bottom of this page.
WebSocket
| Path | Purpose | Auth |
|---|---|---|
/api/v1/admin/chat/ws | Admin chat rail — handoff briefing cards + admin replies (M9 T1) | Keycloak JWT (cookie or Authorization header) |
/api/v1/channels/web/ws/{tenant_slug} | Web-widget customer realtime (M10 T5) | per-tenant signed session cookie (issued by GET /api/v1/channels/web/session/{tenant_slug}) |
The widget WS path is keyed by tenant slug (not tenant UUID) so the
embed snippet stays human-readable. The session-issue endpoint (GET /api/v1/channels/web/session/{tenant_slug}) returns a short-lived
signed cookie that the WS handshake verifies on connect.
The widget reply envelope carries two optional structured fields
alongside text: channel_switch (ADR-0009 steering directive) and
quick_replies (an array of {label, value} tappable chips for
staff / slot / yes-no choices — recognition over recall, so the
customer taps instead of typing an identifier; tapping sends value
exactly as a typed reply would).
Brief request/response shapes for the top-5 endpoints
These are illustrative (truncated to the load-bearing fields). For full field-level contracts, consult the Pydantic models in the linked source files.
POST /api/v1/webhooks/whatsapp
Inbound from Meta Cloud API. Request body shape (one entry, one message):
{
"object": "whatsapp_business_account",
"entry": [{
"id": "<WABA_ID>",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {"phone_number_id": "<PNI>"},
"contacts": [{"wa_id": "254712345678"}],
"messages": [{"from": "254712345678", "type": "text",
"text": {"body": "Hi, can I book?"}}]
},
"field": "messages"
}]
}]
}
Response: 200 OK with empty body (Meta retries on non-2xx). Signature
verified against X-Hub-Signature-256 header before any work runs.
POST /api/v1/admin/tenants
Request:
{
"name": "Acme Spa",
"slug": "acme-spa",
"vertical": "spa",
"default_locale": "en",
"admin": {"phone_e164": "+254712345678", "email": "owner@acme.example"}
}
Response (201 Created):
{
"id": "01HV...ULID",
"slug": "acme-spa",
"vertical": "spa",
"status": "trial",
"default_locale": "en",
"created_at": "2026-05-06T10:00:00Z",
"admins": []
}
POST /api/v1/admin/catalog/import
Multipart form (not JSON):
modality=image # image | multi_image | pdf | text | csv
vertical=spa
files=@menu.jpg # required for image/multi_image/pdf
text=... # required for text/csv
Response (200 OK, draft envelope):
{
"import_id": "01HV...UUID",
"modality": "image",
"vertical": "spa",
"rows": [{"name": "Swedish massage", "duration_min": 60,
"price_kes": 3500, "confidence": 0.91}],
"relations": [{"service_a_name": "Pedicure",
"service_b_name": "Manicure",
"relation_type": "frequently_combined",
"confidence": 0.78}],
"confidence_summary": {"high": 12, "medium": 3, "low": 0}
}
POST /api/v1/admin/catalog/commit
Request:
{
"import_id": "01HV...UUID",
"admin_overrides": {
"<name_key_uuid5>": {"price_kes": 4000, "duration_min": 75}
}
}
Response (200 OK):
{
"committed_at": "2026-05-06T10:05:00Z",
"services_inserted": 12,
"services_updated": 3,
"relations_inserted": 7
}
Idempotent re-calls return 404 Not Found ("draft already committed
or expired") — the draft is evicted on first successful commit.
PATCH /api/v1/admin/personality
Request (partial — any subset of dials):
{
"warmth": 0.7,
"formality": 0.4,
"verbosity": "concise"
}
Response (200 OK, full personality echoed back):
{
"tenant_id": "01HV...ULID",
"warmth": 0.7,
"formality": 0.4,
"verbosity": "concise",
"humor": 0.2,
"updated_at": "2026-05-06T10:10:00Z"
}
Auto-generated OpenAPI
Deferred to M13+ as part of the docs-IA polish pass. Until then,
the FastAPI app's built-in OpenAPI schema is exposed at
http://localhost:8010/openapi.json (machine-readable) and rendered
live as Swagger UI at http://localhost:8010/docs for hands-on
exploration in development. The hand-curated table on this page is
the human-facing reference; the OpenAPI JSON is the
machine-facing one.