Skip to main content

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.

MethodPathPurposeAuthRate limit
GET/api/v1/webhooks/whatsappMeta verification handshake (hub.challenge)verify-token query paramnone
POST/api/v1/webhooks/whatsappMeta Cloud API inbound messageHMAC SHA256 (WHATSAPP_APP_SECRET)none (Meta-managed)
GET/api/v1/webhooks/instagramMeta verification handshakeverify-token query paramnone
POST/api/v1/webhooks/instagramMeta Graph IG DM inboundHMAC SHA256 (INSTAGRAM_APP_SECRET)none
GET/api/v1/webhooks/messengerMeta verification handshakeverify-token query paramnone
POST/api/v1/webhooks/messengerMeta Graph Messenger DM inboundHMAC SHA256 (MESSENGER_APP_SECRET)none
POST/api/v1/webhooks/daraja/stkDaraja STK push callback (M-Pesa)none (Daraja signs nothing — payload validation + correlation only)none
POST/api/v1/webhooks/pesapal/ipnPesaPal payment IPN (cards)HTTP basicnone

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-Key header validated against the ADMIN_API_KEY env 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 via get_current_tenant_from_jwt.
MethodPathPurposeAuth
POST/api/v1/admin/tenantsAtomic onboarding (registry + schema + Keycloak realm + admin user)X-Admin-API-Key
POST/api/v1/admin/tenants/{tenant_id}/suspendMove trial/active tenant to suspendedX-Admin-API-Key
POST/api/v1/admin/tenants/{tenant_id}/restoreMove suspended (or deleted within 7d) back to activeX-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/importUpload + extract draft (image / multi_image / pdf / text / csv)Keycloak JWT
POST/api/v1/admin/catalog/commitCommit reviewed draft to per-tenant DBKeycloak JWT
GET/api/v1/admin/catalog/importsList recent imports (rollback list)Keycloak JWT
GET/api/v1/admin/personalityRead tenant personality dialsKeycloak JWT
PATCH/api/v1/admin/personalityUpdate one or more dialsKeycloak JWT
POST/api/v1/admin/personality/resetReset dial(s) to vertical defaultKeycloak JWT
GET/api/v1/admin/profileTenant 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

PathPurposeAuth
/api/v1/admin/chat/wsAdmin 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.