> ## 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.

# Balance Operations

> Adjust, hold, release, and forfeit balances directly

While rules handle most balance changes automatically, you can also modify balances directly via the API for customer service, corrections, and manual workflows.

## Adjust

Credit or debit a participant's balance:

```bash theme={null}
POST /v1/participants/{id}/balances/adjust
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "type": "CREDIT",
  "amount": "500",
  "description": "Customer service goodwill credit"
}
```

| Field            | Required | Description                                                                                                              |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `program_id`     | Yes      | Program context                                                                                                          |
| `asset_id`       | Yes      | Which asset to adjust                                                                                                    |
| `type`           | Yes      | `CREDIT` or `DEBIT`                                                                                                      |
| `amount`         | Yes      | Positive amount                                                                                                          |
| `allow_negative` | No       | `DEBIT` only. When `true`, allows the debit to overdraw the balance below zero. Used for clawbacks. Defaults to `false`. |
| `bucket`         | No       | `AVAILABLE` (default) or `HELD`                                                                                          |
| `description`    | Yes      | Reason for the adjustment (1-500 characters)                                                                             |

Amounts must be positive. Use `type` to control direction. `DEBIT` fails if the available balance is insufficient, unless `allow_negative` is set to `true`. If `amount` has more decimal places than the asset's `scale`, the request is rejected with a 400 (`invalid_scale`). Round in your client before calling adjust.

### Negative Balances

Set `allow_negative` to `true` on a `DEBIT` to let the balance go below zero. This is useful when you need to recover value that a participant has already spent:

* **Refund clawbacks.** A cardholder earned cashback on a purchase that was later refunded. The cashback has already been redeemed, so the available balance is zero. A negative-balance debit records the debt.
* **Chargeback recovery.** A payment is disputed and reversed, but the associated reward points were already used. The debit brings the balance negative until the participant earns enough to offset it.
* **Corrections.** An incorrect credit was issued and the participant has already spent part of it. A negative-balance debit corrects the ledger without waiting for funds to replenish.

```bash theme={null}
POST /v1/participants/{id}/balances/adjust
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "type": "DEBIT",
  "amount": "25",
  "allow_negative": true,
  "description": "Clawback: refund on order #4821"
}
```

For [`LOT`-mode assets](/guides/lots-and-expiration), the system consumes whatever lots are available first, then posts the remaining amount as an overdraft. The balance goes negative by the uncovered portion.

`allow_negative` also works in [rule actions](/guides/rule-actions#debit). This lets your rules engine handle clawbacks automatically, for example debiting cashback when a refund event arrives, even if the participant's balance is zero.

<Note>
  `allow_negative` only applies to adjustments and rule-based debits. Transfers and redemptions always require sufficient funds.
</Note>

## Hold

Reserve funds by moving them from `AVAILABLE` to `HELD`:

```bash theme={null}
POST /v1/participants/{id}/balances/hold
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "amount": "500",
  "reference_id": "auth_78945",
  "description": "Auth hold - txn 78945"
}
```

| Field          | Required | Description                                                                                                                                        |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `program_id`   | Yes      | Program context                                                                                                                                    |
| `asset_id`     | Yes      | Which asset to hold                                                                                                                                |
| `amount`       | No       | Positive amount to reserve. If omitted, the full `AVAILABLE` balance (`SIMPLE` mode) or all matching lots (`LOT` mode) are held.                   |
| `reference_id` | No       | Correlation ID for this hold (`LOT` mode only). Stamps held lots so a future release can target them. 1-255 characters, alphanumeric plus `._:@-`. |
| `description`  | Yes      | Reason for the hold (1-500 characters)                                                                                                             |

Held funds are not spendable. Use holds for:

* Authorization holds (reserve rewards until the transaction settles)
* Fraud review (freeze funds pending investigation)
* Pending approvals (hold until manual review completes)

## Release

Move held funds back to `AVAILABLE`:

```bash theme={null}
POST /v1/participants/{id}/balances/release
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "reference_id": "auth_78945",
  "description": "Settlement confirmed"
}
```

| Field          | Required | Description                                                                            |
| -------------- | -------- | -------------------------------------------------------------------------------------- |
| `program_id`   | Yes      | Program context                                                                        |
| `asset_id`     | Yes      | Which asset to release                                                                 |
| `amount`       | No       | Amount to release. If omitted, all matching held lots are released.                    |
| `reference_id` | No       | Release only lots stamped with this reference during a previous hold (`LOT` mode only) |
| `description`  | Yes      | Reason for the release (1-500 characters)                                              |

When `reference_id` is provided, only lots stamped with that reference during a previous hold are targeted. You can also filter by lot age using `earned_from` and `earned_to` (RFC 3339 timestamps) for batch-releasing held balances. `LOT` mode only.

## Forfeit

Remove funds permanently from a participant's balance:

```bash theme={null}
POST /v1/participants/{id}/balances/forfeit
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "amount": "100",
  "bucket": "AVAILABLE",
  "description": "Expired points"
}
```

| Field         | Required | Description                                                                                                           |
| ------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `program_id`  | Yes      | Program context                                                                                                       |
| `asset_id`    | Yes      | Which asset to forfeit                                                                                                |
| `amount`      | No       | Positive amount. If omitted, the full bucket balance (`SIMPLE` mode) or all matching lots (`LOT` mode) are forfeited. |
| `bucket`      | Yes      | `AVAILABLE` or `HELD`. Specifies which balance bucket to forfeit from.                                                |
| `description` | Yes      | Reason for the forfeit (1-500 characters)                                                                             |

Forfeited funds move to the `SYSTEM_BREAKAGE` account.

## Void Hold

Cancel HELD lots that were credited directly into HELD (e.g., pending authorization rewards) by `reference_id`, returning value to the original source account:

```bash theme={null}
POST /v1/participants/{id}/balances/void-hold
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "reference_id": "auth_12345",
  "description": "Auth reversal"
}
```

| Field          | Required | Description                                         |
| -------------- | -------- | --------------------------------------------------- |
| `program_id`   | Yes      | Program context                                     |
| `asset_id`     | Yes      | Which asset to void (`LOT` mode only)               |
| `reference_id` | Yes      | Correlation ID matching the original CREDIT to HELD |
| `description`  | Yes      | Reason for the void (1-500 characters)              |

Void hold is the correct operation for auth reversals. It cancels lots that were credited directly into the HELD bucket and returns value to the source (program wallet for `PREFUNDED` assets, `SYSTEM_ISSUANCE` for `UNLIMITED`).

<Note>
  Void hold only processes lots that were created directly in HELD via CREDIT. Lots moved to HELD via a HOLD operation (participant funds) are excluded for safety: returning those to the source would effectively confiscate participant value. Use [release](#release) to return participant-held funds to AVAILABLE.
</Note>

`amount`, `lot_ids`, `earned_from`, and `earned_to` are not supported. The entire provisional accrual matching the `reference_id` is voided.

## Auth / Settlement Pattern

Card transactions follow a two-step lifecycle: the issuer-processor authorizes the transaction first, and the merchant settles (captures) it later, sometimes hours or days after. The settlement amount can differ from the authorization due to tips, partial captures, or currency conversion.

Most programs can skip authorizations and credit rewards on settlement only. This is the simplest approach, and the [Stripe Issuing](/examples/stripe-issuing) example uses it. But if your program needs to show pending rewards to cardholders in real time, before the transaction settles, you can use the auth/settle pattern to hold provisional rewards at authorization and reconcile them when the merchant captures.

This is the dual-message pattern common in card processing: both the authorization and settlement events flow into Scrip, and the system handles the difference between them automatically.

<Note>
  This pattern requires a `LOT`-mode asset. `reference_id` stamps individual lots at authorization so they can be matched at settlement. For `SIMPLE`-mode assets, use amount-based holds and releases without `reference_id`.
</Note>

<Steps>
  <Step title="Authorization: hold provisional rewards">
    When a cardholder taps their card, your issuer-processor fires an authorization webhook. Your backend forwards it as an event with `event.type == "auth"`. A rule credits points into the `HELD` bucket, stamping the lots with the authorization ID:

    ```json theme={null}
    {"type": "CREDIT", "asset_id": "...", "amount": "${{ event.amount }}", "bucket": "HELD", "reference_id": "${{ event.authorization_id }}"}
    ```

    The cardholder's balance now shows pending rewards in the `held` field. These are not spendable.

    Alternatively, if the participant already has an `AVAILABLE` balance and you want to reserve existing points rather than mint new ones:

    ```json theme={null}
    {"type": "HOLD", "asset_id": "...", "amount": "${{ event.amount }}", "reference_id": "${{ event.authorization_id }}"}
    ```
  </Step>

  <Step title="Settlement: reconcile and finalize">
    When the merchant captures the transaction, your backend sends a second event with `event.type == "settlement"`. Two approaches:

    **Auto-reconcile with CREDIT.** Credit to `AVAILABLE` with the same `reference_id`:

    ```json theme={null}
    {"type": "CREDIT", "asset_id": "...", "amount": "${{ event.settlement_amount }}", "reference_id": "${{ event.authorization_id }}"}
    ```

    The system consumes the matching held lots and reconciles any difference between the auth and settlement amounts; see [How auto-reconciliation works](#how-auto-reconciliation-works) below.

    **Simple release.** If the settlement amount always matches the authorization, a release is sufficient. This moves the held lots back to `AVAILABLE` without reconciliation:

    ```json theme={null}
    {"type": "RELEASE", "asset_id": "...", "reference_id": "${{ event.authorization_id }}"}
    ```

    When `amount` is omitted with a `reference_id`, all lots held under that reference are released.
  </Step>

  <Step title="Or: void the authorization">
    If the authorization is reversed before settling, use `VOID_HOLD` to cancel the provisional rewards. This returns value to the source account (program wallet or system issuance):

    ```json theme={null}
    {"type": "VOID_HOLD", "asset_id": "...", "reference_id": "${{ event.authorization_id }}"}
    ```

    For authorizations that expire without settling, set `expires_at` on the original CREDIT (e.g., `"720h"`), which automatically forfeits uncaptured holds after the expiration window.

    See the [Stripe Issuing](/examples/stripe-issuing#uncaptured-authorizations) example for details on handling uncaptured authorizations.
  </Step>
</Steps>

### How auto-reconciliation works

When a `CREDIT` to `AVAILABLE` includes a `reference_id` that matches existing `HELD` lots, the system reconciles automatically:

| Settlement vs. auth                                       | What happens                                                   |
| --------------------------------------------------------- | -------------------------------------------------------------- |
| Equal                                                     | Held lots consumed, same amount credited to `AVAILABLE`        |
| Settlement > auth (over-capture, e.g., tip added)         | Extra amount sourced from program wallet or system issuance    |
| Settlement \< auth (under-capture, e.g., partial capture) | Excess returned to program wallet or system issuance           |
| No held lots found for the `reference_id`                 | Standard credit applied with `reference_id` stamped on the lot |

### When to use this pattern

| Scenario                                                                    | Approach                                                                                             |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Show pending rewards at authorization, reconcile at settlement              | Auth/settle pattern (this section)                                                                   |
| Credit rewards only after settlement                                        | Credit on settlement directly, with no holds needed. See [Stripe Issuing](/examples/stripe-issuing). |
| Reserve existing points for a pending operation (e.g., redemption approval) | [Hold](#hold) and [release](#release) without `reference_id`                                         |

## Idempotency

All balance operations accept an idempotency key so retries never double-apply. Pass it either as an `idempotency_key` field in the body or as the standard `Idempotency-Key` HTTP header. The two are equivalent. If both are provided they must match; mismatched values return a `400` (`header_body_mismatch`) rather than silently picking one. Header values must be 1-255 printable ASCII characters.

Replaying the same key with identical parameters returns the original result without creating new postings. The same key with different parameters returns `409` (`idempotency_conflict`). See [Idempotency](/api-reference/introduction#idempotency) in the API reference for the full contract.

```bash theme={null}
POST /v1/participants/{id}/balances/adjust
Idempotency-Key: goodwill-credit-case-8841
{
  "program_id": "program-uuid",
  "asset_id": "asset-uuid",
  "type": "CREDIT",
  "amount": "500",
  "description": "Customer service goodwill credit"
}
```

## Lot Preservation

For `LOT`-mode assets, hold and release preserve each lot's metadata and identity across bucket transitions. See [Lot-Aware Operations](/guides/lots-and-expiration#lot-aware-operations) for the full behavior.

## Inactive Participants

Most balance operations (adjust, hold, release) are blocked for `SUSPENDED` and `CLOSED` participants. The API returns a 409 error with code `participant_inactive`.

Forfeit and void hold are the exceptions: they are allowed on `CLOSED` participants so you can clean up remaining balances after account closure. Both are still blocked for `SUSPENDED` participants.
