Document 04 · Client-used interfaces
API reference
REST calls made by reachable app code, including request fields, expected status, and the response shape the client consumes.
Scope: call sites in the running client. The configured but unused /v1/resources, /v1/trait-definitions, and /v1/auth/mfa/verify routes are not listed.
1. Conventions
| Item | Client behavior |
| Base URL | EXPO_PUBLIC_API_BASE_URL, trailing slash removed. Default paths come from src/config/apiEndpoints.ts; most may be replaced by an endpoint-specific environment URL. |
| Authentication | Authorization: Bearer <access_token> and, when available, X-Account-Id: <account.id>. |
| Format | Accept: application/json; JSON writes add Content-Type: application/json. Empty DELETE/POST requests deliberately omit content type. |
| Success envelope | Usually { "data": ... }. Lists may also include page or meta. |
| Errors | Problem body fields consumed: code, detail or title, optional entitlement. Retry-After becomes retryAfterSeconds. |
| Session expiry | HTTP 401 or codes invalid_token, missing_bearer_token, token_expired, user_not_found clear the local session. |
| Idempotency | Child creation, onboarding, scan creation, play-session creation, milestone writes, saved items, article reads and consent writes use a generated idempotency key where implemented. |
| Cache | Authenticated repository calls use cache: no-store. |
2. Authentication and account
| Method and path | Auth | Request | Expected / consumed response |
POST /v1/auth/register | No | {email, password, given_name?, family_name?, mfa_code?, recaptcha_token?, onboarding?} | 201 · {data: AuthSession} |
POST /v1/auth/login | No | Same email-auth body | 200 · {data: AuthSession} |
POST /v1/auth/google | No | {id_token} | 200 existing / 201 created · session envelope |
POST /v1/auth/apple | No | {identity_token, authorization_code?, given_name?, family_name?} | 200 existing / 201 created · session envelope |
GET /v1/me | Yes | — | 200 · {data: {user, accounts[]}} |
DELETE /v1/me | Yes | — | 200 or 204. A 404 is treated as already deleted. |
POST [configured logout URL] | Yes | No body | Best-effort; response is ignored. The call is skipped unless EXPO_PUBLIC_AUTH_LOGOUT_URL is explicitly set. |
AuthSession {
access_token: string
user: User
account?: Account
// optional token/session metadata returned by server
}
3. Children and onboarding
| Method and path | Request | Expected / consumed response |
GET /v1/children | — | 200 · {data: ChildRecord[]} |
POST /v1/children | ChildCreateInput + Idempotency-Key | 201 · child envelope. 402 entitlement is surfaced to the paywall. |
PATCH /v1/children/{childId} | ChildUpdateInput; no If-Match | 200 · child envelope |
DELETE /v1/children/{childId} | — | 202 · {data: JobRecord}. Child is removed from UI immediately. |
GET /v1/jobs/{jobId} | — | 200 · job envelope; client follows poll_after_ms for up to 60 seconds. |
POST /v1/onboarding | RegisterOnboardingPayload + idempotency key | 200 or 201 · {data: OnboardingResult} |
POST /v1/onboarding (anonymous) | Legacy onboarding snapshot, no auth | Any 2xx is accepted; used by “Continue for now.” |
ChildCreateInput {
given_name: string
preferred_name?: string
birthdate: string // YYYY-MM-DD
sex?: string | null
locale?: string
timezone?: string
}
RegisterOnboardingPayload {
child: ChildCreateInput
assessment: four-area milestone answers
interests: InterestWrite[]
traits?: TraitWrite[]
// onboarding metadata carried by the typed request
}
4. Child interests and traits
| Method and path | Request | Expected response |
GET /v1/interest-tags?age_months=N&curated=false | Optional auth; curated=false only when requested | Any OK · {data: InterestTag[]}; failure returns empty list. |
PUT /v1/children/{childId}/interests | {interests: InterestWrite[]} | 200 · interest records |
POST /v1/children/{childId}/interests | InterestWrite | 201 · one interest record |
PATCH /v1/children/{childId}/interests/{id} | InterestUpdate | 200 · one interest record |
DELETE /v1/children/{childId}/interests/{id} | — | 204 |
PUT /v1/children/{childId}/traits | {traits: TraitWrite[]} | 200 · trait records |
POST /v1/children/{childId}/traits | TraitWrite | 201 · one trait record |
PATCH /v1/children/{childId}/traits/{id} | TraitUpdate | 200 · one trait record |
DELETE /v1/children/{childId}/traits/{id} | — | 204 |
5. App read models and care queue
| Method and path | Query/body | Expected / consumed response |
GET /v1/children/{childId}/home | — | 200 · {data: HomeScreenData} |
GET /v1/children/{childId}/portrait | — | 200 · {data: PortraitScreenData}. No include=report is sent. |
GET /v1/children/{childId}/sessions | limit, optional completed, since, cursor | 200 · {data: PlaySessionRecord[], page?: {has_more,next_cursor}}. Full history loops at 100/page, max 20 pages. |
GET /v1/children/{childId}/care-queue | — | 200 · {data: CareQueueItem[], meta} |
POST /v1/children/{childId}/care-queue/{itemId}/actions | {action, resolution?, snooze_until?} | 200 · updated care queue item |
6. Activities, scans, and play sessions
| Method and path | Request | Expected / consumed response |
GET /v1/activities | age_months, limit=50, optional setting; optional auth; locale headers/query | OK · activity list/read model. Signed-in callers require remote results; signed-out paths may use bundled content. |
POST /v1/children/{childId}/scans | See SceneScanInput below; idempotency header and body key | 201 · {data:{scan: ScanRecord}}. 402 is allowed to reach paywall handling. |
GET /v1/scans/{scanId} | — | 200 · scan envelope; polled until completed/failed/expired/rejected, max 90 seconds. |
GET /v1/scans/{scanId}/detections | — | 200 · detection array |
GET /v1/scans/{scanId}/suggestions | — | 200 · activity suggestions |
POST /v1/children/{childId}/sessions | {activity_id, client_event_id, context, suggestion_id?} + idempotency key | 200 or 201 · play session |
PATCH /v1/sessions/{sessionId} | {completed:true, ended_at} | 200 · completed play session |
PUT /v1/sessions/{sessionId}/feedback | {rating?, difficulty_feedback?, enjoyment?, child_engaged?, comment?} | 200 · data must be present; concrete payload is not otherwise consumed. |
SceneScanInput {
content_type: "image/jpeg" | "image/png"
bytes: number
image_base64: string
idempotency_key: string
room_hint?: string
user_prompt?: string[]
user_tags?: string[]
regenerate_of_suggestion_id?: string
regenerate_mode?: "similar_activity"
preview_mock?: true // localhost preview only
}
Play session context is from_scan | from_library | from_milestone | from_search | from_class | ad_hoc. Feedback maps caregiver chips to too_easy|just_right|too_hard and loved|liked|disliked.
7. Milestones
| Method and path | Request | Expected response |
GET /v1/children/{childId}/milestones | limit, optional cursor | 200 · paged milestone observations; repository follows pages. |
PUT /v1/children/{childId}/milestones/{milestoneId} | Milestone observation write + idempotency key | 200 · observation |
PUT /v1/children/{childId}/milestones/by-code/{code} | Milestone observation write + idempotency key | 200 · observation |
DELETE /v1/children/{childId}/milestones/by-code/{code} | — | 204 |
8. Saved items and article reads
| Method and path | Request | Expected / consumed response |
GET /v1/children/{childId}/saved-items | object_type, limit | 200 · saved item list |
POST /v1/children/{childId}/saved-items | {object_type, object_ref, note?} + idempotency key | 200 or 201 · saved item |
DELETE /v1/saved-items/{savedItemId} | — | 204 |
GET /v1/children/{childId}/article-reads | limit, optional cursor | 200 · article read list/page |
PUT /v1/children/{childId}/article-reads/{articleId} | Read/completion state + idempotency key | 200 · article read |
The Resources catalog itself comes from the home.resources read model for signed-in users; the configured standalone resources route has no reachable caller.
9. Tracker
| Method and path | Request | Expected / consumed response |
GET /v1/children/{childId}/tracker/entries?limit=100 | — | 200 · {data: ApiTrackerEntry[]}; merged into the local store. |
POST /v1/children/{childId}/tracker/sync | {entries: ApiTrackerEntryInput[]}, maximum 200 per request | 200 · result rows with client_event_id, outcome: accepted|duplicate|rejected, and optional server entry. |
PATCH /v1/tracker/entries/{entryId} | One ApiTrackerEntryInput | 200 · updated entry |
DELETE /v1/tracker/entries/{entryId} | No body/content type | 204 or 404; both clear the local tombstone. |
ApiTrackerEntryInput {
kind: "feeding" | "sleep" | "diaper" | "measurement" | "health" | "pump"
occurred_at: ISODateTime
ended_at?: ISODateTime
duration_seconds?: number
note?: string
client_event_id?: string
feeding?: {method?, amount_ml?, food_items?, meal_type?, reacted_to?, mood_after?}
sleep?: {kind?, quality?, location?, wake_count?, mood_before?, mood_after?, slept_through?}
diaper?: {kind?, consistency?, color?, has_rash?, in_potty?}
measurement?: {kind?, value?, unit?: "kg"|"cm", source?: "parent"}
health?: {kind?, description?, temperature_c?}
pump?: {left_ml?, right_ml?, left_seconds?, right_seconds?, total_ml?}
}
Optional input values are omitted rather than sent as null. Response rows add id, child_id, created_at, and may return nullable optional values.
10. Notifications and devices
| Method and path | Request | Expected response |
GET /v1/notifications | cursor?, limit, unread_only? | 200 · {data: AppNotification[], meta/page} |
POST /v1/notifications/mark-viewed | Exactly one of {ids: string[]} or {before: ISODateTime} | 200 |
PATCH /v1/notifications/{id} | {viewed:true} or {dismissed:true} | 200 · updated notification |
GET /v1/notifications/preferences | — | 200 · notification preferences |
PUT /v1/notifications/preferences | Complete preference update used by the screen | 200 · updated preferences |
POST /v1/devices | Device upsert with platform, push token and client metadata | 200 · {data: Device}; id stored locally. |
DELETE /v1/devices/{deviceId} | — | 204 or 404 |
11. Entitlements, config, consent, and support
| Method and path | Request | Expected / consumed response |
GET /v1/account/entitlements | — | 200 · {data: Entitlements}. Failure retains free fallback. |
GET /v1/config | Optional bearer | OK · {data:{flags, limits}}; used for payment switch and protocol limits. |
POST /v1/consents | {consent_type, granted, method:"in_app", policy_version} + idempotency key | 200 or 201. Photo consent writes three consent types in parallel, best-effort. |
POST /v1/support/requests | {type, message, child_id?, current_url?, client_context?} | 202; response body is not consumed. |
Entitlements {
plan: string
tier?: {code, label, rank}
is_active: boolean
source: string
expires_at?: ISODateTime | null
in_trial?: boolean
trial_ends_at?: ISODateTime | null
features?: Record<string, boolean>
limits?: Record<string, number> // -1 means unlimited
active_promotions?: unknown[]
payments_enabled?: boolean
}
12. Non-REST interfaces
| Interface | Current use | Current constraint |
RevenueCat react-native-purchases | Configure by account id, read offerings, purchase one of three product ids, restore purchases, log out SDK. | The environment reader omits both RevenueCat public key names, so configuration receives no key and this service returns unavailable in the current snapshot. |
| Expo notifications | Request permission, obtain push token, register/unregister device. | Unavailable paths fail without crashing. |
| Apple / Google auth SDKs | Obtain identity token, then exchange through the REST auth endpoints. | Platform/client-id configuration controls availability. |
| Camera/image APIs | Capture/select, resize and encode room photos; select/crop avatar photos. | Scan requires explicit consent; avatar remains local. |
13. Typical authenticated call
sequenceDiagram
participant UI as Screen
participant R as Repository
participant A as authorizedFetch
participant API as API
UI->>R: typed input
R->>A: URL, method, expected status
A->>API: Bearer + X-Account-Id + JSON
API-->>A: data envelope or problem
alt expected status
A-->>R: Response
R-->>UI: mapped typed data
else error
A-->>UI: AuthApiError
end