# Checkout and order confirmation

## Sending the shopper to checkout

Before showing Buy now, read the cart and confirm every line is available.

Link the button to `buyLink`. It opens Groupon's checkout page for that cart. If you have added
CJ, the shopper passes through CJ first; this is what credits your commission. The page shows
your logo if you gave one (see [Get your API key](/docs/get-your-api-key.md)).

Keep the `cartId` in the shopper's server-side session so you can tie the returning shopper to
the cart they bought.

## The return redirect

If you gave no redirect URL, the shopper ends on Groupon's receipt page and gets Groupon's
confirmation email, and your site does not receive a `grouponOrderUuid`.

If you gave a redirect URL, Groupon redirects the shopper's browser there after a successful
payment, with the order id added:

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

Your handler for that URL must:

1. Read `grouponOrderUuid` and check it looks like a UUID. Ignore any other parameter. Handle
   each `grouponOrderUuid` once: a repeat visit shows the result you saved.
2. Call the booking endpoint from your server.
3. Match the order's items to the cart in the shopper's session. If there is no session or no
   match, show a neutral "check your Groupon email" page and do not mark any cart as purchased.
4. Keep the `cartId` and a snapshot of its lines until the order status is final. On success,
   mark the cart as purchased. On `REJECTED` or `EXPIRED`, read the cart; if it is gone, create a
   new one from the snapshot.
5. Render the confirmation page.

A shopper who closes the tab before the redirect still has a valid order: they receive Groupon's
confirmation email and can find the purchase in their Groupon account.

## Reading the order

```bash
curl -s "https://api-staging-core.livingsocial.com/partner_storefront/bookings/5d0c2f3a-8b1e-4c7d-a9f0-1e2d3c4b5a69" \
  -H "Authorization: Bearer $GROUPON_API_KEY" \
  -H "User-Agent: MyStore/1.0 (+https://mystore.example)"
```

The order's own facts sit at the top level. Everything about what was bought is under `items`:
one entry per product and option, in the order they were bought. An order placed from a cart has
one item per cart line, and every unit item belongs to exactly one item.

This example is an order from a two-line cart. The second line is still awaiting fulfilment, so
the whole order reads `ON_HOLD`:

```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" }
      ]
    }
  ]
}
```

The full field list is in the [API reference](/reference).

## Matching an order to your cart

`items[].productId` and `items[].optionId` are the same ids you sent as `productId` and
`optionId` on the cart lines, and the same as the ids from the products list. Join each order
item to your cart and your catalog on `optionId` without any translation. If `productId` is
`null`, the product has since left Groupon's catalog; `optionId` still identifies what was
bought.

## Status meaning

| Status | Meaning | What to show |
|---|---|---|
| ON_HOLD | No payment has been taken yet, or it was taken but the vouchers' state could not be read yet. Normal for the first seconds after the redirect. | "Finalizing your order..." and poll again. |
| CONFIRMED | Paid. Vouchers are issued. | Success page. |
| REDEEMED | Every voucher has been used at the merchant. | Success page, marked as used. |
| REJECTED | Payment failed. No order was completed. | "Payment failed" and a link back to the cart. |
| CANCELLED | The order was cancelled or refunded. | "This order was cancelled." |
| EXPIRED | The order was not paid for in time. | "This order was not completed" and a link back to the cart. |
| PENDING | Payment is being processed, or it has been taken and the vouchers are still being issued. | "Finalizing your order..." and poll again. |

The same statuses apply at three levels: the order (`status`), each item (`items[].status`) and
each voucher (`items[].unitItems[].status`). The order's status is never more advanced than its
least-advanced item: one item still `ON_HOLD` keeps the whole order `ON_HOLD`, and the order
reads `REDEEMED` only when every item does.

Cancelled items are set aside. An item is `CANCELLED` once all its vouchers are, and the order
once all its items are. An order with some items cancelled takes the status of the rest, and
each cancelled voucher reads `CANCELLED` in `unitItems`.

Treat any status you do not recognize as unresolved: show it as not completed, keep the cart,
and never start a new purchase because of it. This keeps your code working if a new status is
added.

## Polling

Right after the redirect the status is often `ON_HOLD` or `PENDING`. Poll from your server: wait
2 s, then 4 s, 8 s, 15 s, 30 s (about one minute in total). Stop as soon as the status is neither
`ON_HOLD` nor `PENDING`. If it is still `ON_HOLD` or `PENDING` after the last try, show "Your
order is being processed, you will receive a confirmation email from Groupon" together with the
voucher buttons below.

## The voucher buttons

Each item's `unitItems` has one entry per purchased unit of that item: an item with `quantity: 2`
has two entries. Render the order grouped by item. For each item, show the product and option
(joined to your catalog on `optionId`), then for each of its `unitItems` render a button such as
"View voucher 1 on Groupon" whose href is that entry's `myGrouponUrl`.

The shopper must log in to their Groupon account to open it. That page is where they view and
redeem the voucher.

An item can still be `ON_HOLD` or `PENDING` while others are `CONFIRMED`. Show the buttons you
have and a "still processing" note on that item.

A voucher whose status is `CANCELLED` was cancelled or refunded. Show it as cancelled, without a
voucher button.

Treat `grouponOrderUuid` as sensitive. Do not log it in analytics or expose it on public pages.

## Errors

See [Handling errors](/docs/handling-errors.md) for the full code table. On the booking endpoint,
`INVALID_BOOKING_UUID` means no order with that id, or the order was not placed from one of your
carts: do not retry, show a neutral message. A booking read never changes the order, so it is
always safe to retry `UPSTREAM_UNAVAILABLE` with the same backoff as polling.
