Skip to main content
The events feed is a log of changes in a library. Read it to keep a local copy of a library in sync, such as a mobile app’s cache or a search index, without listing every asset on each sync. Each event says that one entity changed. It doesn’t carry the entity’s new state: read that from the entity’s own endpoint.

Prerequisites

  • An API key with read access to the library
  • The Python SDK gumnut-sdk 0.168.0 or later, for the examples on this page

How the feed behaves

GET /api/events returns events in pages, each with an opaque cursor:
Three properties shape how you sync:
  • Nothing is skipped. Reading forward from a stored cursor returns every event committed after it, each once. A cursor is the only checkpoint you need.
  • Events are not in commit order. Two changes to one entity can arrive in either order, so apply an event by reading the entity’s current state, not by replaying the change.
  • Events can trail their commit. An event appears once every write that started before it has finished, so a change can take a moment to show up.
created_at is when the change’s transaction started. Use it for display only: it is neither the feed order nor a checkpoint, and a later event can carry an earlier time.

Sync a library

  1. Load your stored cursor, or start with none for a first sync.
  2. Request a page with after_cursor set to that cursor.
  3. Apply each event, as described below.
  4. After applying the page, store its next_cursor.
  5. Repeat until has_more is false. You are caught up; the next sync starts again at step 1.
This example syncs assets, albums, and album memberships. It requests only those entity types, so its cursor never passes an event it doesn’t apply.
store stands for your local storage, keyed by entity type and ID. Save the cursor in the same place as the data, ideally in the same transaction, so the two can’t drift apart. Make applying an event idempotent. If your client stops after applying a page but before storing its cursor, the next sync replays that page. While has_more is true, next_cursor also bounds the read to events that were ready when it began, so a busy library can’t keep a sync running forever. Changes made during the sync arrive in the next one. Pass the same library_id, entity_types, and created_at_gte on every page.

Apply events

Treat every event as “this changed”: read the entity’s current state and upsert it, or remove your copy when the read no longer returns it. Because you always apply current state, events that arrive out of order still leave you correct. Re-reads count against your rate limit. The list endpoints accept up to 200 ids, so read a page’s entities of each type in one call rather than one call per event.
  • Deletions and trash. Event types ending in _deleted or _removed mean the entity is gone, and asset_trashed moves an asset to the trash. A default read omits both, so the same rule removes your copy. asset_restored brings an asset back. Trashing or restoring an asset also hides or reveals its faces and album memberships, and changes its people’s and stack’s counts, without events of their own: re-read those by asset_id too.
  • Album membership. Each add creates a new membership with its own ID, so re-read memberships by ID like any other entity. Deleting an asset or album, or trashing an asset, removes or hides its memberships without separate events, so drop yours along with it, as the example does. When you remove an asset from an album, album_asset_removed also names the album_id and asset_id in its payload.
  • People and faces. A person’s face and asset counts change through its faces, so they arrive as face_* events, not person_updated. A face_updated payload names the face’s person_id and previous_person_id, and a face_deleted payload carries previous_person_id. Refetch each person that isn’t null, and expect a 404 for one deleted in the same change.
To sync people, faces, or stacks too, add their list endpoints to READERS and apply the rules above. A metadata event’s entity_id is an asset ID. The GET /api/events page in the API Reference tab lists the event types. Request only the entity types you apply. Within them, re-read state for any event type your client doesn’t recognize rather than failing.

Sync several entity types together

Some clients read one entity type at a time, such as albums before the photos in them. Each read stops at its own point, so a later read could include a change an earlier one stopped short of. Every response includes as_of, the point that read stopped at. Pass the first response’s as_of to every other read in the same sync so they all stop at the same point:
Keep a separate cursor per type, but never store as_of: it belongs to one sync. A shared as_of stops every read at one point, but an entity can still arrive before an entity it refers to. Let your client tolerate a missing reference until a later event fills it in.

Handle errors

GET /api/events returns 400 for:
  • an unknown entity type
  • a malformed after_cursor or as_of
  • an after_cursor or as_of ahead of the server, as after a restore that went back in time
Retrying the same request returns the same error. After a cursor error, discard the stored cursor and resync from none. created_at_lt is deprecated and ignored; a time bound could skip events. Bound a read with next_cursor and as_of instead.