# Event feed

mint v1 has no webhooks. Instead, every company has an ordered **event feed**: you ask for everything after the last
event you processed. This works behind firewalls, needs no public endpoint on your side and never loses an event while
your system is down.

A person approves a film in mint; only then can it be downloaded. The key needs `films:read`, `events:read` and
`files:read`. Downloads and the feed are free of credits.

1. `GET /events?limit=1`: note `latest_seq` as H.
2. `GET /films?status=approved` (all pages): download each film from `file_url` and compare the SHA-256 of the bytes with
   the `ETag`.
3. From then on, for example once a day: `GET /events?after=H&types=film.approved,film.approval_revoked,film.deleted`,
   process the events, store `next_after` as the new H, repeat while `has_more`.
4. `410 cursor_expired` or `400 invalid_cursor`: start again with step 1.

A film can be downloaded as long as its approval is active: after a revocation the download returns `404 not_approved`;
after a new approval only the new revision is available. Films that change between step 1 and 2 come twice; that is
harmless because the SHA-256 identifies the file.

### Event types

- `film.created`
- `film.status_changed`
- `film.storyboard_ready`
- `film.approved`
- `film.approval_revoked`
- `film.cancelled`
- `film.deleted`
- `film.progress`
- `feedback.updated`
- `storyboard.preview_ready`
- `operation.completed`

New types may be added within v1; ignore types you do not know. Events are kept for 180 days.

### Rules for a robust consumer

- Store `next_after` only after the page was processed. If you crash in between, you read the page again.
- Events can come twice (at least once delivery). Use the event `id` to skip duplicates.
- `data` is a snapshot at the time of the event; `film` (with `films:read`) is the current state.
- Poll every few minutes. There is no need to poll faster than once a minute.

```js Poll the feed (Node.js)
let after = await loadCursor(); // e.g. from your database, start with "0"
for (;;) {
  const url = new URL("https://api.cream-ai.de/api/v1/events");
  url.searchParams.set("after", after);
  url.searchParams.set("types", "film.approved,film.approval_revoked,film.deleted");
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.MINT_API_KEY}` },
  });
  if (res.status === 410 || res.status === 400) {
    after = await resync(); // step 1 and 2 above
    continue;
  }
  const page = await res.json();
  for (const event of page.data) await handle(event);
  after = String(page.next_after);
  await saveCursor(after);
  if (!page.has_more) break;
}
```
