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

# Gating features

> Check a customer's balance before an action, then track each successful use exactly once.

Metering is two calls from your backend: `check` before an action asks whether the customer can afford it, and `track` after it succeeds spends the credits.

```mermaid theme={"system"}
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "IBM Plex Sans, sans-serif", "fontSize": "14px", "actorBkg": "#f7f6f3", "actorBorder": "#c4bfb5", "actorTextColor": "#1a1816", "signalColor": "#3d3934", "signalTextColor": "#1a1816", "noteBkgColor": "#eff2fd", "noteBorderColor": "#bfcbf7", "noteTextColor": "#1d36a0", "labelBoxBkgColor": "#f7f6f3", "labelBoxBorderColor": "#c4bfb5", "labelTextColor": "#1a1816", "loopTextColor": "#1a1816"}}}%%
sequenceDiagram
    participant App as Your backend
    participant L as Ledger
    App->>L: check(customerId, featureId)
    L-->>App: allowed, availableCredits, creditCost
    alt allowed is false
        Note over App: Offer an upgrade or a credit pack
    else allowed is true
        Note over App: Run the action
        App->>L: track(customerId, featureId, requestId)
        L-->>App: credits spent
        App->>L: Retry with the same requestId
        L-->>App: idempotent: true, nothing charged
    end
```

The examples continue from [Setup](https://docs.integrateledger.com/getting-started/setup.md): a `chat_message` feature that costs 1 credit, and a customer on the Free plan.

## Check, run, track

```typescript TypeScript theme={"system"}
const { allowed } = await billing.checkCredits({
	customerId: user.id,
	featureId: "chat_message",
});

if (!allowed) {
	return showUpgradeOffer();
}

const message = await sendChatMessage(prompt);

await billing.trackUsage({
	customerId: user.id,
	featureId: "chat_message",
	requestId: message.id, // unique to this one use
});
```

```python Python theme={"system"}
decision = billing.check_credits(customer_id=user.id, feature_id="chat_message")

if not decision["allowed"]:
    return show_upgrade_offer()

message = send_chat_message(prompt)

billing.track_usage(
    customer_id=user.id,
    feature_id="chat_message",
    request_id=message.id,  # unique to this one use
)
```

* **`check` never spends.** It returns `{ "allowed": true, "availableCredits": 50, "creditCost": 1 }`, and `allowed: false` with a `200` when the balance can't cover the cost. That's the moment to offer an [upgrade](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md) or a [credit pack](https://docs.integrateledger.com/configure-pricing/credit-packs.md).
* **Track only what succeeded.** If the action failed, don't track it and the customer isn't charged.
* **The request ID makes `track` idempotent.** Use an ID you already store with the action: a message ID, a job ID, or a UUID you save with it. Up to 255 characters, and one ID belongs to one feature.
* **Retry with the same ID.** If a `track` response is lost, send it again with the **same** `requestId`. Ledger returns the original result with `"idempotent": true` and charges nothing more. A new ID records a second use.

`track` returns what it spent:

```json Response theme={"system"}
{
  "allowed": true,
  "availableCredits": 49,
  "chargedCredits": 1,
  "overageCredits": 0,
  "overageRemainingCents": null,
  "request_id": "msg_0001",
  "idempotent": false
}
```

## Handle a refused track

`check` doesn't reserve credits, so another request can spend the last credit between your `check` and your `track`. `track` decides again in its own transaction and can refuse with a `409`. Nothing is charged, and the same request ID can succeed later, for example after the customer buys a pack.

```typescript TypeScript theme={"system"}
import { TrackError } from "integrateledger/models/errors";

try {
	await billing.trackUsage({ customerId, featureId: "chat_message", requestId });
} catch (error) {
	if (!(error instanceof TrackError) || error.statusCode !== 409) {
		throw error;
	}
	console.warn(error.data$.error); // "insufficient_credits", ...
}
```

```python Python theme={"system"}
from integrateledger import LedgerAPIError

try:
    billing.track_usage(customer_id=customer_id, feature_id="chat_message", request_id=request_id)
except LedgerAPIError as error:
    if error.status != 409:
        raise
    print(error.code)  # "insufficient_credits", ...
```

| `error` | Meaning | What to do |
| - | - | - |
| `insufficient_credits` | The balance can't cover the cost. The body has `availableCredits` and `creditCost`. | Treat it like `allowed: false`. |
| `overage_cap_reached` | This use would pass the customer's monthly [overage](https://docs.integrateledger.com/configure-pricing/overage.md) cap. | Offer a higher cap, an upgrade or a pack. |
| `request_id_conflict` | The request ID was already used for a different feature. | Make IDs unique per action. |

A `404 customer_not_found` or `feature_not_found` means the customer or feature doesn't exist with this key's environment. [Errors](https://docs.integrateledger.com/errors.md) lists every code.

## When Ledger is unreachable

The SDKs [fail open](https://docs.integrateledger.com/fail-open.md) by default: `check` is allowed and `track` is replayed later with the same request ID, both marked `failedOpen: true`, so an outage never blocks your users. Pass `failOpen: false` (Python `fail_open=False`) to handle outages yourself.

Call Ledger from your backend only, never from a browser or mobile app. **Usage** in the dashboard lists every tracked request, charged or blocked, with the reason.

> [!NOTE]
>
> **Other ways.** Without an SDK, call [`POST /v1/check`](https://docs.integrateledger.com/api-reference/usage/check) and [`POST /v1/track`](https://docs.integrateledger.com/api-reference/usage/track); over HTTP the request ID field is `request_id`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.