Skip to main content

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_time and timezone

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

  1. date parameter is optional. Defaults to today in the user's timezone when neither date nor since/until is provided.
  2. since/until override date-based boundaries. When either is present, the day boundary logic (timezone, day_start_time) is bypassed entirely and timestamps are treated as UTC.
  3. since is inclusive (>=), until is exclusive (<). Consistent with the since filter convention used elsewhere in the API.
  4. since must be before until when both are provided. Returns 400 VALIDATION_ERROR otherwise.
  5. type filters to a single event type. Can be combined with date, since, or until.
  6. Day boundaries use User.day_start_time and User.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.
  7. All event types merged: Feedings, diapers, sleep, and notes combined into one list.
  8. Sorted by timestamp descending (newest first).
  9. Sleep events use start_time as their timestamp for sorting purposes.
  10. Active sleep sessions included: Sleep with end_time=null appears in timeline with null duration.
  11. Sleep spanning day boundary: A sleep session appears on the day that contains its start_time (consistent with dashboard totals).
  12. Each event includes ID: So iOS can construct edit/delete URLs for the appropriate resource.
  13. No pagination in MVP: Returns all events for the queried range.
  14. Child access verified via ChildAccess table.
  15. 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)
  • since param filters events at or after that timestamp
  • until param filters events strictly before that timestamp
  • since/until bypass day-boundary logic (raw UTC comparison)
  • since >= until returns 400 VALIDATION_ERROR
  • Invalid since/until format returns 400 VALIDATION_ERROR
  • type param filters to a single event type (feeding, diaper, sleep, note)
  • Invalid type value returns 400 VALIDATION_ERROR
  • type can be combined with date, since, or until
  • 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

  1. Mixed events: Day with feedings, diapers, sleep, notes → all returned, correctly sorted
  2. Single type only: Day with only feedings → returns feedings only, count matches
  3. No events: Valid date with no events → 200, {events: [], count: 0}
  4. Future date: Tomorrow's date → 200, empty events array

Sorting

  1. Correct descending order: Events at 14:00, 13:00, 12:00 → returned in that order
  2. Same timestamp: Two events at exact same time → both returned (stable sort)
  3. Sleep uses start_time: Sleep 08:00-10:00 sorted by 08:00, not 10:00

Day Boundaries (day_start_time)

  1. 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)
  2. At boundary: Event at 07:00 ET on Feb 10 → IS in Feb 10's timeline
  3. End of day: Event at 06:59 ET on Feb 11 → IS in Feb 10's timeline
  4. Timezone conversion: User in America/New_York, events stored UTC → boundaries correct
  5. Custom day_start_time: User sets day_start_time=06:00 → boundary shifts accordingly

Sleep-Specific

  1. Active sleep in timeline: Active sleep (no end_time) appears with null duration
  2. Overnight sleep: Start 22:00 Feb 9, end 06:00 Feb 10 → appears on Feb 9's timeline (start_time date)

Event Data Fields

  1. Feeding breast: event_type=feeding, data has type, started_side, both_sides, duration_minutes, amount_ml
  2. Feeding bottle: event_type=feeding, data has type=bottle, started_side=null, both_sides=false, amount_ml=120
  3. Diaper: event_type=diaper, data has type (pee/poop/both), ec_attempt, ec_success
  4. Sleep: event_type=sleep, data has start_time, end_time, duration_minutes
  5. Note: event_type=note, data has category, text

Validation

  1. Missing date: GET without date param → 400
  2. Invalid date format: "02-10-2026" → 400
  3. Invalid date: "2026-13-40" → 400

Access Control

  1. No access: User without ChildAccess → 404 (prevents information disclosure)
  2. Shared access: Caregiver with access → 200, events from all caregivers
  3. Child not found: → 404

Data Integrity

  1. Events from multiple caregivers: Both owner and caregiver logged events → all appear
  2. Deleted events don't appear: Delete a feeding, refresh timeline → gone
  3. 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)