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

# Metered Entitlements

> Track and gate usage with time-based credit grants

<Info>
  Metered entitlements require Standard Billing (`billingVersion: 1`). See
  [Billing Versions](/platform/billing/billing-versions) for details on the two billing models.
</Info>

Metered entitlements give customers a credit grant when they subscribe. As they consume resources—API calls, compute minutes, storage operations—usage events deplete the grant. At any point, you can query the balance to see how many credits remain and whether the customer still has access.

Unlike static entitlements (which grant a fixed configuration value), metered entitlements track real-time consumption against a replenishing credit pool.

## How it works

1. Create a **metered feature** with a billable metric that measures consumption
2. Attach the feature to a plan price with an **entitlement template** defining the grant amount, reset period, and rollover rules
3. Customer subscribes → a grant is provisioned automatically for the current period
4. Your app sends **usage events** as the customer consumes
5. Call the **balance endpoint** to check remaining credits and gate access in real time

## Setup

### Creating a metered feature

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.paygentic.io/v0/features" \
    -H "Authorization: Bearer sk_test_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "productId": "prod_abc123",
      "merchantId": "mer_abc123",
      "name": "API Calls",
      "key": "api-calls",
      "type": "metered"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.paygentic.io/v0/features', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_test_YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      productId: 'prod_abc123',
      merchantId: 'mer_abc123',
      name: 'API Calls',
      key: 'api-calls',
      type: 'metered',
    }),
  });

  const feature = await response.json();
  ```
</CodeGroup>

### Linking to a price (entitlement template)

When creating or updating a price, include an `entitlementTemplate` to configure how grants are provisioned for subscribers:

```bash cURL theme={null}
curl -X POST "https://api.paygentic.io/v0/prices" \
  -H "Authorization: Bearer sk_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "planId": "plan_def456",
    "unitPrice": "0",
    "billableMetricId": "metric_xyz789",
    "feature": {
      "id": "feat_abc123",
      "entitlementTemplate": {
        "usagePeriod": {
          "interval": "P1M"
        },
        "issueAfterReset": 1000,
        "isSoftLimit": false,
        "resetMaxRollover": 0,
        "resetMinRollover": 0,
        "preserveOverageAtReset": false
      }
    }
  }'
```

<Note>
  Setting `unitPrice: "0"` grants customers access without charging for individual events. This is the typical setup when you want to use metered entitlements for quota enforcement rather than per-event billing.
</Note>

### Linking the price to a plan

```bash cURL theme={null}
curl -X PATCH "https://api.paygentic.io/v0/plans/plan_def456" \
  -H "Authorization: Bearer sk_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prices": ["price_ghi789"]
  }'
```

## Entitlement template reference

| Field                     | Type              | Default      | Description                                                                         |
| ------------------------- | ----------------- | ------------ | ----------------------------------------------------------------------------------- |
| `usagePeriod.interval`    | ISO 8601 duration | required     | How often the grant resets. `"P1M"` monthly, `"P1D"` daily, `"P1W"` weekly          |
| `usagePeriod.anchor`      | datetime          | `activeFrom` | Reference point for computing period boundaries                                     |
| `issueAfterReset`         | number            | —            | Credits granted at the start of each period. If omitted, no grant is provisioned    |
| `issueAfterResetPriority` | integer           | `0`          | Burn order when multiple grants exist (lower = burned first)                        |
| `isSoftLimit`             | boolean           | `false`      | If `true`, customer retains access even when balance reaches 0                      |
| `resetMaxRollover`        | number            | `0`          | Max unused credits carried into the next period (`0` = use-it-or-lose-it)           |
| `resetMinRollover`        | number            | `0`          | Minimum credits guaranteed to roll over                                             |
| `preserveOverageAtReset`  | boolean           | `false`      | If `true`, overage from the previous period is deducted from the new period's grant |

## Checking the balance

Use `GET /v1/entitlements/{entitlementId}` to get a real-time snapshot. For metered entitlements, the response includes live balance and usage data inline.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.paygentic.io/v1/entitlements/ent_abc123" \
    -H "Authorization: Bearer sk_test_YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://api.paygentic.io/v1/entitlements/ent_abc123',
    {
      headers: {
        'Authorization': 'Bearer sk_test_YOUR_API_KEY',
      },
    }
  );

  const entitlement = await response.json();
  // { featureType: 'metered', balance: 750, hasAccess: true, ... }
  ```
</CodeGroup>

**Response example:**

```json theme={null}
{
  "object": "entitlement",
  "id": "ent_abc123",
  "customerId": "cus_xyz789",
  "featureId": "feat_789",
  "featureKey": "api-calls",
  "featureType": "metered",
  "subscriptionId": "sub_abc",
  "status": "active",
  "activeFrom": "2026-01-01T00:00:00Z",
  "activeTo": null,
  "hasAccess": true,
  "metadata": {},
  "balance": 750,
  "usageInPeriod": 250,
  "overage": 0,
  "currentPeriodStart": "2026-02-01T00:00:00Z",
  "currentPeriodEnd": "2026-03-01T00:00:00Z"
}
```

**Response fields:**

| Field                                     | Description                                            |
| ----------------------------------------- | ------------------------------------------------------ |
| `id`                                      | The entitlement ID                                     |
| `featureKey`                              | The unique identifier for the feature                  |
| `featureType`                             | Always `"metered"` for metered features                |
| `status`                                  | Entitlement status: `active`, `expired`, or `canceled` |
| `hasAccess`                               | `true` if `balance > 0`, or if `isSoftLimit` is `true` |
| `balance`                                 | Remaining credits across all active grants             |
| `usageInPeriod`                           | Total usage consumed in the current period             |
| `overage`                                 | Usage beyond available credits                         |
| `currentPeriodStart` / `currentPeriodEnd` | Current usage period boundaries                        |

## Gating access

Check the balance before allowing an operation, then send the usage event regardless:

```javascript theme={null}
async function canMakeApiCall(entitlementId) {
  const response = await fetch(
    `https://api.paygentic.io/v1/entitlements/${entitlementId}`,
    { headers: { 'Authorization': 'Bearer sk_test_YOUR_API_KEY' } }
  );
  const { hasAccess } = await response.json();
  return hasAccess;
}

// In your request handler
if (!(await canMakeApiCall(customer.entitlementId))) {
  return res.status(429).json({ error: 'API call quota exceeded' });
}

// Proceed with the request, then send a meter event
await fetch('https://api.paygentic.io/v0/events', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_test_YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'api.call',
    subject: customerId,
    data: { quantity: 1 },
  }),
});
```

<Warning>
  The grant engine does not block usage events in real time. Always check `hasAccess` before allowing an operation—do not rely on the event pipeline to enforce limits.
</Warning>

## Soft vs. hard limits

|                      | Hard Limit (`isSoftLimit: false`) | Soft Limit (`isSoftLimit: true`)         |
| -------------------- | --------------------------------- | ---------------------------------------- |
| **When balance = 0** | `hasAccess` returns `false`       | `hasAccess` stays `true`                 |
| **Best for**         | Strict quotas, free-tier caps     | Overage billing, best-effort enforcement |
| **Overage**          | Not accrued (access denied)       | Tracked in the `overage` field           |

## Grant lifecycle examples

These examples show how grants behave across billing periods under different configurations.

### Example 1 — monthly grant, use-it-or-lose-it

```
issueAfterReset: 1000, resetMaxRollover: 0, isSoftLimit: false
```

| Date       | Event                                                | Balance            |
| ---------- | ---------------------------------------------------- | ------------------ |
| Jan 1      | Grant issued                                         | 1000               |
| Jan (mid)  | 600 credits consumed                                 | 400                |
| Feb 1      | Period reset — unused 400 forfeited, new 1000 issued | 1000               |
| Feb (late) | 1000 credits consumed                                | 0                  |
| Feb (late) | Another request                                      | `hasAccess: false` |

### Example 2 — monthly grant with rollover cap

```
issueAfterReset: 1000, resetMaxRollover: 500, isSoftLimit: false
```

| Date  | Event                                                     | Balance |
| ----- | --------------------------------------------------------- | ------- |
| Jan 1 | Grant issued                                              | 1000    |
| Jan   | Only 200 consumed — 800 remaining                         | 800     |
| Feb 1 | Reset: 800 > 500 cap, rolled over as 500; new 1000 issued | 1500    |
| Feb   | Heavy usage month                                         | —       |

The rollover is applied before the new grant, so the customer starts the period with `rollover + issueAfterReset`.

### Example 3 — daily grants on a monthly subscription

```
usagePeriod.interval: "P1D", issueAfterReset: 100, resetMaxRollover: 50
```

A customer's subscription renews monthly, but their grant resets daily. Each day they receive a fresh allocation.

| Day   | Event                                          | Balance |
| ----- | ---------------------------------------------- | ------- |
| Day 1 | Daily grant issued                             | 100     |
| Day 1 | 80 consumed                                    | 20      |
| Day 2 | Reset: 20 ≤ 50 cap, rolls over; new 100 issued | 120     |
| Day 2 | 110 consumed                                   | 10      |
| Day 3 | Reset: 10 rolls over; new 100 issued           | 110     |

### Example 4 — soft limit with overage carry-forward

```
issueAfterReset: 1000, isSoftLimit: true, preserveOverageAtReset: true
```

| Date   | Event                                        | Balance | Overage |
| ------ | -------------------------------------------- | ------- | ------- |
| Jan 1  | Grant issued                                 | 1000    | 0       |
| Jan    | 1200 consumed                                | 0       | 200     |
| Jan 31 | Access still allowed (soft limit)            | —       | 200     |
| Feb 1  | Reset: new 1000 issued, 200 overage deducted | 800     | 0       |

The `preserveOverageAtReset` flag ensures customers who overconsume one period have a reduced allowance the next period.

### Example 5 — minimum rollover floor

```
issueAfterReset: 1000, resetMaxRollover: 100, resetMinRollover: 100
```

When both min and max rollover are set to the same value, customers always carry forward exactly that amount regardless of actual usage:

| Jan usage | Remaining | Rolls over as       |
| --------- | --------- | ------------------- |
| 950       | 50        | 100 (raised to min) |
| 900       | 100       | 100 (exact match)   |
| 200       | 800       | 100 (capped at max) |

## Rollover and overage reference

At each period reset, the carried-over balance is computed as:

```
rolledOver = min(resetMaxRollover, max(resetMinRollover, currentBalance))
```

| Ending balance | `resetMaxRollover` | `resetMinRollover` | Rolls over as         |
| -------------- | ------------------ | ------------------ | --------------------- |
| 800            | 1000               | 0                  | 800 (within range)    |
| 1200           | 1000               | 0                  | 1000 (capped at max)  |
| 50             | 1000               | 100                | 100 (raised to min)   |
| 0              | 0                  | 0                  | 0 (use-it-or-lose-it) |

When `preserveOverageAtReset` is `true`, any overage accumulated in the previous period is subtracted from the new period's starting balance after rollover and the new grant are applied.

## Next steps

* [Grants](/platform/billing/grants) — Add credits mid-cycle via direct creation or paid purchase
* [Features and Entitlements](/platform/pricing/features) — Feature types and automatic provisioning
* [Meter Events](/platform/metering/meter-events) — Send usage data that powers metered entitlements
* [Billable Metrics](/platform/pricing/billable-metrics) — Define what you measure
