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

# Direct Deduction

> Deduct from a customer's wallet in one request when cost is known.

Use `POST /v1/billing/deduct` to deduct from a customer's available wallet balance in a single request.

This is the right choice for fixed-price image generation, fixed-cost API calls, and other one-time operations where cost is known upfront. It is one of the billing primitives you use for work you price yourself.

<Note>
  Model calls you route through the Velobase AI gateway (`POST /v1/chat/completions`, `POST /v1/messages`) bill the customer wallet automatically at real token cost. You do not need to call `deduct` for those. Use direct deduction for your own priced work. See the [gateway quickstart](/quickstart) for routing model calls.
</Note>

If cost is unknown in advance, or you need to reserve funds before settlement, use [staged deduction](/integration/staged-deduction) instead.

## Minimal Example

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

## Parameters

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

<ParamField path="body.transaction_id" type="string" required>
  Unique ID for this charge transaction. Also serves as the idempotency key: repeating a request with the same `transaction_id` returns the original result without double-charging.
</ParamField>

<ParamField path="body.amount_usd" type="number" required>
  Amount to deduct, in US dollars. Must be greater than 0. You can instead pass `amount_cents` or `amount_credits`, where 1 USD = 100 cents = 1,000,000 credits. A bare `amount` is legacy and is interpreted as cents.
</ParamField>

<ParamField path="body.credit_types" type="string[]">
  Restrict deduction to these wallet categories only. If omitted, the system may draw from any active credits. If provided and the selected categories have insufficient credits, the request fails.
</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 transaction.
</ParamField>

## Response

```json theme={null}
{
  "transaction_id": "img_gen_001",
  "deducted_amount": 50000,
  "deduct_details": [
    {
      "account_id": "...",
      "credit_type": "default",
      "amount": 50000
    }
  ],
  "deducted_at": "2026-04-07T12:00:00.000Z",
  "is_idempotent_replay": false
}
```

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

<ResponseField name="deducted_amount" type="number">
  Total amount actually deducted, in credits (1,000,000 credits = 1 USD).
</ResponseField>

<ResponseField name="deduct_details" type="array">
  Deduction breakdown. Funds may be consumed from multiple wallet categories when the amount spans more than one source.

  <Expandable title="deduct_details properties">
    <ResponseField name="account_id" type="string">
      Underlying funding source ID used for the deduction.
    </ResponseField>

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

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

<ResponseField name="deducted_at" type="string">
  ISO 8601 timestamp when the deduction 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>

## Deduction Rules and Wallet Categories

* The system automatically deducts from all available balance across the customer's wallets.
* If `credit_types` is provided, deduction is limited to those wallet categories only.
* Callers do not choose which wallet category to consume from.
* Funds that expire sooner are consumed first. If expiry is the same, older sources are consumed first.
* Only funds that are currently active are eligible for deduction. Sources that have not started yet or have already expired are ignored.
* Use `GET /v1/customers/{id}` to inspect the remaining balance per wallet and per source.

## Insufficient Balance

Returns `400` if the customer doesn't have enough available balance:

```json theme={null}
{
  "error": {
    "message": "insufficient balance",
    "type": "bad_request"
  }
}
```

If `credit_types` is provided, the error message becomes `insufficient balance in selected credit_types` when the selected wallet categories do not have enough credits.

Failed requests do not produce a partial deduction and do not leave behind a half-completed state.

## Idempotency

Same `transaction_id` returns the same result, with `is_idempotent_replay: true` and no double charge.

## Verify the Result

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

`GET /v1/customers/{id}` returns the customer with a `wallets` map. Each wallet reports `total`, `used`, `frozen`, and `available`, plus a `sources` array broken down by funding source:

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

After deduction, check:

* whether the wallet's `available` decreased (values are in credits, where 1,000,000 credits = 1 USD)
* which `sources[]` entries were consumed
* whether each `sources[].available` value changed as expected

## Error Format

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

```json theme={null}
{
  "error": {
    "message": "insufficient balance in selected credit_types",
    "type": "bad_request",
    "code": "insufficient_balance_in_selected_credit_types"
  }
}
```

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