Skip to content

api/v1 Endpoint Reference

/api/v1 is yaya-engine's service-to-service surface for the sibling Rada services — alert-manager, the Python composer (rada-message-system), the dispatcher, and rada-weather's ETL. Every route in this namespace sits behind auth.admin_token, the same shared bearer-token middleware the WhatsApp batch endpoints use, and most blocks additionally rate-limit at throttle:120,1 (120 requests/minute per IP) [@api-routes] [@docs-orchestration]. The surface exists to replace direct Supabase RPC and Edge Function calls those services used to make; each block below was added to retire one such call as part of the Rada v2 cutover — see Run the cross-repo harness for how these endpoints get exercised against a live yaya-engine deployment and Route surfaces for how /api/v1 sits alongside the rest of the app's routing.

Auth and error shape

AdminTokenMiddleware compares the request's bearer token against config('auth.admin_token') with hash_equals; a missing or wrong token returns 401 {"message": "Invalid admin token."} on every /api/v1 block [@docs-orchestration]. A throttled burst returns 429 {"message": "Too Many Attempts."} on the blocks that carry throttle:120,1 — every block added after the original orchestration block does; the original scheduled-alerts/composed-messages/dispatch-batches block does not carry an explicit throttle beyond the shared admin-token gate [@api-routes] [@docs-users-templates]. Validation failures return Laravel's standard 422 { message, errors: { field: [...] } } shape throughout.

Endpoint map

Method Path Contract doc Traces to
GET /api/v1/scheduled-alerts [@docs-orchestration] ye#314, epic ye#309
POST /api/v1/scheduled-alerts [@docs-orchestration] ye#314
GET /api/v1/scheduled-alerts/{id} [@docs-orchestration] ye#376 (composer read path was 405)
PATCH /api/v1/scheduled-alerts/{id} [@docs-orchestration] ye#314
DELETE /api/v1/scheduled-alerts/{id} [@docs-orchestration] ye#314 (soft delete, is_deleted=true)
PATCH /api/v1/scheduled-alerts/{id}/status [@docs-orchestration] ye#314
POST /api/v1/composed-messages/bulk [@docs-orchestration] ye#314
POST /api/v1/composed-messages/claim [@docs-orchestration] ye#314
POST /api/v1/dispatch-batches [@docs-orchestration] ye#314
POST /api/v1/dispatch-batches/requeue-abandoned [@docs-orchestration] ye#314
POST /api/v1/dispatch-batches/{batch_id}/finalize [@docs-orchestration] ye#314
GET /api/v1/templates [@docs-users-templates] ye#341, epic ye#309
POST /api/v1/users/by-town-ids [@docs-users-templates] ye#341
GET /api/v1/users/with-forecasts [@docs-users-templates] ye#341
GET /api/v1/users/count-by-state [@docs-users-templates] am#68 (P1a.4)
GET /api/v1/users/nearest-town [@docs-users-templates] am#68
GET /api/v1/users [@docs-users-templates] am#68
POST /api/v1/seasons [@docs-events] am#17, Rada v2
GET /api/v1/seasons [@docs-events] am#17
POST /api/v1/events [@docs-events] am#17
GET /api/v1/events [@docs-events] am#17
GET /api/v1/events/active [@docs-events] am#17
PATCH /api/v1/events/{id} [@docs-events] am#17
GET /api/v1/seasons-with-events-and-alerts [@docs-events] am#53 (Review Tab 1/2)
GET /api/v1/forecast-config [@docs-forecast-config] am#48 (Forecast Automation 5/7)
PUT /api/v1/forecast-config [@docs-forecast-config] am#48
GET /api/v1/forecast-config/audit [@docs-forecast-config] am#48
GET /api/v1/dispatch-mode [@docs-dispatch-mode] am#49
PUT /api/v1/dispatch-mode [@docs-dispatch-mode] am#49
GET /api/v1/geography/towns [@docs-geography] ye#353 (P2.2.c, read)
GET /api/v1/geography/states [@docs-geography] ye#353
PATCH /api/v1/geography/towns/{id}/thresholds [@docs-geography] ye#354 (P2.2.d, write)
POST /api/v1/geography/towns/bulk-upsert [@docs-geography] ye#354
POST /api/v1/geography/towns/bulk-update-thresholds [@docs-geography] ye#354
POST /api/v1/town-forecasts/bulk-insert [@docs-town-forecasts] rada-weather#31 (Forecast Automation 7/7)
GET /api/v1/town-forecasts/latest [@docs-town-forecasts] rada-weather#31
GET /api/v1/town-forecasts/model-runs [@docs-town-forecasts] am#50 hover card
POST /api/v1/town-observations/bulk-insert none in docs/api/v1/ am#54, Rada v2
GET /api/v1/forecast-accuracy none in docs/api/v1/ am#54 (Review Mode accuracy)

routes/api.php's comments point town-observations and forecast-accuracy at docs/api/v1/town-observations.md and docs/api/v1/forecast-accuracy.md respectively, but neither file exists in docs/api/v1/ — the contract for those two routes currently lives only in the route file's inline comments and the controllers themselves [@api-routes] [@docs-api-v1-dir].

Scheduled alerts, composed messages, dispatch batches

This is the original orchestration block (P1b.2, ye#314), replacing the RPCs and Edge actions alert-manager, the composer, and the dispatcher used to call directly against Supabase: get_scheduled_alerts_by_country, create_scheduled_alert, update_alert_status, insert_composed_messages, claim_composed_messages, log_dispatch_batch_start, and the finalize_batch_* family [@docs-orchestration]. POST /composed-messages/bulk accepts up to 25,000 rows per call, chunks inserts internally at 1,000, and dedupes on (alert_id, user_phone) — this is the endpoint rada:loadtest:composer-bulk-insert exercises to verify the composer's 21k-rows-per-30-minute SLA [@docs-orchestration]. POST /composed-messages/claim mirrors claim_composed_messages exactly, including its partition-by-(alert_id, meta_template_id, yaya-user-presence) locking strategy verified concurrency-safe by tests/Feature/Api/V1/ComposedMessageClaimAtomicityTest.php [@docs-orchestration].

Users and templates

Added in P1a.2.1 (ye#341) to retire direct Supabase reads from the composer, alert-manager, and BroadcastService. country on these endpoints accepts either the full English name (Kenya) or ISO-2 (KE) and normalizes internally, unlike the geography block below which is ISO-2-only [@docs-users-templates]. GET /users/with-forecasts is a temporary cross-DB read — it still proxies Supabase's get_users_for_weather_alert RPC until the weather tables migrate under P2.4, and surfaces an upstream failure as 502 rather than 422 [@docs-users-templates]. GET /templates only implements the meta provider; sms/telegram/push return 501 because no current consumer calls them through this endpoint yet [@docs-users-templates].

Seasons, events, and geography

The Season → Event → Alerts hierarchy (am#17) is scoped to a country, not an FSP, because a weather event affects the same geography regardless of which FSP's users live there; multiple concurrent active events per country are allowed by design (am#73) [@docs-events]. Country handling on this block is strict on writes, lenient on reads: an unknown country on POST /seasons or /events returns 422, but the same unknown country on a list endpoint just matches zero rows and returns 200 with meta.count: 0 [@docs-events]. Alert-to-event auto-assignment runs only at POST /scheduled-alerts creation time and never re-runs on a later PATCH, so an alert that started standalone stays standalone unless an operator sets event_id explicitly [@docs-events].

The geography block (ye#353 read / ye#354 write) moves towns_24h_rain_thresholds and states_24h_rain_thresholds out of Supabase. Unlike the seasons/events/users blocks, geography accepts only ISO-2 country codes, not full English names [@docs-geography]. PATCH /geography/towns/{id}/thresholds distinguishes an omitted field (leave untouched) from an explicit null (clear the column), and enforces yellow < orange < red ordering only on the newer 48hr/72hr threshold sets, not on the original 24hr set, because production already has rows with a null threshold in the 24hr set that the alert engine treats as advisory [@docs-geography].

Forecast config and dispatch mode

Both blocks (am#48, am#49) back per-country configuration toggles in alert-manager's UI and are storage/API only — the menu and toggle themselves are alert-manager-side work. Both default the country's setting when nothing has been saved (weight_mode: auto for forecast config, mode: manual for dispatch mode, the safe human-in-the-loop baseline) and both write an append-only audit row on every successful PUT, even a no-op save [@docs-forecast-config] [@docs-dispatch-mode]. Dispatch mode reads the acting user from an X-Atram-Acting-User header rather than the request body, and tolerates the header's absence by recording a null actor, since the endpoint authenticates service-to-service with no logged-in user [@docs-dispatch-mode].

Town forecasts, observations, and forecast accuracy

town_forecasts is rada-weather#31's unified, append-only forecast store: every ETL run inserts a fresh row per (town_id, model, forecast_date) rather than updating in place, so GET /town-forecasts/latest resolves the current reading as the highest id per group, while GET /town-forecasts/model-runs returns the full run history for the am#50 hover card [@docs-town-forecasts]. The companion rada:prune-forecast-store artisan command (scheduled daily at 01:00) deletes superseded rows older than a 90-day default window while always keeping the latest row per group [@docs-town-forecasts]. POST /town-observations/bulk-insert and GET /forecast-accuracy follow the same append-only, ISO-2-country convention as the rest of /api/v1, per the route file's inline comments, but — as noted above — their full field-level contract isn't yet written up under docs/api/v1/ the way the other blocks are [@api-routes].