Feature: Today Timeline API
Version: 1.2.0 Last Reviewed: 2026-03-07 Status: Approved
User Story
The iOS Today tab needs an API endpoint that returns all events for a given day in chronological order for multi-caregiver coordination.
MVP Scope
- Single endpoint returns all events (feedings, diapers, sleep, notes) for a date
- Events sorted by timestamp descending (newest first)
- Each event includes type, key fields, and ID for editing
- Day boundaries use the authenticated user's
day_start_timeandtimezone
NOT in MVP
- Pagination (a single day rarely exceeds 50 events for a newborn)
- Cursor-based navigation between days
- Real-time updates (polling only)
- Event grouping (morning/afternoon/evening/night sections)
API Contract
Get Timeline
GET /api/v1/children/:childId/timeline?date=2026-02-10
GET /api/v1/children/:childId/timeline?since=2026-02-10T00:00:00.000Z&until=2026-02-12T00:00:00.000Z
GET /api/v1/children/:childId/timeline?since=2026-02-10T00:00:00.000Z&type=feeding
GET /api/v1/children/:childId/timeline (defaults to today)
Authorization: Bearer <token>
Query Parameters:
- date: OPTIONAL. YYYY-MM-DD format. Interpreted in user's timezone using day_start_time boundary.
Defaults to today (in user's timezone) when neither date nor since/until provided.
- since: OPTIONAL. ISO 8601 datetime. Filter events at or after this timestamp (UTC).
When provided, date param is ignored for boundary calculation.
- until: OPTIONAL. ISO 8601 datetime. Filter events strictly before this timestamp (UTC).
When provided, date param is ignored for boundary calculation.
Must be after since when both are provided.
- type: OPTIONAL. One of: feeding, diaper, sleep, note. Filter to a single event type.
Response 200:
{
"events": [
{
"id": "uuid",
"event_type": "feeding",
"timestamp": "2026-02-10T14:30:00.000Z",
"data": {
"type": "breast",
"started_side": "left",
"both_sides": false,
"duration_minutes": 15,
"amount_ml": null
}
},
{
"id": "uuid",
"event_type": "diaper",
"timestamp": "2026-02-10T13:00:00.000Z",
"data": {
"type": "poop",
"ec_attempt": false,
"ec_success": false
}
},
{
"id": "uuid",
"event_type": "sleep",
"timestamp": "2026-02-10T08:00:00.000Z",
"data": {
"start_time": "2026-02-10T08:00:00.000Z",
"end_time": "2026-02-10T10:15:00.000Z",
"duration_minutes": 135
}
},
{
"id": "uuid",
"event_type": "note",
"timestamp": "2026-02-10T07:00:00.000Z",
"data": {
"category": "medication",
"text": "Vitamin D drops"
}
},
{
"id": "uuid",
"event_type": "sleep",
"timestamp": "2026-02-10T06:00:00.000Z",
"data": {
"start_time": "2026-02-10T06:00:00.000Z",
"end_time": null,
"duration_minutes": null
}
}
],
"count": 5
}
Response 200 (no events):
{
"events": [],
"count": 0
}
Error Responses
Standard error format. Error codes: 400 VALIDATION_ERROR, 401 UNAUTHORIZED, 404 NOT_FOUND (also used for unauthorized access to prevent information disclosure)
Business Rules
dateparameter is optional. Defaults to today in the user's timezone when neitherdatenorsince/untilis provided.since/untiloverride date-based boundaries. When either is present, the day boundary logic (timezone, day_start_time) is bypassed entirely and timestamps are treated as UTC.sinceis inclusive (>=),untilis exclusive (<). Consistent with thesincefilter convention used elsewhere in the API.sincemust be beforeuntilwhen both are provided. Returns 400 VALIDATION_ERROR otherwise.typefilters to a single event type. Can be combined withdate,since, oruntil.- Day boundaries use
User.day_start_timeandUser.timezone(date mode only): If user's day_start_time is 07:00 and timezone is America/New_York, then "2026-02-10" means 2026-02-10T07:00 ET to 2026-02-11T06:59:59 ET. - All event types merged: Feedings, diapers, sleep, and notes combined into one list.
- Sorted by timestamp descending (newest first).
- Sleep events use start_time as their timestamp for sorting purposes.
- Active sleep sessions included: Sleep with end_time=null appears in timeline with null duration.
- Sleep spanning day boundary: A sleep session appears on the day that contains its start_time (consistent with dashboard totals).
- Each event includes ID: So iOS can construct edit/delete URLs for the appropriate resource.
- No pagination in MVP: Returns all events for the queried range.
- Child access verified via ChildAccess table.
- Read-only endpoint: No audit logging.
Acceptance Criteria
- Single GET returns all event types for a given date
- No params defaults to today's events (200, not 400)
-
sinceparam filters events at or after that timestamp -
untilparam filters events strictly before that timestamp -
since/untilbypass day-boundary logic (raw UTC comparison) -
since>=untilreturns 400 VALIDATION_ERROR - Invalid
since/untilformat returns 400 VALIDATION_ERROR -
typeparam filters to a single event type (feeding, diaper, sleep, note) - Invalid
typevalue returns 400 VALIDATION_ERROR -
typecan be combined withdate,since, oruntil - Events are sorted by timestamp descending (newest first)
- Each event includes id, event_type, timestamp, and type-specific data
- Feeding data includes started_side and both_sides (not old side field)
- Day boundaries respect user's day_start_time and timezone (date mode)
- Active sleep sessions appear with null end_time/duration
- Empty result returns {events: [], count: 0} (not 404)
- Response includes count field
- Invalid date format returns 400
- Child access is verified (404 for unauthorized, same as child not found, prevents information disclosure)
- Response time <200ms for typical day (~30 events)
Test Cases
Happy Path
- Mixed events: Day with feedings, diapers, sleep, notes → all returned, correctly sorted
- Single type only: Day with only feedings → returns feedings only, count matches
- No events: Valid date with no events → 200, {events: [], count: 0}
- Future date: Tomorrow's date → 200, empty events array
Sorting
- Correct descending order: Events at 14:00, 13:00, 12:00 → returned in that order
- Same timestamp: Two events at exact same time → both returned (stable sort)
- Sleep uses start_time: Sleep 08:00-10:00 sorted by 08:00, not 10:00
Day Boundaries (day_start_time)
- Before boundary: day_start_time=07:00 ET. Event at 06:59 ET on Feb 10 → NOT in Feb 10's timeline (it's in Feb 9's)
- At boundary: Event at 07:00 ET on Feb 10 → IS in Feb 10's timeline
- End of day: Event at 06:59 ET on Feb 11 → IS in Feb 10's timeline
- Timezone conversion: User in America/New_York, events stored UTC → boundaries correct
- Custom day_start_time: User sets day_start_time=06:00 → boundary shifts accordingly
Sleep-Specific
- Active sleep in timeline: Active sleep (no end_time) appears with null duration
- Overnight sleep: Start 22:00 Feb 9, end 06:00 Feb 10 → appears on Feb 9's timeline (start_time date)
Event Data Fields
- Feeding breast: event_type=feeding, data has type, started_side, both_sides, duration_minutes, amount_ml
- Feeding bottle: event_type=feeding, data has type=bottle, started_side=null, both_sides=false, amount_ml=120
- Diaper: event_type=diaper, data has type (pee/poop/both), ec_attempt, ec_success
- Sleep: event_type=sleep, data has start_time, end_time, duration_minutes
- Note: event_type=note, data has category, text
Validation
- Missing date: GET without date param → 400
- Invalid date format: "02-10-2026" → 400
- Invalid date: "2026-13-40" → 400
Access Control
- No access: User without ChildAccess → 404 (prevents information disclosure)
- Shared access: Caregiver with access → 200, events from all caregivers
- Child not found: → 404
Data Integrity
- Events from multiple caregivers: Both owner and caregiver logged events → all appear
- Deleted events don't appear: Delete a feeding, refresh timeline → gone
- Count matches array length: count field equals events array length
Boundaries
- No pagination (returns all events for the queried range)
- No cursor-based day navigation (client sends specific date or since/until range)
- Sleep spanning the day boundary appears only on start_time's day (date mode)
- Performance target: <200ms for typical day
- No audit logging (read-only endpoint)