# LivingSocial documentation for builders

> Every guide for builders in reading order, as one Markdown file: How it works; Get your API key (AI builders); Syncing the catalog; Managing the cart; Checkout and order confirmation; Handling errors. Each guide is also served on its own at /docs/<slug>.md. The API documentation is the OpenAPI document at /partner-openapi.json (LivingSocial Partner API).

---

# How it works

This guide is for builders of a website or app on the Partner API.

The LivingSocial Partner API lets partners, creators and builders (and the AI coding assistants
they build with) list eligible Groupon US offers on their own site from a synced copy of the
catalog, mirror the shopper's cart at Groupon, and send the shopper to Groupon's checkout to pay.
There is no search endpoint, and only US inventory is exposed.

## End-to-end flow

```text
1. Get your key      Sign in to this portal, register a storefront, create an API key
2. Sync catalog      GET /partner_storefront/products -> your database
                      (a full load, then a full resync every day; a delta every two hours
                      is optional)
3. Shopper browses    Your site renders offers from YOUR database
4. Add to cart        First item:  POST /partner_storefront/carts
                                    -> you receive cart `id` + `buyLink`
                       Later edits: POST/PATCH/DELETE on
                                    /partner_storefront/carts/{cartId}/items
5. Buy now             Your "Buy now" button href = the latest `buyLink` from any cart response
6. Pay at Groupon      Shopper pays on Groupon's checkout page
7. Return              Groupon redirects the shopper to YOUR redirect URL
                        with ?grouponOrderUuid=<uuid>
8. Confirm             GET /partner_storefront/bookings/{bookingId}
                        -> status + one "View on Groupon" link per purchased unit
```

Each step has its guide: [Get your API key](/docs/get-your-api-key.md), [Syncing the
catalog](/docs/syncing-the-catalog.md), [Managing the cart](/docs/managing-the-cart.md), [Checkout and
order confirmation](/docs/checkout-and-order-confirmation.md), and [Handling
errors](/docs/handling-errors.md) for every failure. Every endpoint and field is in the [API
documentation](/docs/for-builders/api).

## Calling the API

Every path is relative to `https://api-core.livingsocial.com`. Call the API from your server only, with your key as a
Bearer token:

```bash
curl -s "https://api-core.livingsocial.com/partner_storefront/products?country=US&limit=10" \
  -H "Authorization: Bearer $GROUPON_API_KEY" \
  -H "x-request-id: $(uuidgen)"
```

| Header | When | Value |
|---|---|---|
| Authorization | Always | `Bearer` followed by your API key |
| Content-Type | POST, PATCH | `application/json` |
| x-request-id | Recommended | A UUID you make per request. An error echoes it as `details.requestId`: quote it to support. |

Money is an integer in minor units: `2500` with `currencyPrecision: 2` is $25.00.

## Checkout, and earning commission with CJ

Buy now is the `buyLink` from the latest cart response, verbatim. Never build the checkout URL
yourself.

Earning commission is optional and changes no code. Once your CJ (Commission Junction) publisher
ID is on file (see [Get your API key](/docs/get-your-api-key.md)), the same `buyLink` becomes a CJ
tracking link that leads to the same checkout, and commission is tracked and paid through CJ,
per the [CJ program terms](https://public.cj.com/signup/publisher?advertiserId=5840172).

On the Partner API, the shopper must reach checkout through your `buyLink`; any other route earns
nothing. This is the Partner API's rule; the open agent API has no commission.

---

# Get your API key (AI builders)

This guide is for builders, and the coding assistants they use, who need a key for the LivingSocial
Partner API.

You get the key yourself on this site: no email, no approval. The open agent API needs no key and
earns no commission: see [LivingSocial for agents](/docs/livingsocial-for-agents.md).

## The flow

1. **Sign in with Google** on this site. An email and password works too.
2. **Register a storefront** under **Account** (`/account/storefronts/new`), with the fields
   below. The storefront is your registration with Groupon.
3. **Create its API key** on the storefront's page: give it a name and, optionally, the day it
   stops working.
4. **Copy the key once**, into your server's secret store. It is shown only at creation.
5. **Check it.** This call answers the registration the key acts as:

   ```bash
   curl -s "https://api-core.livingsocial.com/partner_storefront/me" \
     -H "Authorization: Bearer $GROUPON_API_KEY"
   ```

6. **To earn commission**, email req.partner.api@groupon.com your storefront's partner id (on
   the storefront page) and your CJ publisher ID. It is optional and your code does not change.
   Commission is per the [CJ program terms](https://public.cj.com/signup/publisher?advertiserId=5840172).

Next: [Syncing the catalog](/docs/syncing-the-catalog.md).

## Storefront fields

| Field | Required | Rules |
|---|---|---|
| Display name | Yes | At most 60 characters, and no "Groupon" or "LivingSocial" in any spelling. Shown to shoppers on Groupon's checkout page. |
| Contact name | Yes | Up to 255 characters. |
| Contact email | Yes | A monitored address. Groupon uses it to reach you. |
| Logo URL | No | Absolute `https://` URL. Groupon's checkout page shows the logo only when the address is on Groupon's own image CDN (`*.grouponcdn.com`); a logo on your own host is stored but does not render there. |
| Redirect URL | No | Absolute `https://` URL, with no username or password in it. After checkout, Groupon sends the shopper here with `grouponOrderUuid` added as a query parameter. |
| Inventory scope: states | No | Full US state names, exactly as listed below. Leave empty for every state. |
| Inventory scope: category1 | No | Groupon category permalinks. Leave empty for every category. |
| Inventory scope: category2 | No | Groupon subcategory permalinks. Leave empty for every category. |

The products list only returns offers inside your inventory scope, and a filter that names a state
or category outside it is rejected.

**States** (50 values; fields `inventoryStates`, `state`)

Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware, Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana, Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana, Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina, North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina, South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia, Wisconsin, Wyoming

**Top-level category** (1 values; fields `category0`)

local

**Categories** (38 values; fields `category1`)

air-inclusive, all-inclusive, auto-and-home-improvement, automotive, baby-kids-and-toys, beach-destinations, beauty-and-spas, casinos, city, cruises, culinary, electronics, entertainment-and-media, family-trips, food-and-drink, for-the-home, gift-cards, grocery-and-household, health-and-beauty, health-and-fitness, home-improvement, hotel-travel, jewelry-and-watches, luxury, mens-clothing-shoes-and-accessories, outdoor-activities-recreation, personal-services, pet-supplies, retail, romantic, spa-and-wellness, sports-and-outdoors, things-to-do, toys, unique-lodging, v1-personalized-items, waterparks, womens-clothing-shoes-and-accessories

**Subcategories** (207 values; fields `category2`)

africa-and-middle-east, alcohol, apparel-local, appliances-goods, art, art-and-home-decor, arts-and-crafts, arts-and-entertainment-local, australia-and-nz, auto-cleaning, auto-parts-and-accessories, auto-repair, baby-gear, baby-toys, babys-fashion, bars, bath, bath-and-body, bath-and-potty, bedding, bird-supplies, blow-outs-and-styling, books-music-and-movies, breweries-wineries-and-distilleries, brow-and-lash, building-sets, cafes-and-treats, camera-video-and-surveillance, camping, camping-trips, car-electronics-and-gps, cat-supplies, cbd, cell-phones-and-accessories, charity, child-car-seats, childrens-books, childrens-jewelry, classes, cleaning-services, clothing-and-shoes, collectibles, computers-and-tablets, consultants, contractors, cosmetic-procedures, cosmetics, custom-baby-and-kids-items, custom-household-essentials, custom-jewelry, custom-kitchen-accessories, custom-novelty-items, custom-photo-prints, dental, diamond-jewelry, dog-supplies, dolls-and-action-figures, drinks, educational-toys, electrical, electronic-toys, electronics-local, electronics-repair, europe, event-rental-services, exercise-and-fitness, fan-shop, fashion-jewelry, fine-metal-jewelry, fire-pits-and-outdoor-heaters, fitness-classes, floor-care-and-cleaning, flooring, flowers-sweets-and-gift-baskets, food, fragrances, fun-and-leisure-activities, furniture, furniture-stores, games-and-puzzles, gemstone-and-pearl-jewelry, golf-goods, groceries-and-markets, gyms, hair-removal, hair-salons, hand-and-power-tools, health-and-beauty-local, health-and-safety, health-care, heating-and-cooling, home-and-garden-local, home-improvement-goods, home-repairs, hvac-and-electrical, interior-design, jewelry-accessories-and-storage, kids-activities, kids-bikes-and-ride-on-toys, kids-local, kitchen-and-dining, lab-grown-diamond-jewelry, lawn-and-garden, local-services, luggage, magazines, makeup, massage, massage-and-relaxation, maternity-clothes, medical, men, mens-accessories, mens-clothing, mens-jewelry, mens-shoes, mountains, movies-and-tv, musical-instruments, nail-salons, natural-medicine, nightlife, office-and-school-supplies, outdoor-decor, outdoor-grills-and-accessories, outdoors, parking, parks, party-supplies, patio-and-garden, patio-and-garden-products, personalized-bags, personalized-clothing-and-accessories, personalized-items, personalized-pets-items, personalized-stationery, pets, photo-book, photography, plumbing, plus-size-womens-clothing, portable-audio, pretend-play, recreation, remodeling, restaurants, rv-camper, salons, seasonal-decor, sightseeing-and-tours, ski-resorts, skin-care, small-animal-supplies, smart-home, software, spa, spa-getaways, sports, sports-and-outdoor-activities, sports-local, strollers, subscriptions, tanning, team-sports, television-and-home-theater, tickets-and-events, tobacco, toddler-and-baby-toys, toddler-and-kids-fashion, transportation, usa, v-books, v-food, v-heating-and-cooling, v-office-and-school-supplies, v-outdoor-power-equipment, v1-aromatherapy, v1-baby-feeding, v1-dental-care, v1-diapers, v1-gaming, v1-hair-care, v1-home-automation, v1-lighting, v1-maternity-clothes, v1-mattresses, v1-nursery, v1-outdoor-garden-decor, v1-outdoor-play, v1-personal-care, v1-personalized-home-decor, v1-sexual-health, v1-shaving, v1-skin-care, v1-storage-and-organization, v2-cycling, v2-gaming, v3-vitamins-and-supplements, vision, watches, wearable-technology, weight-loss, weight-loss-v2, womens-accessories, womens-clothing, womens-intimates, womens-shoes

## Limits

- At most 5 open storefronts per person, and 3 new ones per 24 hours.
- At most 10 active API keys per storefront.
- A key works for at most 90 days, and for 90 days when you set no day. The key list shows the
  date. An expired key answers HTTP 401, like a revoked one.
- A key is `grpn_`, 8 hex characters, `_`, then 56 hex characters. It acts as its own storefront
  only, on the endpoints of the [API documentation](/docs/for-builders/api): it cannot change the
  registration or manage keys.
- Call the API from your server only. Never put the key in a browser, an app, a log or a
  repository.

## Rotating a key

Create the new key, deploy it, then revoke the old one. There is no downtime, and nobody to
email.

## Changing or closing a storefront

You change the display name, contact, logo, redirect URL and inventory scope yourself on the
storefront's page; its keys keep working.

Closing a storefront is permanent: its catalog and carts stop answering, every one of its keys is
revoked, and it no longer counts towards your five.

---

# Syncing the catalog

This guide is for builders of a website or app on the Partner API.

Keep your own copy of the offers in your inventory scope and render your site from it, never live
from Groupon. Every field of a product is in the [API documentation](/docs/for-builders/api).

## What to list

- List a product only when its `status` is `active` and `availabilityRequired` is false, and an
  option only when its `active` is not false (`null` means unknown: treat it as sellable). A
  product with `availabilityRequired: true` needs a booked time slot and cannot be added to a cart.
- `option.price.retail` is the per-unit price the shopper pays: send it as `expectedPrice` on
  cart calls. `option.price.original` is the strike-through price.
- `option.promotion` is informational: the shopper enters `promotion.promoCode` on Groupon's
  checkout page. Cart totals always use retail.
- Pick a picture from `media` by its `role` (`small`, `medium`, `large`, `extra_large`,
  `wide_crop`), falling back to the largest `width`. Not every size exists on every offer.

## Keeping your catalog fresh

| Run | When | What it reads |
|---|---|---|
| Full sync | First, once | Every product in your scope |
| Daily resync | Every day: the minimum | Every product again, then stop listing what the walk did not return |
| Delta refresh | Every two hours: optional | Only what changed since your last refresh (`updatedSince`) |

A delta alone is not enough. It cannot show you an offer that left your scope, and when a
promotion starts Groupon ignores `updatedSince` and returns the whole catalog, so handle a delta
of any size.

One walk serves all three runs:

```text
seen = empty set                                     # resync only
params = { country: "US", limit: 100 }               # delta: add updatedSince: lastRefreshAt
cursor = none
repeat:
    page = GET https://api-core.livingsocial.com/partner_storefront/products?params (+ cursor if set)
    upsert every product in page.data by its id      # replace options, prices, status
    add every id in page.data to seen                # resync only
    cursor = page.nextCursor
until page.hasMore == false
stop listing every product whose id is not in seen   # resync only; keep the row
lastRefreshAt = page.timestamp                       # the final page's timestamp
```

## Rules for every walk

- Send the same parameters on every page; only `cursor` changes. Keep paging until `hasMore` is
  false: a page can be short, or empty, while more follow.
- A page holds at most 100 products (`limit`), and 10 when you send none.
- Save `lastRefreshAt` and stop listing missing products only after a whole walk succeeds. If a
  walk fails part-way, change nothing and run it again. `lastRefreshAt` is the final page's
  `timestamp`, Groupon's clock, never your own.
- Never send `active` on a delta: you need the deactivated rows to stop listing them.
- Run one walk at a time: skip a delta that falls due during a resync.
- If a page answers `BAD_REQUEST` for its cursor, discard the cursor and restart the walk.
- Your database is a cache. The cart re-checks price and availability live.

---

# Managing the cart

This guide is for builders of a website or app on the Partner API.

Groupon keeps a mirror of your shopper's cart, and that mirror is the one that gets paid for. A
cart is created together with its first item.

## Operations

| Operation | Endpoint | When |
|---|---|---|
| Create cart | `POST /partner_storefront/carts` | Shopper adds the first item |
| Read cart | `GET /partner_storefront/carts/{cartId}` | Your cart page loads, or before Buy now after a long idle |
| Add items | `POST /partner_storefront/carts/{cartId}/items` | Shopper adds another item |
| Change quantity | `PATCH /partner_storefront/carts/{cartId}/items/{itemId}` | Shopper changes a quantity |
| Remove item | `DELETE /partner_storefront/carts/{cartId}/items/{itemId}` | Shopper removes a line |
| Abandon cart | `DELETE /partner_storefront/carts/{cartId}` | Shopper empties the cart, or the session ends |

Every operation except Abandon returns the full cart with a fresh `buyLink`: overwrite your local
cart with each response. A cart holds at most 20 different options, and 1 to 100 of each. Store the
cart id against the shopper's session on your server.

## Creating a cart

```bash
curl -s -X POST "https://api-core.livingsocial.com/partner_storefront/carts" \
  -H "Authorization: Bearer $GROUPON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "US",
    "items": [{
      "productId": "0b6f7a52-3c1d-4e8a-9f20-5d7c1a2b3c4d",
      "optionId":  "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "quantity": 2,
      "expectedPrice": 4500
    }]
  }'
```

`expectedPrice` is the per-unit retail price your site shows. When Groupon's live price differs,
the call fails with `PRICE_MISMATCH` and the live price in `details.currentPrice`: update your
price, ask the shopper to confirm, and send it again.

## Reading the response

- `buyLink`: your Buy now button's href, verbatim, from the latest response. Open it in the
  shopper's browser. Never fetch it from your server and never rewrite it.
- `available: false`: the line cannot be bought. Show `unavailableReason` and remove the line
  before checkout.
- `messages`: `price_unavailable` means a line's live price could not be resolved, so the line is
  left out of the totals. `item_declined` means Groupon declined an item although the call
  succeeded: it is not in `items`, so tell the shopper.
- `totals.grandTotal` excludes tax and any promo code. The final amount is on Groupon's checkout
  page.

## Retries, and a cart that is gone

- Adding items is not idempotent: a line for the same option grows by `quantity`. On a timeout,
  read the cart and add only what is missing.
- Discard the `cartId` after you abandon the cart and after an order is placed from it. Adding to
  it then starts a new, empty cart under the same id.
- The cart is gone when one you expect to hold items reads `itemCount: 0`, when a change to a line
  answers `INVALID_PRODUCT_ID`, or when any call answers `INVALID_CART_ID`. Create a new cart from
  your local copy and use its `buyLink`.

---

# Checkout and order confirmation

This guide is for builders of a website or app on the Partner API.

Before showing Buy now, read the cart and check every line is available. The button links to
`buyLink`.

## The return redirect

With a redirect URL on your storefront, Groupon sends the shopper's browser back to it after
checkout, with the order id added:

```text
https://your-site.example/checkout/complete?grouponOrderUuid=5d0c2f3a-8b1e-4c7d-a9f0-1e2d3c4b5a69
```

Without one, your site receives no `grouponOrderUuid`. Your handler for that address:

1. Reads `grouponOrderUuid` and checks it is a UUID. It handles each one once: a repeat visit
   shows the result you saved.
2. Reads the order from your server (below).
3. Matches the order's items to the cart in the shopper's session, on `optionId`. With no
   session or no match, it shows a neutral "check your Groupon email" page and marks nothing as
   purchased.
4. Keeps the `cartId` and a snapshot of its lines until the status is final. On success it marks
   the cart as purchased. On `REJECTED` or `EXPIRED` it reads the cart and, if it is gone, creates
   a new one from the snapshot.
5. Renders the confirmation page.

Treat `grouponOrderUuid` as sensitive: keep it out of analytics and off public pages.

## Reading the order

```bash
curl -s "https://api-core.livingsocial.com/partner_storefront/bookings/5d0c2f3a-8b1e-4c7d-a9f0-1e2d3c4b5a69" \
  -H "Authorization: Bearer $GROUPON_API_KEY"
```

An order from a two-line cart, its second line still being processed:

```json
{
  "id": "5d0c2f3a-8b1e-4c7d-a9f0-1e2d3c4b5a69",
  "status": "ON_HOLD",
  "supplierReference": "LG-ABCD-1234-EFGH",
  "createdAt": "2026-09-18T12:10:44Z",
  "items": [
    {
      "productId": "0b6f7a52-3c1d-4e8a-9f20-5d7c1a2b3c4d",
      "optionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "quantity": 2,
      "status": "CONFIRMED",
      "unitItems": [
        { "id": "a1b2c3d4-0000-4000-8000-000000000001", "status": "CONFIRMED",
          "myGrouponUrl": "https://www.groupon.com/mygroupons/users/<purchaserId>/details/a1b2c3d4-0000-4000-8000-000000000001?inventory_service=vis" },
        { "id": "a1b2c3d4-0000-4000-8000-000000000002", "status": "CONFIRMED",
          "myGrouponUrl": "https://www.groupon.com/mygroupons/users/<purchaserId>/details/a1b2c3d4-0000-4000-8000-000000000002?inventory_service=vis" }
      ]
    },
    {
      "productId": "1c7e8b63-4d2e-4f9b-a031-6e8d2b3c4d5e",
      "optionId": "8d0f7780-8536-41ef-a55f-f18fd2a01bf8",
      "quantity": 1,
      "status": "ON_HOLD",
      "unitItems": [
        { "id": "a1b2c3d4-0000-4000-8000-000000000003", "status": "ON_HOLD",
          "myGrouponUrl": "https://www.groupon.com/mygroupons/users/<purchaserId>/details/a1b2c3d4-0000-4000-8000-000000000003?inventory_service=vis" }
      ]
    }
  ]
}
```

## What to show

| Status | What to show |
|---|---|
| ON_HOLD, PENDING | "Finalizing your order..." and poll again |
| CONFIRMED | Success page |
| REDEEMED | Success page, marked as used |
| REJECTED | "Payment failed" and a link back to the cart |
| EXPIRED | "This order was not completed" and a link back to the cart |
| CANCELLED | "This order was cancelled." |

The same statuses apply to the order, to each item and to each purchased unit. Treat a status you
do not recognise as unresolved: show the order as not completed, keep the cart, and start no new
purchase because of it.

## Polling

Right after the redirect the status is often `ON_HOLD` or `PENDING`. Poll from your server after
2, 4, 8, 15 and 30 seconds, and stop as soon as it is neither. If it still is after the last try,
show "Your order is being processed, you will receive a confirmation email from Groupon" with the
purchase buttons you have.

## The purchase buttons

Show one button per entry of an item's `unitItems`, linking to that entry's `myGrouponUrl`. The
shopper signs in to Groupon to open it. A `CANCELLED` unit gets no button.

---

# Handling errors

This guide is for builders of a website or app on the Partner API.

Every endpoint answers an error in one shape. Branch on `details.error`; `code` is the general
class of the error.

```json
{
  "code": "aborted",
  "message": "Displayed price no longer valid",
  "details": {
    "error": "PRICE_MISMATCH",
    "errorMessage": "Displayed price no longer valid",
    "requestId": "c0ffee00-...",
    "path": "$.items[0]",
    "expectedPrice": 4500,
    "currentPrice": 4900,
    "currency": "USD",
    "currencyPrecision": 2
  }
}
```

## Error codes

| details.error | HTTP | What to do |
|---|---|---|
| BAD_REQUEST | 400 | A field is missing or invalid (`errorMessage` names it), or a cursor is bad. Fix the request, or restart the walk. |
| FORBIDDEN | 403 | Outside your inventory scope, or the storefront is closed. Do not retry. |
| INVALID_PRODUCT_ID | 404 | The product is gone: stop listing it. Or the cart has no such line: read the cart. |
| INVALID_CART_ID | 404 | Not a cart of your storefront. Create a new cart from your local copy. |
| INVALID_BOOKING_UUID | 404 | No such order placed from your carts. Show a neutral message. |
| PRODUCT_NOT_CARTABLE | 400 | The offer needs a booked time slot. Stop listing it. |
| CART_ITEM_LIMIT | 400 | More than 20 different options in a cart. |
| PRICE_MISMATCH | 409 | `expectedPrice` differs from the live price in `details.currentPrice`. Nothing was changed: update your price, ask the shopper to confirm, send it again. |
| RATE_LIMITED | 429 | More than 600 requests in a calendar minute, counted across the storefront's keys. Wait `details.retryAfterSeconds` when the answer carries it; there is no `Retry-After` header. |
| UPSTREAM_UNAVAILABLE | 503 | Groupon could not answer. Retry with backoff, except when adding cart items. |
| none | 401 | A missing, wrong, revoked or expired key; the message is the same for all four. Do not retry: alert an operator. |

## Retry policy

| Situation | Retry? |
|---|---|
| UPSTREAM_UNAVAILABLE, a network error, a timeout, or a 5xx with no Groupon code | Yes, with exponential backoff, at most 5 tries |
| RATE_LIMITED | Yes, after `details.retryAfterSeconds`; without it, back off |
| A failure while adding items to a cart | No. Read the cart and add only what is missing |
| A failure while creating a cart | Yes. A retry creates a separate new cart: use the id of the call that succeeded |
| Reading a cart or an order | Yes. A read changes nothing |
| Every other code | No |
