> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zupy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Coupon Lifecycle

> Browse rewards, redeem coupons, list customer coupons, and validate (use) coupons via the Zupy API

Coupons are the core reward delivery mechanism in Zupy. When a customer redeems a reward, a coupon is created with a unique code (`CZ-XXXXXXXX`). The partner can then list, display, and validate (use) that coupon at the point of sale.

<Note>
  **Prerequisites**: Your API key (`zupy_pk_*`) and familiarity with the [Authentication](/authentication) page. If your integration requires OTP for redemption or coupon usage, see the [OTP Flow](/guides/otp-flow) guide first.
</Note>

## Coupon Lifecycle Overview

Every coupon follows this flow:

```
(optional) Request + Verify OTP  →  X-OTP-Session token
                                       ↓
Browse Rewards → Redeem Reward → Coupon Created (CZ-XXXXXXXX)
                                       ↓
                  List Customer Coupons (active/used/expired)
                                       ↓
                  Validate Coupon (atomically mark as used)
```

<Steps>
  <Step title="(If required) Get an OTP session">
    Most companies enable `require_otp_for_redemption` and `require_otp_for_coupon_usage` by default — without a valid `X-OTP-Session` header, the redeem and validate calls below return `403 otp-required`. See [OTP Flow](/guides/otp-flow) for the full request → verify dance.
  </Step>

  <Step title="Browse Available Rewards">
    Query the reward catalog to see what the customer can redeem.
  </Step>

  <Step title="Redeem Reward">
    Create a coupon by redeeming a reward for a specific customer. Returns the coupon's KSUID `id` **and** the customer-facing `CZ-` code.
  </Step>

  <Step title="List Coupons">
    View the customer's coupons, filtered by status. Use this to find the KSUID `id` you'll need for validation.
  </Step>

  <Step title="Validate (Use) Coupon">
    Atomically mark the coupon as used at the point of sale. Pass the **KSUID `id`** in the path — not the `CZ-` code.
  </Step>
</Steps>

### Two identifiers, one coupon

Every coupon carries two identifiers:

| Identifier    | Format                                     | Where it comes from                                                                                            |
| ------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `id`          | KSUID (e.g. `16a33f27fbbc1801d63d56d2027`) | Returned by `GET /coupons/issued/` and `POST /rewards/{id}/redeem/`. Stable, opaque, never shown to customers. |
| `coupon_code` | `CZ-XXXXXXXX` (e.g. `CZ-914F15F3`)         | Customer-facing — printed on receipts, shown in wallet passes, spoken by the cashier.                          |

**Both work as the `{coupon_id}` path parameter on the validate endpoint** (case-insensitive on the `CZ-`/`CP-` form). When a customer hands you a receipt at the POS, you can validate directly with the printed code — no extra lookup needed.

To go from `CZ-` code → full coupon record (for example, to check status before validating), use the dedicated lookup:

```
GET /api/v2/coupons/issued/?code=CZ-914F15F3
```

Returns the matching coupon (or an empty `data` array if not found) with its `id`, `customer_id`, `status`, `valid_until`, etc.

## Step 1: Browse Available Rewards

Query the reward catalog to see what rewards are available for a customer. The `customer_id` parameter enriches each reward with the customer's redemption status.

```
GET /api/v2/rewards/?customer_id={id}
```

**Key query parameters:**

| Parameter     | Type    | Description                                                       |
| ------------- | ------- | ----------------------------------------------------------------- |
| `customer_id` | string  | Customer KSUID — enriches response with `already_redeemed` flag   |
| `redeemable`  | boolean | If `true`, only returns rewards the customer can currently afford |

**Response fields:**

| Field              | Type    | Description                                        |
| ------------------ | ------- | -------------------------------------------------- |
| `id`               | string  | Reward KSUID (use in redeem URL)                   |
| `name`             | string  | Reward display name                                |
| `points_required`  | integer | Points needed to redeem                            |
| `is_available`     | boolean | Whether the reward is currently active             |
| `already_redeemed` | boolean | Whether this customer already redeemed this reward |
| `validity_days`    | integer | Days the coupon remains valid after redemption     |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.zupy.com/api/v2/rewards/?customer_id=2awTHloSJX7kGGprFerOOsvABcd" \
    -H "X-API-Key: zupy_pk_your_api_key_here"
  ```

  ```python Python theme={null}
  import requests

  BASE_URL = "https://api.zupy.com/api/v2"
  HEADERS = {"X-API-Key": "zupy_pk_your_api_key_here"}

  # All rewards with customer context
  response = requests.get(
      f"{BASE_URL}/rewards/",
      params={"customer_id": "2awTHloSJX7kGGprFerOOsvABcd"},
      headers=HEADERS,
  )
  rewards = response.json()["data"]

  # Only rewards the customer can afford
  response = requests.get(
      f"{BASE_URL}/rewards/",
      params={"customer_id": "2awTHloSJX7kGGprFerOOsvABcd", "redeemable": True},
      headers=HEADERS,
  )
  affordable_rewards = response.json()["data"]
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.zupy.com/api/v2";
  const API_KEY = "zupy_pk_your_api_key_here";

  // All rewards with customer context
  let response = await fetch(
    `${BASE_URL}/rewards/?customer_id=2awTHloSJX7kGGprFerOOsvABcd`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const { data: rewards } = await response.json();

  // Only rewards the customer can afford
  response = await fetch(
    `${BASE_URL}/rewards/?customer_id=2awTHloSJX7kGGprFerOOsvABcd&redeemable=true`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const { data: affordableRewards } = await response.json();
  ```
</CodeGroup>

**Example response:**

```json theme={null}
{
  "data": [
    {
      "id": "2bxRKmpWJY8lHHqsGfsQQtwCDef",
      "name": "Free Dessert",
      "points_required": 200,
      "is_available": true,
      "already_redeemed": false,
      "validity_days": 30
    },
    {
      "id": "2bxRKnqXKZ9mIIrtHgtRRuxEFgh",
      "name": "10% Off Next Order",
      "points_required": 500,
      "is_available": true,
      "already_redeemed": true,
      "validity_days": 15
    }
  ],
  "meta": {}
}
```

## Step 2: Redeem Reward (Create Coupon)

Redeem a reward for a customer. This creates a coupon with a unique code and deducts points from the customer's balance.

```
POST /api/v2/customers/{id}/rewards/{reward_id}/redeem/
```

**Request body:**

| Field          | Type    | Default | Description                                                                             |
| -------------- | ------- | ------- | --------------------------------------------------------------------------------------- |
| `use_z_tokens` | boolean | `false` | If `true`, Z\$ tokens cover any points gap. See [Z\$ Tokens guide](/guides/zupy-tokens) |

**Response fields:**

| Field           | Type    | Description                                                                                                                                                                             |
| --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customer_id`   | string  | Customer KSUID                                                                                                                                                                          |
| `coupon_code`   | string  | Unique coupon code (format: `CZ-XXXXXXXX`). Display this to the customer for in-store redemption.                                                                                       |
| `points_used`   | integer | Points deducted from the customer's balance (`0` for marketing rewards)                                                                                                                 |
| `z_tokens_used` | string  | Z\$ tokens consumed by this redemption — `"0.000000"` when `use_z_tokens=false` or the reward is purely points-based                                                                    |
| `new_balance`   | integer | Customer's remaining **points** balance after the redemption                                                                                                                            |
| `new_z_balance` | string  | Customer's **current Z\$** balance after any token consumption. Always returned (not conditional on `use_z_tokens`) — partners use it to surface the live balance back to the customer. |
| `valid_until`   | string  | ISO 8601 expiration date (server-computed from `reward.validity_days`)                                                                                                                  |
| `created_at`    | string  | ISO 8601 coupon creation timestamp                                                                                                                                                      |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.zupy.com/api/v2/customers/2awTHloSJX7kGGprFerOOsvABcd/rewards/2bxRKmpWJY8lHHqsGfsQQtwCDef/redeem/" \
    -H "X-API-Key: zupy_pk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{"use_z_tokens": false}'
  ```

  ```python Python theme={null}
  customer_id = "2awTHloSJX7kGGprFerOOsvABcd"
  reward_id = "2bxRKmpWJY8lHHqsGfsQQtwCDef"

  response = requests.post(
      f"{BASE_URL}/customers/{customer_id}/rewards/{reward_id}/redeem/",
      json={"use_z_tokens": False},
      headers=HEADERS,
  )
  coupon = response.json()["data"]
  print(f"Coupon code: {coupon['coupon_code']}")
  print(f"Valid until: {coupon['valid_until']}")
  ```

  ```javascript JavaScript theme={null}
  const customerId = "2awTHloSJX7kGGprFerOOsvABcd";
  const rewardId = "2bxRKmpWJY8lHHqsGfsQQtwCDef";

  const response = await fetch(
    `${BASE_URL}/customers/${customerId}/rewards/${rewardId}/redeem/`,
    {
      method: "POST",
      headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ use_z_tokens: false }),
    }
  );
  const { data: coupon } = await response.json();
  console.log(`Coupon code: ${coupon.coupon_code}`);
  console.log(`Valid until: ${coupon.valid_until}`);
  ```
</CodeGroup>

**Example response:**

```json theme={null}
{
  "data": {
    "customer_id": "2awTHloSJX7kGGprFerOOsvABcd",
    "coupon_code": "CZ-A1B2C3D4",
    "points_used": 200,
    "z_tokens_used": "0.000000",
    "new_balance": 350,
    "new_z_balance": "2418.400000",
    "valid_until": "2026-04-25T00:00:00Z",
    "created_at": "2026-03-25T14:30:00Z"
  },
  "meta": {}
}
```

<Tip>
  If your integration's OTP policy requires verification for redemption, include the `X-OTP-Session` header. See the [OTP Flow](/guides/otp-flow) guide.
</Tip>

### Redemption Errors

| Status | Type               | When                                                                                                                                                                 |
| ------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `otp-required`     | OTP policy is on but `X-OTP-Session` was not provided. See [OTP Flow](/guides/otp-flow).                                                                             |
| 404    | `not-found`        | Customer or reward not found in your company                                                                                                                         |
| 422    | `validation-error` | Customer doesn't have enough points — `detail` field explains which constraint failed                                                                                |
| 422    | `validation-error` | Marketing reward already redeemed by this customer (free rewards have a 1-per-customer limit). `errors[].detail` = `"Prêmio gratuito já resgatado por este cliente"` |

## Step 3: List Customer's Coupons

Retrieve all coupons for a customer, optionally filtered by status.

```
GET /api/v2/coupons/issued/?customer_id={id}
```

**Key query parameters:**

| Parameter     | Type   | Description                                                                                           |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `customer_id` | string | Filter to one customer (KSUID). Omit to list all issued coupons for the company                       |
| `status`      | string | Filter by status: `active`, `used`, `expired`, `cancelled`                                            |
| `origin_type` | string | Filter by provenance: `loyalty_reward` (redeemed with points) or `marketing` (claimed without points) |
| `reward_id`   | string | Filter to coupons issued from one reward                                                              |
| `code`        | string | Exact `coupon_code` lookup (case-insensitive)                                                         |

**Response fields:**

| Field                  | Type    | Description                                                                            |
| ---------------------- | ------- | -------------------------------------------------------------------------------------- |
| `id`                   | string  | Coupon KSUID (use in validate URL)                                                     |
| `coupon_code`          | string  | Unique coupon code (`CZ-XXXXXXXX`)                                                     |
| `status`               | string  | `active`, `used`, `expired`, or `cancelled`                                            |
| `origin_type`          | string  | `loyalty_reward` (redeemed by spending points) or `marketing` (claimed without points) |
| `usage_date`           | string  | ISO 8601 timestamp when the coupon was used/validated (`null` while still active)      |
| `remaining_usages`     | integer | How many times the coupon can still be used                                            |
| `total_usages_allowed` | integer | Total uses allowed (1 for single-use coupons)                                          |
| `valid_until`          | string  | ISO 8601 expiration date                                                               |
| `reward_name`          | string  | Name of the redeemed reward                                                            |

<CodeGroup>
  ```bash cURL theme={null}
  # All coupons for a customer
  curl -X GET "https://api.zupy.com/api/v2/coupons/issued/?customer_id=2awTHloSJX7kGGprFerOOsvABcd" \
    -H "X-API-Key: zupy_pk_your_api_key_here"

  # Only active coupons
  curl -X GET "https://api.zupy.com/api/v2/coupons/issued/?customer_id=2awTHloSJX7kGGprFerOOsvABcd&status=active" \
    -H "X-API-Key: zupy_pk_your_api_key_here"
  ```

  ```python Python theme={null}
  customer_id = "2awTHloSJX7kGGprFerOOsvABcd"

  # All coupons
  response = requests.get(
      f"{BASE_URL}/coupons/issued/",
      params={"customer_id": customer_id},
      headers=HEADERS,
  )
  all_coupons = response.json()["data"]

  # Only active coupons
  response = requests.get(
      f"{BASE_URL}/coupons/issued/",
      params={"customer_id": customer_id, "status": "active"},
      headers=HEADERS,
  )
  active_coupons = response.json()["data"]
  print(f"{len(active_coupons)} active coupons")
  ```

  ```javascript JavaScript theme={null}
  const customerId = "2awTHloSJX7kGGprFerOOsvABcd";

  // All coupons
  let response = await fetch(
    `${BASE_URL}/coupons/issued/?customer_id=${customerId}`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const { data: allCoupons } = await response.json();

  // Only active coupons
  response = await fetch(
    `${BASE_URL}/coupons/issued/?customer_id=${customerId}&status=active`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const { data: activeCoupons } = await response.json();
  console.log(`${activeCoupons.length} active coupons`);
  ```
</CodeGroup>

**Example response:**

```json theme={null}
{
  "data": [
    {
      "id": "2cxSLnqYLZ0nJJsuIhuSSvyFGij",
      "coupon_code": "CZ-A1B2C3D4",
      "status": "active",
      "origin_type": "loyalty_reward",
      "usage_date": null,
      "remaining_usages": 1,
      "total_usages_allowed": 1,
      "valid_until": "2026-04-25T00:00:00Z",
      "reward_name": "Free Dessert"
    },
    {
      "id": "2cxSLoRZMA1oKKtvJivTTwzGHjk",
      "coupon_code": "CZ-E5F6G7H8",
      "status": "used",
      "origin_type": "marketing",
      "usage_date": "2026-04-18T14:32:00Z",
      "remaining_usages": 0,
      "total_usages_allowed": 1,
      "valid_until": "2026-04-20T00:00:00Z",
      "reward_name": "10% Off Next Order"
    }
  ],
  "meta": {}
}
```

## Step 4: Validate (Use) Coupon

Validate a coupon to mark it as used. This is an **atomic operation** — concurrent validate calls are race-safe (uses database-level locking).

```
POST /api/v2/customers/{id}/coupons/{coupon_id}/validate/
```

<Tip>
  **POS ergonomics**: `{coupon_id}` accepts **both** identifier formats — pass whichever you have:

  * **KSUID `id`** (e.g. `16a33f27fbbc1801d63d56d2027`) — returned by `GET /coupons/issued/`
  * **`coupon_code`** (e.g. `CZ-914F15F3` or `CP-LEGACY01`) — printed on the customer's receipt or wallet pass

  Lookup is case-insensitive on the code path, so `cz-914f15f3` resolves the same as `CZ-914F15F3`. The response always echoes the canonical upper-case form.

  Use the `CZ-` code when a cashier types it from a receipt; use the KSUID when you already obtained it from a previous `redeem` or `coupons/issued/` call.
</Tip>

Send an **empty POST body** (no JSON needed).

**Response fields:**

| Field                  | Type    | Description                                                                                                                                                                                                              |
| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `customer_id`          | string  | Customer KSUID (matches the `{id}` in the URL path).                                                                                                                                                                     |
| `coupon_code`          | string  | The validated coupon code.                                                                                                                                                                                               |
| `status`               | string  | `"validated"` — confirms the coupon was successfully used.                                                                                                                                                               |
| `remaining_usages`     | integer | Remaining uses after validation.                                                                                                                                                                                         |
| `total_usages_allowed` | integer | Total uses allowed for this coupon.                                                                                                                                                                                      |
| `validated_at`         | string  | ISO 8601 timestamp of validation.                                                                                                                                                                                        |
| `new_balance`          | integer | Customer's points balance after validation. Validate itself debits 0 pts (points were spent at redemption time); this is the current balance, returned so partners don't need a follow-up `GET /customers/{id}/points/`. |
| `new_z_balance`        | string  | Customer's current Z\$ balance after validation (stringified Decimal with 6 places — same format as `RewardRedeemResponse`).                                                                                             |

<Warning>
  This is an **atomic** operation. The backend uses `select_for_update()` to prevent race conditions. If two concurrent validate calls hit the same coupon, only one will succeed — the other returns `409 Conflict`.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.zupy.com/api/v2/customers/2awTHloSJX7kGGprFerOOsvABcd/coupons/2cxSLnqYLZ0nJJsuIhuSSvyFGij/validate/" \
    -H "X-API-Key: zupy_pk_your_api_key_here"
  ```

  ```python Python theme={null}
  customer_id = "2awTHloSJX7kGGprFerOOsvABcd"
  coupon_id = "2cxSLnqYLZ0nJJsuIhuSSvyFGij"

  response = requests.post(
      f"{BASE_URL}/customers/{customer_id}/coupons/{coupon_id}/validate/",
      headers=HEADERS,
  )
  result = response.json()["data"]
  print(f"Coupon {result['coupon_code']} validated at {result['validated_at']}")
  print(f"Remaining uses: {result['remaining_usages']}")
  ```

  ```javascript JavaScript theme={null}
  const customerId = "2awTHloSJX7kGGprFerOOsvABcd";
  const couponId = "2cxSLnqYLZ0nJJsuIhuSSvyFGij";

  const response = await fetch(
    `${BASE_URL}/customers/${customerId}/coupons/${couponId}/validate/`,
    {
      method: "POST",
      headers: { "X-API-Key": API_KEY },
    }
  );
  const { data: result } = await response.json();
  console.log(`Coupon ${result.coupon_code} validated at ${result.validated_at}`);
  console.log(`Remaining uses: ${result.remaining_usages}`);
  ```
</CodeGroup>

**Example response:**

```json theme={null}
{
  "data": {
    "customer_id": "2awTHloSJX7kGGprFerOOsvABcd",
    "coupon_code": "CZ-A1B2C3D4",
    "status": "validated",
    "remaining_usages": 0,
    "total_usages_allowed": 1,
    "validated_at": "2026-03-25T14:30:00Z",
    "new_balance": 350,
    "new_z_balance": "2418.400000"
  },
  "meta": {}
}
```

<Tip>
  If your integration's OTP policy requires verification for coupon usage, include the `X-OTP-Session` header. See the [OTP Flow](/guides/otp-flow) guide.
</Tip>

### Validation Errors

| Status | Type           | When                                                                                                                                                                                                                 |
| ------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `otp-required` | OTP verification required but `X-OTP-Session` not provided. See [OTP Flow](/guides/otp-flow).                                                                                                                        |
| 404    | `not-found`    | Coupon not found under this customer. Confirm: (a) the coupon belongs to the `customer_id` in the path, (b) the `CZ-`/`CP-` code (if you used one) is spelled correctly.                                             |
| 409    | `conflict`     | Coupon already fully consumed. `detail`: `"Cupom já Used"` (or `"Expired"` / `"Cancelled"`). Re-validating an already-used coupon is **safe and idempotent** — it returns 409, never double-applies the side effect. |

### POS Quick Lookup — validate by receipt code

When a customer hands you a printed coupon code at the point of sale, you have two equivalent ways to validate it. **Use whichever fits your flow.**

<CodeGroup>
  ```bash One-shot validate theme={null}
  # Pass the CZ-code directly in the path — no GET needed.
  curl -X POST "https://api.zupy.com/api/v2/customers/{customer_id}/coupons/CZ-914F15F3/validate/" \
    -H "X-API-Key: zupy_pk_..." \
    -H "X-OTP-Session: ..."
  ```

  ```bash Look up first, then validate theme={null}
  # Useful when you need to confirm status / customer / expiry before validating.
  curl "https://api.zupy.com/api/v2/coupons/issued/?code=CZ-914F15F3" \
    -H "X-API-Key: zupy_pk_..."
  # → { "data": [ { "id": "16a33f27...", "customer_id": "...", "status": "active", ... } ] }

  curl -X POST "https://api.zupy.com/api/v2/customers/{customer_id}/coupons/16a33f27.../validate/" \
    -H "X-API-Key: zupy_pk_..." \
    -H "X-OTP-Session: ..."
  ```
</CodeGroup>

<Note>
  If you don't know the `customer_id` ahead of time (cashier scanning a printed code from a customer they don't recognize), the **lookup-first** path tells you both `customer_id` and `status` so you can decide whether to proceed and which OTP session to attach.
</Note>

## Multi-Use Coupons

Some rewards create coupons that can be used more than once. The `remaining_usages` and `total_usages_allowed` fields track usage:

| Coupon Type    | `total_usages_allowed` | Behavior                                                                                                        |
| -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Single-use** | 1                      | After one validate call, `remaining_usages` becomes `0` and status becomes `used`                               |
| **Multi-use**  | 2+                     | Each validate call decrements `remaining_usages`. Status stays `active` until the last use, then becomes `used` |

<Accordion title="Multi-use coupon example">
  A "3x Free Coffee" reward creates a coupon with `total_usages_allowed: 3`:

  ```json theme={null}
  // After redemption
  { "remaining_usages": 3, "total_usages_allowed": 3, "status": "active" }

  // After 1st validate
  { "remaining_usages": 2, "total_usages_allowed": 3, "status": "active" }

  // After 2nd validate
  { "remaining_usages": 1, "total_usages_allowed": 3, "status": "active" }

  // After 3rd validate (final use)
  { "remaining_usages": 0, "total_usages_allowed": 3, "status": "used" }
  ```
</Accordion>

## Coupon Expiration

Coupons have a `valid_until` date set at redemption time, calculated as:

```
valid_until = redemption_time + reward.validity_days
```

* **Auto-expires**: a validate call on an expired coupon returns `409 Conflict`
* **Check before displaying**: partners should compare `valid_until` with the current time before showing a coupon to the customer
* **Expired status**: coupons past their `valid_until` date will show `status: "expired"` in list responses

<Note>
  Expired coupons cannot be reactivated. The customer must redeem the reward again to get a new coupon (if the reward allows re-redemption).
</Note>

## Reward & Coupon Stats

Aggregate how many coupons were issued (rewards redeemed) and how many were used —
company-wide or per reward.

```
GET /api/v2/coupons/issued/stats/
```

**Query parameters** (same filters as the list):

| Parameter     | Description                                       |
| ------------- | ------------------------------------------------- |
| `reward_id`   | **Per-reward stats** — issued/used for one reward |
| `origin_type` | `loyalty_reward` or `marketing`                   |
| `customer_id` | Restrict to one customer                          |
| `code`        | Exact coupon code                                 |

A `status` filter is ignored here so the breakdown always spans every status.

<CodeGroup>
  ```bash cURL theme={null}
  # Company-wide
  curl -X GET "https://api.zupy.com/api/v2/coupons/issued/stats/" \
    -H "X-API-Key: zupy_pk_your_api_key_here"

  # Per reward
  curl -X GET "https://api.zupy.com/api/v2/coupons/issued/stats/?reward_id=2cx...reward" \
    -H "X-API-Key: zupy_pk_your_api_key_here"
  ```
</CodeGroup>

```json Response theme={null}
{
  "data": {
    "total_issued": 120,
    "used": 80,
    "usage_rate": 66.67,
    "by_status": { "active": 30, "used": 80, "expired": 8, "cancelled": 2 },
    "by_origin": { "loyalty_reward": 90, "marketing": 30 }
  },
  "meta": {}
}
```

| Field          | Description                                 |
| -------------- | ------------------------------------------- |
| `total_issued` | Rewards redeemed = coupons issued           |
| `used`         | Coupons validated/consumed                  |
| `usage_rate`   | `used / total_issued * 100`                 |
| `by_status`    | Count per status                            |
| `by_origin`    | Count per provenance (loyalty vs marketing) |

## Full Example: Complete Coupon Flow

End-to-end flow validated in production 2026-05-26: request OTP → verify → browse rewards → redeem → list coupons → validate. Skip the OTP block if your integration's policy disables `require_otp_for_redemption` and `require_otp_for_coupon_usage`.

<CodeGroup>
  ```python Python theme={null}
  import requests

  BASE_URL = "https://api.zupy.com/api/v2"
  HEADERS = {"X-API-Key": "zupy_pk_your_api_key_here"}
  customer_id = "2awTHloSJX7kGGprFerOOsvABcd"
  customer_phone = "+5511987654321"

  # ── Step 0 (when OTP is required): get a trusted session ──
  requests.post(
      f"{BASE_URL}/auth/request-otp/",
      json={"identifier": customer_phone},
      headers=HEADERS,
  )
  # Customer receives a 6-digit code via WhatsApp. Collect it from them, then:
  otp_code = input("OTP from customer: ")
  verify = requests.post(
      f"{BASE_URL}/auth/verify-otp/",
      json={"identifier": customer_phone, "otp_code": otp_code},
      headers=HEADERS,
  ).json()["data"]
  otp_session = verify["otp_session"]
  AUTHED = {**HEADERS, "X-OTP-Session": otp_session}

  # ── Step 1: Browse rewards the customer can afford ──
  response = requests.get(
      f"{BASE_URL}/rewards/",
      params={"customer_id": customer_id, "redeemable": True},
      headers=HEADERS,
  )
  rewards = response.json()["data"]
  print(f"Found {len(rewards)} redeemable rewards")

  # ── Step 2: Redeem the first available reward ──
  reward_id = rewards[0]["id"]
  response = requests.post(
      f"{BASE_URL}/customers/{customer_id}/rewards/{reward_id}/redeem/",
      json={"use_z_tokens": False},
      headers=AUTHED,  # uses the OTP session
  )
  coupon = response.json()["data"]
  print(f"Coupon created: {coupon['coupon_code']}, valid until {coupon['valid_until']}")
  print(f"Customer balance: {coupon['new_balance']} pts, {coupon['new_z_balance']} Z$")

  # ── Step 3: List active coupons (returns the KSUID 'id' field) ──
  response = requests.get(
      f"{BASE_URL}/coupons/issued/",
      params={"customer_id": customer_id, "status": "active"},
      headers=HEADERS,
  )
  active_coupons = response.json()["data"]
  print(f"{len(active_coupons)} active coupons")

  # ── Step 4: Validate the coupon at the point of sale ──
  # IMPORTANT: pass the KSUID 'id' field, NOT the CZ-XXX coupon_code.
  coupon_ksuid = active_coupons[0]["id"]
  response = requests.post(
      f"{BASE_URL}/customers/{customer_id}/coupons/{coupon_ksuid}/validate/",
      headers=AUTHED,
  )
  result = response.json()["data"]
  print(f"Coupon {result['coupon_code']} used at {result['validated_at']}")
  print(f"Remaining uses: {result['remaining_usages']} / {result['total_usages_allowed']}")
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.zupy.com/api/v2";
  const API_KEY = "zupy_pk_your_api_key_here";
  const customerId = "2awTHloSJX7kGGprFerOOsvABcd";
  const customerPhone = "+5511987654321";

  const HEADERS = { "X-API-Key": API_KEY, "Content-Type": "application/json" };

  // ── Step 0 (when OTP is required): get a trusted session ──
  await fetch(`${BASE_URL}/auth/request-otp/`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ identifier: customerPhone }),
  });
  // Customer receives a 6-digit code via WhatsApp. Collect it, then:
  const otpCode = prompt("OTP from customer:");
  const verifyRes = await fetch(`${BASE_URL}/auth/verify-otp/`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ identifier: customerPhone, otp_code: otpCode }),
  });
  const { data: verify } = await verifyRes.json();
  const AUTHED = { ...HEADERS, "X-OTP-Session": verify.otp_session };

  // ── Step 1: Browse rewards the customer can afford ──
  let response = await fetch(
    `${BASE_URL}/rewards/?customer_id=${customerId}&redeemable=true`,
    { headers: HEADERS }
  );
  const { data: rewards } = await response.json();
  console.log(`Found ${rewards.length} redeemable rewards`);

  // ── Step 2: Redeem the first available reward ──
  const rewardId = rewards[0].id;
  response = await fetch(
    `${BASE_URL}/customers/${customerId}/rewards/${rewardId}/redeem/`,
    {
      method: "POST",
      headers: AUTHED,  // uses the OTP session
      body: JSON.stringify({ use_z_tokens: false }),
    }
  );
  const { data: coupon } = await response.json();
  console.log(`Coupon created: ${coupon.coupon_code}, valid until ${coupon.valid_until}`);
  console.log(`Balance: ${coupon.new_balance} pts, ${coupon.new_z_balance} Z$`);

  // ── Step 3: List active coupons (returns the KSUID 'id' field) ──
  response = await fetch(
    `${BASE_URL}/coupons/issued/?customer_id=${customerId}&status=active`,
    { headers: HEADERS }
  );
  const { data: activeCoupons } = await response.json();
  console.log(`${activeCoupons.length} active coupons`);

  // ── Step 4: Validate the coupon at the point of sale ──
  // IMPORTANT: pass the KSUID 'id' field, NOT the CZ-XXX coupon_code.
  const couponKsuid = activeCoupons[0].id;
  response = await fetch(
    `${BASE_URL}/customers/${customerId}/coupons/${couponKsuid}/validate/`,
    { method: "POST", headers: AUTHED }
  );
  const { data: result } = await response.json();
  console.log(`Coupon ${result.coupon_code} used at ${result.validated_at}`);
  console.log(`Remaining: ${result.remaining_usages} / ${result.total_usages_allowed}`);
  ```
</CodeGroup>

## Next Steps

<Card title="Z$ Tokens" icon="coins" href="/guides/zupy-tokens">
  Learn how Z\$ tokens work and how they can cover points gaps in reward redemption
</Card>

<Card title="OTP Flow" icon="lock" href="/guides/otp-flow">
  Set up customer identity verification for sensitive operations
</Card>

<Card title="API Reference" icon="code" href="/api-reference">
  Browse all endpoints with request/response schemas
</Card>
