# Creatives sync

> **TL;DR** — Pull approved ad creatives incrementally: pass your last checkpoint as `updated_since`, follow `next_cursor` to the end, overwrite records by `creative_id`, and move the checkpoint only after the whole run succeeded. Re-check a stored creative with `GET /creatives/{id}`; stop using it after the UTC day of `rights.ends_at`.

## Before you start
- A key with `creatives:export` for the job, in `WANDURA_API_KEY`. The marketing job never needs `playback`.
- The record and the envelope: [Creatives](https://docs.wandura.ai/v1/developers/concepts/creatives).

## The procedure
1. **First run:** `GET /creatives?limit=100` without `updated_since`; while `data.next_cursor` is not `null`, call again with `cursor=<next_cursor>`.
2. **Upsert** every record by `creative_id`. A record can come twice (`updated_at` is rounded down to the second, and `updated_since` is strict): overwrite it.
3. **Checkpoint:** after the run **completed**, save the largest `updated_at` you received.
4. **Later runs:** the same, with `updated_since=<checkpoint>` on every page of the run.
5. **On failure** (including `422` `4222`, a cursor no longer valid): keep the old checkpoint; the next run starts again from it. Nothing is skipped.
6. **Withdrawn creatives** are not announced. Before you use a stored creative, or once a day, call `GET /creatives/{id}`: `404` `4040` means stop using it.
7. **Rights:** usable until the end of the UTC day `rights.ends_at` points at (`ends_at + 86400`).

A few runs an hour is well within the creatives limit.

## Code
```ts
export async function syncCreatives(store: CreativeStore, o: CallOptions = {}): Promise<boolean> {
  const since = await store.checkpoint();
  let cursor: string | null = null;
  let max = since ?? 0;
  try {
    do {
      const qs = new URLSearchParams({ limit: '100' });
      if (since !== null) qs.set('updated_since', String(since)); // the same value on every page of a run
      if (cursor) qs.set('cursor', cursor);
      const { data }: { data: Envelope } = await call<Envelope>('GET', `/api/delivery/v1/creatives?${qs}`, { policy: BACKGROUND, ...o });
      for (const c of data.data.items) { await store.upsert(c); max = Math.max(max, c.updated_at); }
      cursor = data.data.next_cursor;
    } while (cursor);
  } catch (e) {
    // 422 / code 4222 (cursor no longer valid) or anything else: the next run restarts from the old checkpoint.
    log('creatives_sync_failed', { status: (e as { status?: number }).status, code: (e as { errorCode?: string }).errorCode });
    return false;
  }
  await store.saveCheckpoint(max); // only after the whole run: nothing is ever skipped
  return true;
}
```

## Done when
- [ ] A second run with no changes on Wandura's side returns nothing new.
- [ ] A run interrupted half-way does not move the checkpoint.
- [ ] A creative past its rights day is no longer used.

## Related
- [List creatives](https://docs.wandura.ai/v1/developers/reference/creatives-list), [Get a creative](https://docs.wandura.ai/v1/developers/reference/creatives-get)
