Voice API (/api/v1/voice/*)¶
The Voice API is a versioned, token-authenticated surface designed for voice and home-automation integrations (Alexa skills, Home Assistant components, Google Assistant via HA, scripting hooks). It is intentionally separate from the internal session-cookie-authenticated routes the dashboard uses. The contract here is stable and external callers can rely on it.
Auth¶
Every Voice API endpoint requires Authorization: Bearer <token> where the token's scopes include either voice or *. Session cookies are rejected: this is intentionally a machine-to-machine surface, so a stolen browser session can't reach voice endpoints.
Tokens are issued via POST /api/auth/tokens (parent-only), SHA-256 hashed at rest, and never re-displayed after creation. The recommended scope for voice integrations is ['voice'] so a token leak cannot read or modify anything outside /api/v1/voice/*.
bash
curl -X POST http://localhost:3000/api/auth/tokens \
-H "Cookie: prism_session=<your-parent-session>" \
-H "Content-Type: application/json" \
-d '{ "name": "Alexa skill", "scopes": ["voice"] }'
The response includes token. Store it immediately, it is not retrievable later.
Known scopes¶
| Scope | Grants |
|---|---|
voice |
Read + write across /api/v1/voice/* only |
* |
Full account access (legacy default; avoid for new tokens) |
Response shape¶
Every endpoint returns:
json
{
"ok": true,
"spoken": "Today you have Soccer Practice at 4 PM.",
"data": { "...endpoint-specific..." }
}
ok: boolean.falsefor errors.spoken: natural-language string the caller speaks back to the user. Pre-formatted on the server so callers don't need templating.data: optional structured payload. Present on success; omitted on error.
Errors return the same shape with ok: false, an HTTP error status, and a user-friendly spoken apology (no stack traces or IDs).
Rate limiting¶
Per-token, 60 requests per 60 seconds. Returns 429 with a voice-shaped error body when exceeded.
Versioning¶
The path prefix /api/v1/ is the contract version. Breaking changes ship under /api/v2/ with /api/v1/ continuing to function. New non-breaking endpoints can be added to v1 freely.
Endpoints¶
GET /api/v1/voice/family¶
Lists family members, excluding guest accounts. Useful for syncing Alexa custom slot types (FAMILY_MEMBER).
Response data: { count, members: [{ id, name, role, color }] }
Spoken: "Your family has Alex, Jordan, Emma, and Sophie."
GET /api/v1/voice/calendar/today¶
Returns events whose startTime falls within today (server local time).
Response data:
json
{
"count": 1,
"events": [
{
"id": "uuid",
"title": "Soccer Practice",
"startTime": "2026-05-02T21:00:00.000Z",
"endTime": "2026-05-02T22:30:00.000Z",
"allDay": false,
"location": "Community Park"
}
]
}
Spoken examples:
"You have no events today.""Today you have Soccer Practice at 4 PM.""Today you have Standup at 9 AM and Lunch at 12:30 PM.""Today you have A at 8 AM, B at 10 AM, and C at 2 PM."(Oxford comma)
GET /api/v1/voice/calendar/upcoming?count=N¶
Returns the next N events ordered by start time (default 3, clamped to 1..10).
Response data: same shape as calendar/today.
Spoken: "Coming up: Soccer today at 4 PM, Dentist tomorrow on Sunday at 9 AM, and Movie on Tuesday at 6 PM."
GET /api/v1/voice/tasks/today¶
Incomplete tasks whose dueDate falls within today (server local time).
Response data: { count, tasks: [{ id, title, dueDate, priority, assignedTo }] }
Spoken: "You have 2 tasks today: Fix leaky faucet, and Practice piano."
POST /api/v1/voice/shopping/add¶
Body: { item: string, list?: string, quantity?: number, unit?: string }
Fuzzy-matches the list by name when list is given; otherwise adds to the first list (by sortOrder). Invalidates the shopping-lists cache on success.
Spoken: "Added milk to Grocery."
POST /api/v1/voice/chore/complete¶
Body: { chore: string, assignee?: string }
Fuzzy-matches the chore name (case-insensitive substring on title). See "Security model" below for the disambiguation rule and the no-approval guarantee.
Spoken (success): "Marked feed the dog complete."
Spoken (pending approval): "Marked feed the dog complete. A parent will need to approve in the app."
Spoken (ambiguous, ok:false): "Multiple chores match 'feed the dog'. Which family member: Emma, Sophie?". data.candidates lists each option; caller resends with assignee.
POST /api/v1/voice/message/post¶
Body: { message: string }
Author defaults to the first parent (by sortOrder). Voice has no way to verify which family member is speaking, so attributing posts to a designated parent keeps the audit trail honest. A future voiceUser setting could let households pick a different default.
Spoken: "Posted message: 'soccer practice moved to 4pm today.'"
GET /api/v1/voice/weather/today¶
Returns current conditions plus today's high/low. Reuses the active weather provider (WEATHER_PROVIDER=meteo|pirate|openweather) and the household location setting.
Response data: { location, currentTemp, feelsLike, description, condition, humidity, high, low, precipProbability }
Spoken example: "Chicago: currently 65 degrees. Partly cloudy. high 72, low 58."
GET /api/v1/voice/bus/status?student=Emma¶
Returns active bus routes for today with arrival predictions. Optional student filter narrows by studentName (case-insensitive substring). Today-active is determined by each route's activeDays array.
Response data: { count, routes: [{ id, label, studentName, direction, scheduledTime, prediction }] }
Spoken examples:
"No bus routes are scheduled today.""Emma AM: 5 minutes away.""Emma AM: arrived at the stop and Sophie PM: scheduled 15:25, no live data yet."
GET /api/v1/voice/birthdays/upcoming?days=N¶
Returns upcoming birthdays/anniversaries in the next N days (default 30, clamped 1..365). Compares on month/day so historic birth years are tolerated; reports the age the person is turning when the original year is available.
Response data: { count, birthdays: [{ id, name, eventType, nextOccurrence, turning }] }
Spoken examples:
"No upcoming birthdays.""Coming up: Emma's birthday on Saturday, turning 8."
GET /api/v1/voice/meals/today¶
Returns meals planned for today (breakfast, lunch, dinner, snack), ordered by meal type. Tolerates either Sunday- or Monday-start week conventions by scanning meal entries within ±7 days of today.
Response data: { count, meals: [{ id, name, mealType, mealTime }] }
Spoken examples:
"No meals are planned for today.""Today's plan is dinner: Tacos.""Today's meals: breakfast: Oatmeal, lunch: Salad, and dinner: Tacos."
GET /api/v1/voice/chores/today?assignee=Emma¶
Returns enabled chores due today or overdue. With assignee set, returns chores assigned to that family member (case-insensitive name match) plus chores with no assignee (anyone-can-do). Without assignee, returns all chores due today.
Response data: { count, chores: [{ id, title, assignedTo, nextDue, pointValue }], assigneeName }
Spoken examples:
"No chores are due today.""Emma has one chore today: Feed the dog.""You have 3 chores today: Take out trash, Vacuum, and Wipe counters."
GET /api/v1/voice/message/recent?count=N¶
Returns the most recent (non-expired) family messages, newest first. count defaults to 3, clamped to 1..10.
Response data: { count, messages: [{ id, message, authorName, createdAt }] }
Spoken examples:
"No recent family messages.""Latest message from Alex today: soccer at 4.""Recent messages: Alex today: soccer, Jordan yesterday: groceries done, and Emma on Friday: party RSVP."
Security model for write operations¶
Voice cannot escalate privileges. Specifically:
- Chore completions inherit the chore's
assignedToas the completer. Voice does not let one family member claim another's points. - Ambiguous chore names require disambiguation. If a fuzzy name match returns multiple chores assigned to different family members (e.g. both Emma and Sophie have "Feed the dog"), the endpoint returns
ok: falsewith aspokenprompt asking for the assignee ("Multiple chores match 'feed the dog'. Which family member?") anddata.candidates: [...]. The caller resends withassigneein the body. A single match completes immediately. - Chores with
requiresApproval: truecreate pending completions when completed via voice, just like the in-app flow. Thespokenresponse makes this explicit (e.g. "Marked feed the dog complete. A parent will need to approve in the app."). - Approval is in-app only, behind the Parent PIN. Voice has no way to approve a pending chore: there is no way to verify the speaker is a parent.
Roadmap¶
The next phase is the Alexa skill itself, then a HACS-published Home Assistant custom_component, tracked in #56. A later "device-control intents" phase will add a server-sent command bus so voice can drive the running dashboard UI (e.g. "pull up the lasagna recipe").