---
title: "The last remaining use of a capped coupon was taken."
method: POST
path: "coupon.exhausted"
tags: ["coupon.* events"]
---

# The last remaining use of a capped coupon was taken.

`POST coupon.exhausted` (webhook)

### When this fires

A settled redemption took the **last remaining use** of a capped coupon. Fires immediately after the `coupon.redeemed` that consumed it, in the same emission.

Use it to close a campaign automatically: pull the code out of a landing page, stop an ad, post "sold out", or notify the creator that the cap they set has been reached.

> **Only capped coupons can fire this.** A coupon with `max_redemptions: null` is unlimited and never emits this event.

### Exhaustion counts live reservations, so it can un-exhaust

This is the one behaviour that surprises people. The quota is enforced against settled redemptions **plus reservations whose hold has not expired**, so a coupon can report exhausted while the last few uses are still only held by checkouts in progress.

If one of those checkouts is abandoned, the hold expires and its use returns to the pool. The coupon becomes redeemable again, and **no event fires for that**, because nothing changed on the coupon row.

That design is deliberate: it means an abandoned checkout never permanently burns a use, and no sweeper job is needed to reclaim one. The consequence for you is that `coupon.exhausted` is a strong signal but not a permanent state. If your automation does something hard to undo (pausing an ad spend, emailing a list) verify the current state with [the coupon endpoint](https://docs.subscriby.net/api/v1/reference/coupons) first, and read `redemptions.remaining` rather than comparing counts yourself.

> **`redemptions_count` can exceed `max_redemptions`.** The counter is bumped when a checkout takes a hold, not when it settles, and is only decremented when a hold is explicitly released. Lapsed holds the stale-reservation sweep has not yet reclaimed still sit in it, so on this event `redemptions_count` is equal to or greater than `max_redemptions`, never below it. Read `redemptions.remaining` from the API if you need the reservation-aware figure.

### Caveats

- **It can fire more than once for the same coupon.** Exhausted → a hold expires → redeemable → exhausted again is a legitimate sequence. De-dup on the event `id`, not on the coupon id.
- **A creator raising `max_redemptions` does not emit a matching "un-exhausted" event.** That is a `coupon.updated` with `max_redemptions` in its `changes`.
- **Expiry is a different thing entirely.** A coupon that runs past `expires_at` stops applying without ever being exhausted, and emits nothing.

### Related events

- `coupon.redeemed`: the redemption that consumed the last use; always precedes this.
- `coupon.updated`: where a raised cap shows up.
- `coupon.deactivated`: switched off by decision rather than run out by use.

## Headers

- `SB-Signature` string, required
- `SB-Event-Id` string, required
- `SB-Event-Name` string, required
- `Content-Type` string, required
- `User-Agent` string, required

## Payload

- object: The envelope every event is delivered in.
  - `id` string, required: A ULID unique to the event, prefixed `evt_`. Every retry carries the same id, so it is the key to deduplicate on.
  - `type` 'coupon.exhausted', required: The event name, always `coupon.exhausted` here.
  - `created_at` string, date-time, required: When the event was emitted, server time, ISO 8601.
  - `api_version` string, required: The contract version of the payload shape, `2026-05-01` today. A breaking change to a shape ships under a new version.
  - `project_id` string, uuid, nullable, required: The project the event belongs to; null for team-scoped and billing events.
  - `data` object, required: The event-specific payload. Keys that do not apply to a given emission are omitted rather than sent as null, so check for presence.
    - `coupon_id` string, uuid, required: The coupon that ran out.
    - `code` string, required: The code as stored: uppercase.
    - `max_redemptions` integer, required: The cap that was reached. Never `null` on this event.
    - `redemptions_count` integer, nullable, required: Settled redemptions plus reservations not yet released: the denormalised counter, bumped at hold time, not at settlement. Read fresh at emission.

## Acknowledgement `2XX`

Your endpoint acknowledged the delivery. Any 2xx status within 30 seconds marks it delivered; the response body is ignored.

## Other responses

- `default`: Any other status, a connection failure, or no answer within 30 seconds counts as a failed attempt. The delivery is retried 8 times, after 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 3 days; the last failure dead-letters it, and it can be retried from the dashboard or `POST /v1/webhook-deliveries/{delivery}/retry`. After 20 consecutive failures the endpoint is paused until it is resumed.

---

[API](https://skmtc.dev/memberpass/apis/subscriby-rest-api.md) · [All operations](https://skmtc.dev/memberpass/apis/subscriby-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/memberpass/apis/subscriby-rest-api/revisions/59c4df91d764?raw)
