# Syncing the catalog

The products list returns one page of deals inside your inventory scope. Load the catalog into
your own database once, then pull only what changed every few hours. Your site renders deals
from your database, never live from Groupon. The full response shape, with every field, is in
the [API reference](/reference).

## How to use the fields

- **Price**: `option.price.retail` is the per-unit price the shopper pays. `option.price.original`
  is the strike-through price.
- **What to list**: show a product only when `status` is `active` and `availabilityRequired` is
  false. Show an option only when its `active` is not false (`null` means unknown: treat it as
  sellable). `sold_out` and `expired` products cannot be bought.
- Products with `availabilityRequired: true` need a time-slot booking and cannot be added to a
  cart. They are rare. Skip them.
- **Quantity limits**: enforce `minUnits` and `maxUnits` from the option in your UI (`null`
  maximum means no maximum).
- **Promotion**: `option.promotion` is a promotional price the shopper gets by entering
  `promotion.promoCode` on Groupon's checkout page before `promotion.endsAt`. It is informational:
  show it as "Use code SAVE15 at checkout." Cart totals always use retail. `promotion` can be
  `null` at any time.
- **Images**: each entry in a product's `media` has a `role` that names a size (`small`,
  `medium`, `large`, `extra_large`, `wide_crop`). Every entry is the same picture at a different
  size, and not every size is present on every deal. Pick by role, for example `large` for a
  product page and `medium` for a grid, falling back to the entry with the largest `width`.
  `width` and `height` can be `null`.
- A page can contain fewer products than `limit`, or even none, while `hasMore` is true. Keep
  paging until `hasMore` is false.

## Keeping your catalog fresh

Deals keep changing: prices move, and deals are activated and deactivated. Load the catalog
once, then pull only what changed every few hours using `updatedSince`. Store the time of your
last refresh so the next pull knows where to start.

### Step 1: initial full load, once

```text
params = { country: "US", limit: 10 }               # no updatedSince
cursor = none
repeat:
    page = GET https://api-staging-core.livingsocial.com/partner_storefront/products?params (+ cursor if set)
    upsert every product in page.data by its id
    cursor = page.nextCursor
until page.hasMore == false
lastRefreshAt = page.timestamp - 10 minutes    # the final page's timestamp; persist ONLY after
                                                # the whole walk succeeds
```

### Step 2: delta refresh, every few hours

```text
params = { country: "US", limit: 10, updatedSince: lastRefreshAt }    # do NOT send active
cursor = none
repeat:
    page = GET https://api-staging-core.livingsocial.com/partner_storefront/products?params (+ cursor if set)
    upsert every product in page.data by its id    # replace options, prices, status
    cursor = page.nextCursor
until page.hasMore == false
lastRefreshAt = page.timestamp - 10 minutes    # the final page's timestamp; persist ONLY after
                                                # the whole walk succeeds
```

Store `lastRefreshAt` durably, as a database row. If a walk fails part-way, do not advance it:
restart the walk from the previous `lastRefreshAt`.

The 10-minute overlap is intended. Upserts are idempotent, so re-reading a deal is harmless, and
missing one is not.

Take the time from the final page's `timestamp` (Groupon's clock), not your own clock: an
`updatedSince` in the future is rejected.

Do not send `active=true` on a delta. A deal that was deactivated since your last refresh comes
back with a status other than `active`; you need that row to stop listing it.

After each upsert, recompute whether the product is listable (see "How to use the fields" above).

A delta can legitimately return the whole catalog. When a promotion starts, many prices change
at once without touching each deal's own update time, so Groupon ignores `updatedSince` for that
walk. The decision is made once, on the first page, and carried in the cursor, so a walk is
never half incremental and half full. Your code must handle a delta of any size.

You can repeat the full load at any time, for example to rebuild your database.

## Rules for every walk

- Page sequentially. Do not request pages in parallel.
- Send the same parameters on every page of a walk. Only `cursor` changes. Changing a filter
  mid-walk invalidates the cursor.
- A cursor is valid for 24 hours.
- If a page returns `BAD_REQUEST` with a cursor problem, discard the cursor and restart that
  walk from page 1.
- On a retryable failure, retry the same page with backoff. See [Handling errors](/docs/handling-errors.md).
- The catalog in your database is a cache. The cart endpoints re-check price and availability
  live, so they are the final authority at add-to-cart time.

## Example request

```bash
curl -s "https://api-staging-core.livingsocial.com/partner_storefront/products?country=US&limit=10&active=true" \
  -H "Authorization: Bearer $GROUPON_API_KEY" \
  -H "User-Agent: MyStore/1.0 (+https://mystore.example)" \
  -H "x-request-id: $(uuidgen)"
```
