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

# Staged Deduction

> When cost is unknown upfront, freeze funds first and settle the actual cost later.

Use the `freeze -> consume / unfreeze` flow to bill work that you price yourself, when the cost is unknown upfront. This is one half of Velobase: the billing ledger for your own work.

This is the right choice for async tasks, batch jobs, and any flow where the final cost is only known after execution.

<Note>
  If you are billing model calls, you usually do not need this flow. Point your OpenAI or Anthropic SDK at `https://api.velobase.io/v1` and pass `X-Velobase-Customer: <user-id>`, and the gateway freezes, settles the real token cost, and deducts from the customer wallet for you on every call. See the [Quickstart](/quickstart) for the gateway path. Use staged deduction below when you price the work yourself rather than per model token.
</Note>

`transaction_id` is the unique ID for this charge transaction and stays the same across freeze, consume, and unfreeze.

All amount fields are in cents. 1 USD = 100 cents = 1,000,000 credits. A bare `amount` is legacy and is interpreted as cents.

<Steps>
  <Step title="Freeze">
    Reserve a maximum budget before starting work. After freezing, the funds are no longer available to spend, but they have not been consumed yet.

    ```bash theme={null}
    curl -X POST https://api.velobase.io/v1/billing/freeze \
      -H "Authorization: Bearer your_api_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "customer_id": "user_987",
        "transaction_id": "llm_chat_001",
        "amount_cents": 100
      }'
    ```

    **Freeze Parameters**

    <ParamField path="body.customer_id" type="string" required>
      The unique identifier for this customer. The customer must already exist and have enough available funds in their wallet to cover the freeze amount.
    </ParamField>

    <ParamField path="body.transaction_id" type="string" required>
      Unique ID for this charge transaction. Use the same value for the subsequent `consume` or `unfreeze` call.
    </ParamField>

    <ParamField path="body.amount_cents" type="number" required>
      Maximum amount to reserve, in cents (1 USD = 100 cents = 1,000,000 credits). You may instead send `amount_usd` or `amount_credits`. A bare `amount` is legacy and is read as cents. Must be greater than 0. Set this to the upper bound of expected cost.
    </ParamField>

    <ParamField path="body.credit_types" type="string[]">
      Restrict the freeze to these wallet categories only. If omitted, the system may use any active funds. The same categories are used automatically during consume.
    </ParamField>

    <ParamField path="body.business_type" type="string">
      Transaction category for reporting. Examples: `TASK`, `ORDER`, `TOKEN_USAGE`.
    </ParamField>

    <ParamField path="body.description" type="string">
      A human-readable note for this freeze.
    </ParamField>

    **Freeze Response**

    ```json theme={null}
    {
      "transaction_id": "llm_chat_001",
      "frozen_amount": 100,
      "freeze_details": [
        {
          "account_id": "...",
          "credit_type": "default",
          "amount": 100
        }
      ],
      "is_idempotent_replay": false
    }
    ```

    <ResponseField name="transaction_id" type="string">
      The transaction ID passed in the request.
    </ResponseField>

    <ResponseField name="frozen_amount" type="number">
      Total amount reserved by this freeze, in cents.
    </ResponseField>

    <ResponseField name="freeze_details" type="array">
      Breakdown of which wallet sources were frozen. The system may freeze across multiple wallet categories.

      <Expandable title="freeze_details properties">
        <ResponseField name="account_id" type="string">
          Underlying wallet source ID that was frozen.
        </ResponseField>

        <ResponseField name="credit_type" type="string">
          Wallet category that was frozen.
        </ResponseField>

        <ResponseField name="amount" type="number">
          Amount frozen from that source, in cents.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="is_idempotent_replay" type="boolean">
      `true` when the response was returned from a previous request with the same `transaction_id`.
    </ResponseField>

    Returns `400` with `insufficient balance` if the customer wallet does not have enough available funds.

    If `credit_types` is provided during `freeze`, and the selected wallet categories do not have enough funds, the error message becomes `insufficient balance in selected credit_types`.
  </Step>

  <Step title="Consume">
    Settle the actual cost after the task finishes. Any unused funds are returned automatically.

    ```bash theme={null}
    curl -X POST https://api.velobase.io/v1/billing/consume \
      -H "Authorization: Bearer your_api_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "transaction_id": "llm_chat_001",
        "actual_amount": 73
      }'
    ```

    Example: freeze `100` cents, finish with an actual cost of `73` cents, and the remaining `27` cents is automatically returned.

    **Consume Parameters**

    <ParamField path="body.transaction_id" type="string" required>
      The same transaction ID used in the freeze step.
    </ParamField>

    <ParamField path="body.actual_amount" type="number">
      Actual amount to consume, in cents. If less than the frozen amount, the remainder is returned. Defaults to the full frozen amount if omitted.
    </ParamField>

    **Consume Response**

    ```json theme={null}
    {
      "transaction_id": "llm_chat_001",
      "consumed_amount": 73,
      "returned_amount": 27,
      "consume_details": [
        {
          "account_id": "...",
          "credit_type": "default",
          "amount": 73
        }
      ],
      "consumed_at": "2026-04-07T12:00:00.000Z",
      "is_idempotent_replay": false
    }
    ```

    <ResponseField name="consumed_amount" type="number">
      Amount permanently consumed by this operation, in cents.
    </ResponseField>

    <ResponseField name="returned_amount" type="number">
      Unused frozen funds automatically returned to the available wallet balance, in cents (`frozen_amount - actual_amount`).
    </ResponseField>

    <ResponseField name="consume_details" type="array">
      Breakdown of which wallet sources were consumed.

      <Expandable title="consume_details properties">
        <ResponseField name="account_id" type="string">
          Underlying wallet source ID that was consumed.
        </ResponseField>

        <ResponseField name="credit_type" type="string">
          Wallet category that was consumed.
        </ResponseField>

        <ResponseField name="amount" type="number">
          Amount consumed from that source, in cents.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="consumed_at" type="string">
      ISO 8601 timestamp when consumption completed.
    </ResponseField>

    <ResponseField name="is_idempotent_replay" type="boolean">
      `true` when the response was returned from a previous request with the same `transaction_id`.
    </ResponseField>
  </Step>

  <Step title="Unfreeze (Cancel)">
    Release frozen funds without consuming. Use when the task is cancelled.

    ```bash theme={null}
    curl -X POST https://api.velobase.io/v1/billing/unfreeze \
      -H "Authorization: Bearer your_api_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "transaction_id": "llm_chat_001"
      }'
    ```

    **Unfreeze Response**

    ```json theme={null}
    {
      "transaction_id": "llm_chat_001",
      "unfrozen_amount": 100,
      "unfreeze_details": [
        {
          "account_id": "...",
          "credit_type": "default",
          "amount": 100
        }
      ],
      "unfrozen_at": "2026-04-07T12:01:00.000Z",
      "is_idempotent_replay": false
    }
    ```

    <ResponseField name="unfrozen_amount" type="number">
      Total amount returned to the available wallet balance, in cents.
    </ResponseField>

    <ResponseField name="unfreeze_details" type="array">
      Breakdown of which wallet sources were unfrozen.

      <Expandable title="unfreeze_details properties">
        <ResponseField name="account_id" type="string">
          Underlying wallet source ID that was unfrozen.
        </ResponseField>

        <ResponseField name="credit_type" type="string">
          Wallet category that was unfrozen.
        </ResponseField>

        <ResponseField name="amount" type="number">
          Amount released from that source, in cents.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="unfrozen_at" type="string">
      ISO 8601 timestamp when the unfreeze completed.
    </ResponseField>

    <ResponseField name="is_idempotent_replay" type="boolean">
      `true` when the response was returned from a previous request with the same `transaction_id`.
    </ResponseField>
  </Step>
</Steps>

## Wallet Categories and Consumption Order

* The system automatically freezes and consumes from all available wallet funds.
* If `credit_types` is provided during `freeze`, only those wallet categories are eligible for this transaction.
* Callers do not choose which wallet category to use.
* Funds that expire sooner are handled first. If expiry is the same, older sources are handled first.
* Only funds that are currently active participate in freeze and consume. Funds that have not started yet or have already expired are ignored.
* Use the customer API to inspect remaining funds per wallet source.

## Idempotency

All three operations are idempotent on `transaction_id`. Duplicate calls return the original result with `is_idempotent_replay: true`.

## Verify the Result

Fetch the customer to inspect their wallets:

```bash theme={null}
curl https://api.velobase.io/v1/customers/user_987 \
  -H "Authorization: Bearer your_api_key_here"
```

```json theme={null}
{
  "id": "user_987",
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "metadata": {},
  "wallets": {
    "default": {
      "total": 1000,
      "used": 73,
      "frozen": 0,
      "available": 927,
      "sources": [
        {
          "source": "deposit",
          "total": 1000,
          "used": 73,
          "frozen": 0,
          "available": 927,
          "starts_at": null,
          "expires_at": null
        }
      ]
    }
  },
  "created_at": "2026-04-07T11:00:00.000Z"
}
```

After each step, check the `wallets["default"]` summary:

* whether `frozen` increases after freeze
* whether `used` increases after consume
* whether `available` changes as expected
* which wallet `sources[]` were used

All wallet amounts are in cents (1 USD = 100 cents = 1,000,000 credits).

## Error Format

All API errors return an HTTP error status and a structured `error` object:

```json theme={null}
{
  "error": {
    "message": "freeze record not found",
    "type": "not_found",
    "code": "freeze_record_not_found"
  }
}
```

<ResponseField name="error.message" type="string">
  Human-readable description of the error.
</ResponseField>

<ResponseField name="error.type" type="string">
  High-level error category.
</ResponseField>

<ResponseField name="error.code" type="string">
  Stable machine-readable error code for programmatic handling.
</ResponseField>
