Overview
Built for family life
Photo Album, Calendar, and Chat under one admin key on Premium Max (2,100 GB). Base URL:
https://api.ily.chat/v1
- Chat mirrors membership — private channels without the admin stay invisible.
- Album uploads: open an upload, add JPEG assets, complete once to verify and publish.
- API-created calendar events and chat messages show source = API.
Examples on this page
All curl samples use the placeholder key ily_fam_KEY
and path ids like FAMILY_ID,
UPLOAD_ID,
ASSET_ID, and CHANNEL_ID.
Replace them with your real values.
Create a key in the ILY app
Settings → Family Admin API (owner). Email OTP required. Secret shown once.
Call /v1 with the key
Bearer ily_fam_KEY — never send app JWTs to /v1.
Use cases
What can you build?
The Family Admin API is for Premium Max households that want to automate their private family space — without giving a third party the full app login.
- Automatically upload from a NAS or home server
- Send daycare or school photos into the family album
- Convert school emails into calendar events
- Post household automation alerts into family chat
- Import scanned historical family photos
- Connect a digital photo frame or camera
- Let a nanny or preschool system publish updates without receiving full family access
AI coding tools
Build with AI
If you’re using Cursor, ChatGPT, Claude, Copilot, or a similar assistant, give it the OpenAPI specification — the same machine-readable contract this page is based on. That beats pasting random curls: the model sees every path, parameter, and response shape in one file.
1. Grab the OpenAPI file
Download or link either format (they’re equivalent):
https://ily.chat/openapi/family-admin-v1.yaml
https://ily.chat/openapi/family-admin-v1.json
2. Hand it to your AI
- In the IDE — drop the file into the project (e.g.
openapi/family-admin-v1.yaml) and @-mention it, or paste the URL and ask the agent to fetch it. - In a chat tool — attach the YAML/JSON upload, or paste the raw URL and tell the model to treat it as the source of truth.
- Keep secrets out of the chat — never paste a real
ily_fam_…key. Use placeholders and put the real key in env vars on your machine.
3. Starter prompt
Copy something like this (swap in your goal):
Use this OpenAPI as the only source of truth for the ILY Family Admin API:
https://ily.chat/openapi/family-admin-v1.yaml
Base URL: https://api.ily.chat/v1
Auth: Authorization: Bearer ily_fam_KEY (Premium Max family admin key)
Build a small script that:
1) opens an upload (POST /families/{family_id}/uploads)
2) adds JPEG assets with content_hash = SHA-256 of the display bytes
3) PUTs display + 1000×1000 thumb to the signed URLs
4) calls POST …/complete and handles partial failures in assets[]
Follow the OpenAPI field names and error codes exactly. Do not invent endpoints.
Tip: if the assistant drifts, point it back at the OpenAPI file and this docs page
(https://ily.chat/docs/api) for human-readable detail on
rate limits, content_hash, and calendar UTC behavior.
Auth
Family Admin API keys
One rotatable key per family. Active Premium Max required. Keys may suspend with
premium_max_required if Max lapses.
Today a single key can upload photos, manage calendar, and post to chat.
Named keys with narrower scopes (for example album-write-only or calendar-only)
are planned so a leaked integration key has less blast radius.
Request headers
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationreq* | string | Bearer ily_fam_KEY — preferred. |
| X-ILY-Family-Api-Keyalt | string | Same key without Bearer, if your client cannot set Authorization. |
| Content-Typebody | string | application/json on POST/PUT with a body. |
* Provide Authorization or X-ILY-Family-Api-Key.
Authorization: Bearer ily_fam_KEY
Alternative:
X-ILY-Family-Api-Key: ily_fam_KEY
Key lifecycle
- Create in Settings → Family Admin API (owner only)
- Email OTP on create and regenerate
- Plaintext secret shown once; regenerate invalidates the old secret
Example
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/calendar/event-types \
-H "Authorization: Bearer ily_fam_KEY"
Limits
Rate limits & budgets
Limits are fixed-window counters. Exceeding a limit returns
HTTP 429 with
{"error":"…","code":"rate_limited"}.
There is no Retry-After header today — retry with
exponential backoff (start ~1s, cap ~60s).
Request limits (per rolling 60s window)
| Bucket | Limit | Applies to |
|---|---|---|
| Per key | 120 / min | Every authenticated /v1 request |
| Per family | 300 / min | All keys for that family combined |
| Upload open / add assets | 20 / min | POST …/uploads, POST …/assets |
| Complete / cancel | 40 / min | POST …/complete, POST …/cancel |
| Metadata reads | 120 / min | Upload/asset status, message list, channel reads |
| Chat post | 60 / min | POST …/messages |
Upload byte budgets
| Budget | Limit | Notes |
|---|---|---|
| Per key / hour | 200 MB claimed | Sum of file_size_bytes + archive_size_bytes at asset-create time |
| Per family / UTC day | 100 GB claimed | Same claimed sizes; resets at UTC midnight |
| Family storage | plan quota | Premium Max is 2,100 GB; over-quota returns plan_limit |
Photos
Upload into the album
Open an upload, add JPEG assets with signed PUT URLs, PUT to S3, then complete once to verify and publish. Photos in v1 are publish-only — no album timeline or download URLs via API.
v1 media policy
- JPEG only — display (≤1536×2048, ≤8 MB) + thumb (exactly 1000×1000, ≤2 MB)
- Optional archive (≤60 MB) when
upload_full_resis set on the upload (all-or-nothing) - 1–250 assets per upload · 12h TTL · multiple open uploads allowed
- Complete verifies JPEG headers in parallel, then publishes passers
content_hash & sizes
-
content_hash— SHA-256 of the exact display JPEG bytes you will PUT (lowercase hex, 64 characters). Hash after you finish resizing / encoding that display file. If you strip EXIF/metadata, hash the bytes after stripping. Do not hash the thumb or archive. Used for family-level dedupe and S3 keying; the server does not re-hash the object on complete. -
file_size_bytes— byte length of the display JPEG only (claimed against upload budgets). Thumbnail size is not declared — keep thumbs ≤2 MB. -
archive_size_bytes— required whenupload_full_res; archive JPEG size only.
Open upload
POST …/uploads → upload_id
Add assets
POST …/uploads/{id}/assets (1–250) → all signed PUT URLs
PUT to S3
Upload display + thumb (+ archive). Re-sign incomplete via status if needed.
Complete
POST …/uploads/{id}/complete verifies and publishes.
Open a new upload. Set upload_full_res: true to require an archive for every asset.
Example
curl -sS -X POST https://api.ily.chat/v1/families/FAMILY_ID/uploads \
-H "Authorization: Bearer ily_fam_KEY" \
-H "Content-Type: application/json" \
-d '{"upload_full_res": false}'
Response 200
{
"upload_id": "UPLOAD_ID",
"upload_full_res": false,
"status": "open",
"expires_in": 43200
}
Roll-up + per-asset status. Optional ?include_put_urls=true
re-signs PUT URLs for incomplete assets only (overwrite allowed until published).
Alias: GET …/uploads/{upload_id}.
curl -sS "https://api.ily.chat/v1/families/FAMILY_ID/uploads/UPLOAD_ID/status?include_put_urls=true" \
-H "Authorization: Bearer ily_fam_KEY"
Add 1–250 assets and receive all signed PUT URLs. Declare display dims ≤1536×2048.
When upload_full_res, include archive_size_bytes for each asset.
Body fields (per asset)
| Name | Type | Description |
|---|---|---|
| content_hashreq | string | SHA-256 (lowercase hex) of display JPEG bytes. |
| file_size_bytesreq | integer | Display JPEG size in bytes (≤8 MB). Not thumb/archive. |
| width_pxreq | integer | Display width ≤1536. |
| height_pxreq | integer | Display height ≤2048. |
| idempotency_keyreq | string | Stable client key; re-issue returns the same pending asset. |
| taken_atopt | date-time | Capture time (RFC3339). |
| archive_size_bytescond | integer | Required when upload_full_res; archive size ≤60 MB. |
curl -sS -X POST https://api.ily.chat/v1/families/FAMILY_ID/uploads/UPLOAD_ID/assets \
-H "Authorization: Bearer ily_fam_KEY" \
-H "Content-Type: application/json" \
-d '{
"assets": [{
"content_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"file_size_bytes": 420000,
"width_px": 1536,
"height_px": 2048,
"idempotency_key": "photo-001"
}]
}'
Response fields
| Name | Type | Description |
|---|---|---|
| asset_id | uuid | Id for status / complete results. |
| display_put_url | uri | PUT JPEG display master. |
| thumb_put_url | uri | PUT JPEG 1000×1000 thumb (size not declared; keep ≤2 MB). |
| archive_put_url | uri | null | Present when upload_full_res. |
Single-asset status; optional include_put_urls for incomplete only.
Verifies all pending assets (HEAD + JPEG header Range GET), publishes passers into the album,
and leaves failures retryable. Idempotent for already published assets. Closes the upload when all succeed.
Always inspect the per-asset assets[] array — one call can mix successes and failures.
curl -sS -X POST https://api.ily.chat/v1/families/FAMILY_ID/uploads/UPLOAD_ID/complete \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · partial success possible
{
"upload_id": "UPLOAD_ID",
"status": "open",
"published": 1,
"already_published": 1,
"failed": 1,
"pending": 2,
"assets": [
{
"asset_id": "ASSET_OK",
"status": "published",
"album_asset_id": "ALBUM_ASSET_ID"
},
{
"asset_id": "ASSET_DUP",
"status": "already_published",
"album_asset_id": "EXISTING_ALBUM_ASSET"
},
{
"asset_id": "ASSET_FAIL",
"status": "failed",
"error": {
"code": "invalid_jpeg",
"message": "display object is not a valid JPEG"
}
},
{
"asset_id": "ASSET_RETRY",
"status": "pending"
}
]
}
Per-asset status
| status | Meaning |
|---|---|
| published | Newly verified and written into the album this call. |
| already_published | Idempotent — already in the album (same content_hash or prior complete). |
| failed | Verification failed; see error. Fix bytes / re-PUT, then complete again. |
| pending | Still incomplete (missing object, not ready). Re-PUT if needed, then complete again. |
Upload status is completed only when every asset is published;
otherwise it stays open so you can retry.
Abandon the upload and clean up staging.
curl -sS -X POST https://api.ily.chat/v1/families/FAMILY_ID/uploads/UPLOAD_ID/cancel \
-H "Authorization: Bearer ily_fam_KEY"
Calendar
Events
Full read/write. API-created events keep source = api; the admin can still edit them in the app.
Timezone & dates
-
All datetimes are stored and returned in UTC.
A bare
YYYY-MM-DDis parsed as midnight UTC that day (so"2026-03-12"→2026-03-12T00:00:00Z). - All-day events are not anchored to a family timezone — use the UTC calendar date you intend. Prefer RFC3339 with an explicit offset when the local civil date matters across DST.
-
List range
from…tois inclusive on both ends (starts_at >= from && starts_at <= to). AYYYY-MM-DDtoonly includes events at that day’s 00:00:00Z — pass an end-of-day RFC3339 if you need the whole day. - Recurrence expansion uses fixed UTC weekday numbers; daylight-saving transitions do not shift all-day UTC midnights. Timed recurring events keep their UTC clock time across DST.
List calendar events in a date range.
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
Query parameters
| Name | Type | Description |
|---|---|---|
| fromreq | string | Inclusive range start — RFC3339 or YYYY-MM-DD (→ 00:00:00Z). |
| toreq | string | Inclusive range end — RFC3339 or YYYY-MM-DD (→ 00:00:00Z). |
Example
curl -sS "https://api.ily.chat/v1/families/FAMILY_ID/events?from=2026-03-01&to=2026-03-31" \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · Event[]
[
{
"id": "EVENT_ID",
"title": "Soccer practice",
"event_type": "activity",
"starts_at": "2026-03-12T00:00:00Z",
"ends_at": null,
"all_day": true,
"notes": "Bring water bottle",
"recurrence_freq": null,
"recurrence_interval": 1,
"child_ids": [],
"attendee_user_ids": [],
"source": "api"
}
]
Create a calendar event. Response includes source: "api".
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
Body (JSON · EventWrite)
| Name | Type | Description |
|---|---|---|
| titlereq | string | Event title. |
| event_typereq | string | Event type key or id (see event-types). |
| starts_atreq | string | RFC3339 or YYYY-MM-DD (→ midnight UTC). |
| ends_atopt | string | null | End time / date when applicable. |
| all_dayopt | boolean | Default true. Anchored to UTC date, not a family TZ. |
| notesopt | string | null | Free-text notes. |
| child_idsopt | uuid[] | Related children. |
| attendee_user_idsopt | uuid[] | Attending members. |
| recurrence_freqopt | string | null | daily · weekly · monthly · yearly. |
| recurrence_intervalopt | integer | Default 1 (every N periods). |
| recurrence_by_weekdayopt | integer[] | null | ISO weekdays: 1=Monday … 7=Sunday. Values outside 1–7 are ignored. |
| recurrence_untilopt | string | null | Recurrence end bound (UTC). |
| recurrence_countopt | integer | null | Occurrence count limit (1–999). |
Example
curl -sS -X POST https://api.ily.chat/v1/families/FAMILY_ID/events \
-H "Authorization: Bearer ily_fam_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Soccer practice",
"event_type": "activity",
"starts_at": "2026-03-12",
"all_day": true,
"notes": "Bring water bottle"
}'
Response 200 · Event
{
"id": "EVENT_ID",
"title": "Soccer practice",
"event_type": "activity",
"starts_at": "2026-03-12T00:00:00Z",
"ends_at": null,
"all_day": true,
"notes": "Bring water bottle",
"recurrence_interval": 1,
"child_ids": [],
"attendee_user_ids": [],
"source": "api"
}
Fetch a single calendar event.
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
| event_idreq | uuid | Event id. |
Example
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/events/EVENT_ID \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · Event
{
"id": "EVENT_ID",
"title": "Soccer practice",
"event_type": "activity",
"starts_at": "2026-03-12T00:00:00Z",
"all_day": true,
"source": "api"
}
Update a calendar event. Preserves source: "api" when the event originated from the API. Body uses the same EventWrite fields as create.
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
| event_idreq | uuid | Event id. |
Example
curl -sS -X PUT https://api.ily.chat/v1/families/FAMILY_ID/events/EVENT_ID \
-H "Authorization: Bearer ily_fam_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Soccer practice (rescheduled)",
"event_type": "activity",
"starts_at": "2026-03-13",
"all_day": true,
"notes": "Field B"
}'
Response 200 · Event
{
"id": "EVENT_ID",
"title": "Soccer practice (rescheduled)",
"starts_at": "2026-03-13T00:00:00Z",
"source": "api"
}
Delete a calendar event. Returns an empty body.
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
| event_idreq | uuid | Event id. |
Example
curl -sS -X DELETE https://api.ily.chat/v1/families/FAMILY_ID/events/EVENT_ID \
-H "Authorization: Bearer ily_fam_KEY" \
-w "\n%{http_code}\n"
Response 204 No Content
HTTP/1.1 204 No Content
List calendar event types (system + family custom).
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
Example
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/calendar/event-types \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · EventType[]
[
{
"key": "activity",
"label": "Activity",
"color": "#1a5fc4",
"sort_order": 10
}
]
Response fields
| Name | Type | Description |
|---|---|---|
| key | string | Stable key used in event_type. |
| label | string | Display label. |
| color | string | UI color. |
| sort_order | integer | Ordering hint. |
Chat
Channels & messages
Membership-scoped. Private channels that exclude the admin stay hidden. Posted messages use source = api / sender API.
List chat channels the family admin can access.
Path parameters
| Name | Type | Description |
|---|---|---|
| family_idreq | uuid | Family id. |
Example
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/channels \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · Channel[]
[
{
"id": "CHANNEL_ID",
"name": "Family",
"is_default": true,
"color": null
}
]
Get one channel the admin is a member of.
Path parameters
| Name | Type | Description |
|---|---|---|
| channel_idreq | uuid | Channel id. |
Example
curl -sS https://api.ily.chat/v1/channels/CHANNEL_ID \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · Channel
{
"id": "CHANNEL_ID",
"name": "Family",
"is_default": true
}
List messages in a channel. Attached assets are metadata only (no image bytes).
Path parameters
| Name | Type | Description |
|---|---|---|
| channel_idreq | uuid | Channel id. |
Query parameters
| Name | Type | Description |
|---|---|---|
| limitopt | integer | Page size 1–200 (default 50). |
| beforeopt | string | RFC3339 timestamp — return messages strictly older than this created_at. |
The response is a bare Message[] (newest page first).
There is no next_before / has_more field —
continue by setting before to the oldest
created_at in the page. Stop when the page is empty or shorter than limit.
Example
curl -sS "https://api.ily.chat/v1/channels/CHANNEL_ID/messages?limit=50" \
-H "Authorization: Bearer ily_fam_KEY"
# next older page — use oldest created_at from the previous response
curl -sS "https://api.ily.chat/v1/channels/CHANNEL_ID/messages?limit=50&before=2026-03-12T16:00:00Z" \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · Message[]
[
{
"id": "MESSAGE_ID",
"source": "api",
"author_display_name": "API",
"body": "Practice moved to 4pm",
"created_at": "2026-03-12T16:00:00Z",
"deleted": false
}
]
Send a message. Attributed as API (source: "api"). Provide body and/or asset_id.
Path parameters
| Name | Type | Description |
|---|---|---|
| channel_idreq | uuid | Channel id. |
Body (JSON)
| Name | Type | Description |
|---|---|---|
| bodyopt | string | Message text (default empty). Required if no asset. |
| asset_idopt | uuid | null | Attach a previously uploaded image (metadata in reads). |
| reply_to_message_idopt | uuid | null | Reply target. |
Example
curl -sS -X POST https://api.ily.chat/v1/channels/CHANNEL_ID/messages \
-H "Authorization: Bearer ily_fam_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Practice moved to 4pm"}'
Response 200 · Message
{
"id": "MESSAGE_ID",
"source": "api",
"author_display_name": "API",
"body": "Practice moved to 4pm",
"created_at": "2026-03-12T16:00:00Z",
"deleted": false
}
Fetch a single message.
Path parameters
| Name | Type | Description |
|---|---|---|
| channel_idreq | uuid | Channel id. |
| message_idreq | uuid | Message id. |
Example
curl -sS https://api.ily.chat/v1/channels/CHANNEL_ID/messages/MESSAGE_ID \
-H "Authorization: Bearer ily_fam_KEY"
Response 200 · Message
{
"id": "MESSAGE_ID",
"source": "api",
"author_display_name": "API",
"body": "Practice moved to 4pm",
"created_at": "2026-03-12T16:00:00Z",
"deleted": false
}
Errors
Error codes
Errors return JSON with error (human message) and
code (stable machine code).
| Code | HTTP | When |
|---|---|---|
| premium_max_required | 402 | Not on Premium Max / Max lapsed |
| invalid_api_key | 401 | Missing or unknown key |
| api_key_revoked | 401 | Key revoked |
| api_key_suspended | 403 | Key suspended |
| forbidden | 403 | Not allowed / membership |
| locked | 403 | Family locked |
| not_found | 404 | Unknown resource |
| bad_request | 400 | Validation error |
| plan_limit | 402 | Storage / plan quota |
| rate_limited | 429 | Request or upload-byte budget exceeded (no Retry-After; backoff) |
| conflict | 409 | Conflicting state |
| internal | 500 | Unexpected server error |
Example error body
{
"error": "Premium Max is required for the Family Admin API",
"code": "premium_max_required"
}