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].