# Handling errors

Every endpoint uses one error shape. The HTTP status is meaningful: `code` is the general class
of error, and `details.error` is the specific Groupon code to branch your logic on.

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

## One helper for every endpoint

```ts
function grouponErrorCode(body: any): string {
  return body?.details?.error ?? body?.code ?? "UNKNOWN";
}

function grouponRequestId(body: any): string | undefined {
  return body?.details?.requestId;
}
```

## Error codes

| details.error | HTTP | code | Meaning / what to do |
|---|---|---|---|
| BAD_REQUEST | 400 | invalid_argument | A field is missing or invalid, or a cursor is bad. `errorMessage` names the field. Fix the request; for a cursor error, restart the walk. |
| FORBIDDEN | 403 | permission_denied | Outside your inventory scope, or the storefront is deactivated. Do not retry. |
| INVALID_PRODUCT_ID | 404 | not_found | The product is gone, or the cart has no such line. For a product: mark it not listable. For a cart line: read the cart, and if it comes back empty, create a new one. |
| INVALID_CART_ID | 404 | not_found | No such cart for your storefront: it never existed, or another storefront created it. Do not retry: create a new cart from your local copy. |
| INVALID_BOOKING_UUID | 404 | not_found | No such order placed from your carts. Do not retry. Show a neutral message. |
| PRODUCT_NOT_CARTABLE | 400 | failed_precondition | The deal needs a time-slot booking. Remove it from your listing. |
| CART_ITEM_LIMIT | 400 | failed_precondition | More than 20 different options in a cart. `details` carries `limit` and `actual`. |
| PRICE_MISMATCH | 409 | aborted | `expectedPrice` differs from the live price. `details.currentPrice` is the live per-unit price; `details.path` points at the item. Nothing was changed: update your price, ask the shopper to confirm, retry. |
| INVALID_BOOKING_STATE, UNPROCESSABLE_ENTITY | 400 / 409 | failed_precondition | Reserved. Treat as a non-retryable failure. |
| RATE_LIMITED | 429 | resource_exhausted | Too many requests for this storefront right now. Our own limit is 600 requests per storefront per minute, counted by the calendar minute across all of the storefront's keys. `details.retryAfterSeconds` says how many seconds are left until it starts again. |
| UPSTREAM_UNAVAILABLE | 503 | unavailable | Groupon could not answer right now. Retry with backoff, except adding cart items (see the rule below). |

`HTTP 401` with `code: "unauthenticated"` means a missing, wrong, revoked or expired key, on every
endpoint that takes one. The message is the same whatever the reason, so it never tells anybody
whether a key exists. Do not retry; alert an operator.

## Retry policy

Decide whether to retry from the error code first, not the HTTP status alone.

| Situation | Retry? |
|---|---|
| UPSTREAM_UNAVAILABLE, a network error, a timeout, or a 5xx with no Groupon error code | Yes. Exponential backoff, max 5 tries. Exception: adding cart items, below. |
| RATE_LIMITED (429) | Yes, but wait at least `details.retryAfterSeconds` seconds first. The answer carries no `Retry-After` header today. When `retryAfterSeconds` is missing (Groupon's own limit, passed on), back off exponentially. |
| Any retryable failure on adding items to a cart | No. Read the cart, compare, then add only what is missing. |
| Any retryable failure on creating a cart | Yes. A retry creates a separate new cart, which is harmless. Use the id from the call that succeeded. |
| BAD_REQUEST with a cursor problem on the products list | Not the same page. Discard the cursor and restart the walk from page 1. See [Syncing the catalog](/docs/syncing-the-catalog.md). |
| BAD_REQUEST, FORBIDDEN, INVALID_PRODUCT_ID, PRODUCT_NOT_CARTABLE, PRICE_MISMATCH, CART_ITEM_LIMIT, INVALID_BOOKING_UUID, INVALID_BOOKING_STATE, UNPROCESSABLE_ENTITY, INVALID_CART_ID | No. Handle the code as described above. |
| HTTP 401 (authentication failure) | No. Alert an operator: check the key was copied correctly, or mint a new one. |

A booking read never changes the order, so it is always safe to retry.
