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

# Redemption lifecycle

> Authorize, capture, fail, and cancel redemptions with reserved participant value

Use an asynchronous redemption when fulfillment happens after you accept the request. Authorization moves the exact amount from the participant's `AVAILABLE` balance to `HELD` and returns a `PENDING` redemption. Your fulfillment service then completes, fails, or cancels that redemption.

Instant redemptions suit flows that can deliver immediately. Both modes use the same amount and catalog redemption endpoints.

## Choose a fulfillment mode

| Mode         | Create behavior                                      | Initial status | Next action                                            |
| ------------ | ---------------------------------------------------- | -------------- | ------------------------------------------------------ |
| Instant      | Debits `AVAILABLE` and credits the redemption target | `COMPLETED`    | Deliver the reward, then reverse if a refund is needed |
| Asynchronous | Moves `AVAILABLE` to `HELD` under a reservation      | `PENDING`      | Complete, fail, or cancel before `expires_at`          |

Set the default on the program:

```json theme={null}
{
  "fulfillment_mode": "ASYNC",
  "pending_redemption_ttl": "24h"
}
```

`fulfillment_mode` defaults to `INSTANT`. `pending_redemption_ttl` is optional and accepts a duration from 60 seconds through 365 days. A create request can override the mode with `capture`:

| `capture` | Behavior                             |
| --------- | ------------------------------------ |
| `true`    | Capture immediately                  |
| `false`   | Authorize and return `PENDING`       |
| Omitted   | Use the program's `fulfillment_mode` |

## Authorize a redemption

Set `capture` to `false` on an amount redemption:

```bash theme={null}
curl -X POST https://api.scrip.dev/v1/participants/{participant_id}/redemptions \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "program_id": "550e8400-e29b-41d4-a716-446655440000",
    "asset_id": "550e8400-e29b-41d4-a716-446655440001",
    "amount": "2500",
    "description": "Hotel transfer order 8472",
    "capture": false,
    "ttl_seconds": 86400,
    "idempotency_key": "hotel-transfer-8472"
  }'
```

Catalog item redemptions accept the same `capture`, `expires_at`, and `ttl_seconds` fields at `POST /v1/participants/{id}/redemptions/items`.

The response includes the lifecycle and ledger anchors:

```json theme={null}
{
  "id": "7f422b43-4ba2-41c7-b05d-e4e90b68e978",
  "status": "PENDING",
  "amount": "2500",
  "journal_entry_id": "657a0312-1aaa-4ba7-842b-edfbb39edf14",
  "reservation_id": "d74f90be-9788-4106-852a-e4c03dcc356d",
  "expires_at": "2026-07-28T15:00:00Z"
}
```

For a pending redemption, `journal_entry_id` identifies the authorization hold. `capture_journal_entry_id`, `captured_at`, and `resolved_at` appear after successful capture.

## Track catalog inventory

For a `UNIT_BASED` catalog reward, authorization increments both `committed_units` and `pending_units`. Capture moves the quantity from `pending_units` to `delivered_units` while `committed_units` stays unchanged. Failure, cancellation, and timeout decrement `pending_units` and `committed_units` without changing `delivered_units`.

Releasing pending inventory can return an `OUT_OF_STOCK` reward to `ACTIVE`. See [Rewards catalog](/guides/rewards-catalog#inventory-management) for the full inventory contract.

## Use the returned deadline

You can send one request deadline:

* `expires_at`: absolute RFC 3339 timestamp
* `ttl_seconds`: lifetime relative to authorization

The two fields are mutually exclusive. A request or program deadline must be at least 60 seconds and no more than 365 days. If the request and program both set a deadline, the earlier one wins. If neither sets one, Scrip uses 90 days.

For a `LOT` asset, the earliest selected lot expiration is a physical limit and can shorten that policy deadline. This shortening is valid. Always schedule fulfillment against the `expires_at` returned on the redemption instead of calculating a deadline from your request.

If the resulting deadline is less than 60 seconds away, authorization returns
`422 fulfillment_window_too_short`.

Capture is accepted only before the effective deadline. At or after the deadline, the complete endpoint returns `409 reservation_expired`, even if the timeout worker has not resolved the row yet.

## Resolve the redemption

### Complete

Complete only after your fulfillment provider confirms delivery:

```bash theme={null}
curl -X POST https://api.scrip.dev/v1/redemptions/{redemption_id}/complete \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hotel-transfer-8472-complete" \
  -d '{}'
```

Completion moves the authorized amount from `HELD` to the redemption target and changes the redemption to `COMPLETED`. Capture is full-only. The amount cannot be changed during completion.

An authorization can still complete while the participant is `SUSPENDED`
because the spend was already approved. A `CLOSED` participant cannot be
captured.

### Fail

Use failure when fulfillment was attempted and could not be delivered:

```bash theme={null}
curl -X POST https://api.scrip.dev/v1/redemptions/{redemption_id}/fail \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hotel-transfer-8472-fail" \
  -d '{"reason": "provider_rejected"}'
```

### Cancel

Use cancellation when your application or the participant stops fulfillment before delivery:

```bash theme={null}
curl -X POST https://api.scrip.dev/v1/redemptions/{redemption_id}/cancel \
  -H "Authorization: Bearer $SCRIP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hotel-transfer-8472-cancel" \
  -d '{"reason": "customer_cancelled"}'
```

Fail and cancel return live reserved value from `HELD` to `AVAILABLE`. They do not recreate LOT value that already expired.

## State transitions

```text theme={null}
PENDING ──complete──> COMPLETED
PENDING ──fail──────> FAILED
PENDING ──cancel────> CANCELLED

COMPLETED ──partial reverse──> PARTIALLY_REVERSED ──reverse remainder──> FULLY_REVERSED
COMPLETED ──full reverse──────────────────────────────────────────────> FULLY_REVERSED
```

| Status               | Meaning                                                        |
| -------------------- | -------------------------------------------------------------- |
| `PENDING`            | Authorized value is reserved in `HELD`                         |
| `COMPLETED`          | Value reached the redemption target                            |
| `FAILED`             | Fulfillment failed and live reserved value was released        |
| `CANCELLED`          | Fulfillment was cancelled and live reserved value was released |
| `PARTIALLY_REVERSED` | Some completed value was returned                              |
| `FULLY_REVERSED`     | All completed value was returned                               |

Only `COMPLETED` and `PARTIALLY_REVERSED` redemptions can be reversed. See [Redemptions](/guides/redemptions#reversals).

The first terminal request wins. Repeating the same outcome returns the stored redemption. A different terminal request returns `409 redemption_already_resolved`.

## Expiration and reserved lots

A reservation protects value from generic `RELEASE`, `FORFEIT`, and `VOID_HOLD` operations. It does not stop normal LOT expiration. Reserved lots keep their original expiration, maturity, and issuer lineage.

The timeout worker changes an overdue `PENDING` redemption to `FAILED` with `failure_reason: "expired"`. If all reserved LOT value already expired, no value returns to `AVAILABLE`. If part remains live, only that remainder is released.

| Inventory mode | Authorization                                                        | Capture                                 |
| -------------- | -------------------------------------------------------------------- | --------------------------------------- |
| `LOT`          | Selected lots move to `HELD` under an internal reservation reference | Only those referenced lots are consumed |
| `SIMPLE`       | A reservation claims part of the aggregate `HELD` balance            | The exact reserved amount is consumed   |

## Webhooks and balance mirrors

Authorization emits `redemption.pending` and `balance.held`. The pending webhook includes the `reservation_id` and effective `expires_at`. A fulfillment provider can identify the reservation and schedule capture without fetching the redemption first. Completion emits `redemption.completed`. Failure and timeout emit `redemption.failed` plus `balance.released` when live value is released. Cancellation emits `redemption.cancelled` plus `balance.released` when live value is released.

Successful capture does not emit a `balance.*` event. If you maintain a balance mirror from webhooks, reconcile it with the participant balance and statement endpoints so `HELD` does not remain overstated.

## Time windows

The participant redemption list filters `from` and `to` by creation time, which is the authorization time for a pending redemption. Reporting recognizes redeemed value when capture succeeds, using `captured_at`.

A redemption authorized in June and captured in July appears in a June-filtered redemption list and in July reporting. Use each surface for its stated purpose.
