Skip to content
ILY

Import & Automation API · v1

Import into your family space.

Publish photos, manage the calendar, and post to chat with named scoped keys — import and automation only; media browsing stays in the ILY apps.

Album · Calendar · Chat — included with Premium Max

Overview

Built for import & automation

Publish into Photo Album, Calendar, and Chat with named scoped keys on Premium Max (2,100 GB). This API does not serve album timelines or media download URLs. Base URL:

http
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 named keys in the ILY app

Settings → Family Admin API (owner). Choose name + scopes. 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):

url
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):

prompt
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

Named scoped API keys

Create up to 10 named keys per family, each with its own scopes. Active Premium Max required. All keys for a family suspend together if Max lapses (premium_max_required / api_key_suspended). Available scopes: photos:write, videos:write, calendar:read, calendar:write, chat:read, chat:write. Missing a required scope returns HTTP 403 with insufficient_scope. There is no media-read scope — this API never returns album browse or signed view/download URLs.

Request headers

Headers

NameTypeDescription
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.

http
Authorization: Bearer ily_fam_KEY

Alternative:

http
X-ILY-Family-Api-Key: ily_fam_KEY

Key lifecycle

  • Create in Settings → Family Admin API (owner only) with a name and scopes
  • Email OTP on create, rotate, and revoke
  • Plaintext secret shown once; rotating a key keeps its id and invalidates the old secret

Example

bash
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 per family (all keys combined) — not rolling windows. Each window is 60 seconds from the first hit in that bucket. Exceeding a limit returns HTTP 429 with {"error":"…","code":"rate_limited"} and a Retry-After header (seconds). Honor Retry-After; you may still use exponential backoff as a fallback.

Request limits (fixed 60s window, all keys combined)

BucketLimitApplies to
Per family 120 / min Every authenticated /v1 request (all keys 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

BudgetLimitNotes
Per family / hour 10 GB claimed All keys combined. Sum of file_size_bytes + archive_size_bytes at asset-create time. Sized so sustained imports can approach the daily budget.
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 & videos

Upload into the album

Open an upload, add photo and/or video assets with signed PUT URLs, PUT to S3, then complete once to verify and publish. Media in v1 is publish-only — no album timeline or download URLs via API. Photos need photos:write; videos need videos:write.

v1 media policy

  • Photos — display JPEG (≤1536×2048, ≤8 MB) + thumb (exactly 1000×1000, ≤2 MB)
  • Optional photo archive (≤60 MB) when upload_full_res is set (all-or-nothing; photos only)
  • Videos — video/*, recommend ≤720p long-edge, ≤2 GiB; poster thumb exactly 1000×1000 JPEG
  • Videos ≥16 MiB use S3 multipart (mode: multipart + part URLs)
  • 1–250 assets per upload · 12h TTL · multiple open uploads allowed
  • Complete verifies pending assets in parallel, then publishes passers

content_hash & sizes

  • content_hash — Photos: SHA-256 of the exact display JPEG bytes. Videos: SHA-256 of the exact poster thumb JPEG bytes (lowercase hex, 64 characters). Verified on complete (mismatch → content_hash_mismatch).
  • file_size_bytes — Photos: display JPEG size. Videos: encoded video size (claimed against upload budgets). Keep thumbs ≤2 MB.
  • mime_type — default image/jpeg; use video/* for videos (requires duration_ms).
  • archive_size_bytes — required when upload_full_res; photo archive JPEG size only.

Open upload

POST …/uploads → upload_id

Reconcile signatures

POST …/uploads/{id}/reconcile with content_hash + metadata → soft_complete for already-imported items, PUT URLs only for needs_upload

PUT to S3

Upload only needs_upload items. Soft-complete items skip this step entirely.

Complete

POST …/uploads/{id}/complete verifies staged assets and publishes. Soft-complete items never enter the upload session.

POST /families/{family_id}/uploads

Open a new upload. Set upload_full_res: true to require an archive for every asset.

Example

bash
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

json
{
  "upload_id": "UPLOAD_ID",
  "upload_full_res": false,
  "status": "open",
  "expires_in": 43200
}
GET /families/{family_id}/uploads/{upload_id}/status

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}.

bash
curl -sS "https://api.ily.chat/v1/families/FAMILY_ID/uploads/UPLOAD_ID/status?include_put_urls=true" \
  -H "Authorization: Bearer ily_fam_KEY"
POST /families/{family_id}/uploads/{upload_id}/reconcile

Preferred re-import step. Send content_hash + metadata for the files you intend to upload. Hashes already in the album return soft_complete (no staging, no PUT URLs, no budget claim). New hashes return needs_upload with signed PUT URLs.

Response fields

NameTypeDescription
soft_completeintegerCount already published — skip upload.
needs_uploadintegerCount with PUT URLs to upload.
assets[].statusstringsoft_complete or needs_upload.
assets[].album_asset_iduuidPresent when soft_complete.
POST /families/{family_id}/uploads/{upload_id}/assets

Add 1–250 assets and receive signed PUT URLs for new content. Known content_hash values return soft_complete (prefer /reconcile for bulk preflight). Declare display dims ≤1536×2048. When upload_full_res, include archive_size_bytes for each photo asset.

Body fields (per asset)

NameTypeDescription
content_hashreqstringPhotos: SHA-256 of display JPEG. Videos: SHA-256 of poster thumb JPEG.
file_size_bytesreqintegerPhotos: display JPEG ≤8 MB. Videos: video bytes ≤2 GiB.
width_pxreqintegerPhoto display width ≤1536; video width.
height_pxreqintegerPhoto display height ≤2048; video height.
idempotency_keyreqstringStable client key; re-issue returns the same pending asset.
mime_typeoptstringDefault image/jpeg; use video/* for videos.
duration_mscondintegerRequired for videos.
taken_atoptdate-timeCapture time (RFC3339).
archive_size_bytescondintegerRequired when upload_full_res (photos only); archive size ≤60 MB.
bash
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

NameTypeDescription
asset_iduuidId for status / complete results.
modestringsingle or multipart (large videos).
display_put_urluriPhotos: PUT JPEG display master.
original_put_urluriVideos (single): PUT video bytes.
thumb_put_urluriPUT JPEG 1000×1000 thumb/poster (keep ≤2 MB).
partsarrayVideos (multipart): [{part_number, url}].
archive_put_urluri | nullPresent when upload_full_res (photos).
GET /families/{family_id}/uploads/{upload_id}/assets/{asset_id}

Single-asset status; optional include_put_urls for incomplete only (re-signs multipart parts too).

POST /families/{family_id}/uploads/{upload_id}/complete

Verifies all pending assets, publishes passers into the album, and leaves failures retryable. Idempotent for already published assets. Closes the upload when all succeed. For multipart videos, send part ETags in the body. Always inspect the per-asset assets[] array — one call can mix successes and failures.

bash
curl -sS -X POST https://api.ily.chat/v1/families/FAMILY_ID/uploads/UPLOAD_ID/complete \
  -H "Authorization: Bearer ily_fam_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": [{
      "asset_id": "VIDEO_ASSET_ID",
      "parts": [{"part_number": 1, "etag": "\"etag-from-s3\""}]
    }]
  }'

Response 200 · partial success possible

json
{
  "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

statusMeaning
publishedNewly verified and written into the album this call.
already_publishedIdempotent — already in the album (same content_hash or prior complete).
failedVerification failed; see error (including content_hash_mismatch). Fix bytes / re-PUT, then complete again.
pendingStill 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.

POST /families/{family_id}/uploads/{upload_id}/cancel

Abandon the upload and clean up staging.

bash
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-DD is 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 is half-open: starts_at >= from && starts_at < to. For all of March use from=2026-03-01&to=2026-04-01. A bare YYYY-MM-DD to is midnight UTC that day (exclusive).
  • 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.
GET /families/{family_id}/events

List calendar events in a date range.

Path parameters

NameTypeDescription
family_idrequuidFamily id.

Query parameters

NameTypeDescription
fromreqstringInclusive range start — RFC3339 or YYYY-MM-DD (→ 00:00:00Z).
toreqstringExclusive range end — RFC3339 or YYYY-MM-DD (→ 00:00:00Z).

Example

bash
curl -sS "https://api.ily.chat/v1/families/FAMILY_ID/events?from=2026-03-01&to=2026-04-01" \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · Event[]

json
[
  {
    "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"
  }
]
POST /families/{family_id}/events

Create a calendar event. Response includes source: "api".

Path parameters

NameTypeDescription
family_idrequuidFamily id.

Body (JSON · EventWrite)

NameTypeDescription
titlereqstringEvent title.
event_typereqstringEvent type key or id (see event-types).
starts_atreqstringRFC3339 or YYYY-MM-DD (→ midnight UTC).
ends_atoptstring | nullEnd time / date when applicable.
all_dayoptbooleanDefault true. Anchored to UTC date, not a family TZ.
notesoptstring | nullFree-text notes.
child_idsoptuuid[]Related children.
attendee_user_idsoptuuid[]Attending members.
recurrence_freqoptstring | nulldaily · weekly · monthly · yearly.
recurrence_intervaloptintegerDefault 1 (every N periods).
recurrence_by_weekdayoptinteger[] | nullISO weekdays: 1=Monday … 7=Sunday. Values outside 1–7 return 400 bad_request.
recurrence_untiloptstring | nullRecurrence end bound (UTC).
recurrence_countoptinteger | nullOccurrence count limit (1–999).

Example

bash
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

json
{
  "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"
}
GET /families/{family_id}/events/{event_id}

Fetch a single calendar event.

Path parameters

NameTypeDescription
family_idrequuidFamily id.
event_idrequuidEvent id.

Example

bash
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/events/EVENT_ID \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · Event

json
{
  "id": "EVENT_ID",
  "title": "Soccer practice",
  "event_type": "activity",
  "starts_at": "2026-03-12T00:00:00Z",
  "all_day": true,
  "source": "api"
}
PUT /families/{family_id}/events/{event_id}

Update a calendar event. Preserves source: "api" when the event originated from the API. Body uses the same EventWrite fields as create.

Path parameters

NameTypeDescription
family_idrequuidFamily id.
event_idrequuidEvent id.

Example

bash
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

json
{
  "id": "EVENT_ID",
  "title": "Soccer practice (rescheduled)",
  "starts_at": "2026-03-13T00:00:00Z",
  "source": "api"
}
DELETE /families/{family_id}/events/{event_id}

Delete a calendar event. Returns an empty body.

Path parameters

NameTypeDescription
family_idrequuidFamily id.
event_idrequuidEvent id.

Example

bash
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
HTTP/1.1 204 No Content
GET /families/{family_id}/calendar/event-types

List calendar event types (system + family custom).

Path parameters

NameTypeDescription
family_idrequuidFamily id.

Example

bash
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/calendar/event-types \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · EventType[]

json
[
  {
    "key": "activity",
    "label": "Activity",
    "color": "#1a5fc4",
    "sort_order": 10
  }
]

Response fields

NameTypeDescription
keystringStable key used in event_type.
labelstringDisplay label.
colorstringUI color.
sort_orderintegerOrdering hint.

Chat

Channels & messages

Membership-scoped. Private channels that exclude the admin stay hidden. Posted messages use source = api / sender API.

GET /families/{family_id}/channels

List chat channels the family admin can access.

Path parameters

NameTypeDescription
family_idrequuidFamily id.

Example

bash
curl -sS https://api.ily.chat/v1/families/FAMILY_ID/channels \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · Channel[]

json
[
  {
    "id": "CHANNEL_ID",
    "name": "Family",
    "is_default": true,
    "color": null
  }
]
GET /channels/{channel_id}

Get one channel the admin is a member of.

Path parameters

NameTypeDescription
channel_idrequuidChannel id.

Example

bash
curl -sS https://api.ily.chat/v1/channels/CHANNEL_ID \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · Channel

json
{
  "id": "CHANNEL_ID",
  "name": "Family",
  "is_default": true
}
GET /channels/{channel_id}/messages

List messages in a channel. Attached assets are metadata only (no image bytes).

Path parameters

NameTypeDescription
channel_idrequuidChannel id.

Query parameters

NameTypeDescription
limitoptintegerPage size 1–200 (default 50).
cursoroptstringOpaque cursor from next_cursor (preferred; encodes time + message id).
beforeoptstringDeprecated. Prefer cursor. Accepts opaque cursor, {rfc3339}~{message_id}, or RFC3339-only.

Response shape: {"messages":[…],"next_cursor":"…"} (newest page first). Continue with cursor=<next_cursor> until next_cursor is omitted. Opaque cursors avoid skipping messages that share the same created_at.

Example

bash
curl -sS "https://api.ily.chat/v1/channels/CHANNEL_ID/messages?limit=50" \
  -H "Authorization: Bearer ily_fam_KEY"

# next older page — pass next_cursor from the previous response
curl -sS "https://api.ily.chat/v1/channels/CHANNEL_ID/messages?limit=50&cursor=CURSOR" \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · MessageListResponse

json
{
  "messages": [
    {
      "id": "MESSAGE_ID",
      "source": "api",
      "author_display_name": "API",
      "body": "Practice moved to 4pm",
      "created_at": "2026-03-12T16:00:00Z",
      "deleted": false
    }
  ],
  "next_cursor": "CURSOR"
}
POST /channels/{channel_id}/messages

Send a message. Attributed as API (source: "api"). Provide body and/or asset_id.

Path parameters

NameTypeDescription
channel_idrequuidChannel id.

Body (JSON)

NameTypeDescription
bodyoptstringMessage text (default empty). Required if no asset.
asset_idoptuuid | nullAttach a previously uploaded image (metadata in reads).
reply_to_message_idoptuuid | nullReply target.

Example

bash
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

json
{
  "id": "MESSAGE_ID",
  "source": "api",
  "author_display_name": "API",
  "body": "Practice moved to 4pm",
  "created_at": "2026-03-12T16:00:00Z",
  "deleted": false
}
GET /channels/{channel_id}/messages/{message_id}

Fetch a single message.

Path parameters

NameTypeDescription
channel_idrequuidChannel id.
message_idrequuidMessage id.

Example

bash
curl -sS https://api.ily.chat/v1/channels/CHANNEL_ID/messages/MESSAGE_ID \
  -H "Authorization: Bearer ily_fam_KEY"

Response 200 · Message

json
{
  "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_required402Not on Premium Max / Max lapsed
invalid_api_key401Missing or unknown key
api_key_revoked401Key revoked
api_key_suspended403Key suspended
insufficient_scope403Key missing a required scope
forbidden403Not allowed / membership
locked403Family locked
not_found404Unknown resource
bad_request400Validation error
plan_limit402Storage / plan quota
rate_limited429Request or upload-byte budget exceeded; includes Retry-After
conflict409Conflicting state
internal500Unexpected server error

Example error body

json
{
  "error": "Premium Max is required for the Family Admin API",
  "code": "premium_max_required"
}