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

# Usage-based billing

> Report what customers use, and have Saasybill bill it in arrears

<Info>
  This page covers the API. Meters and the plans that bill them are set up in the app. For how plans price, see [Plans explained](/core/plans).
</Info>

Some plans bill for what a customer uses, not only for a fixed fee. Your platform reports the usage as events. Saasybill totals them at the end of each billing period, prices the total, and raises the invoice in Xero.

***

## How it works

A **meter** is what is counted, such as `api_calls` or `storage_gb`. A plan chooses which meters it bills and prices each one on its own. A subscription on that plan is billed for the meters it uses.

<Steps>
  <Step title="Find the meter">
    Each plan lists the meters it bills in `meters`. Read the `meter_code` there. See [Find the meter](#find-the-meter).
  </Step>

  <Step title="Report usage">
    Send events to `POST /v1/usage_events` as usage happens. Each names a subscription, a meter, a quantity and a time. Nothing is billed yet.
  </Step>

  <Step title="The period closes">
    At the subscription's renewal date, Saasybill totals each meter's events for the period that just ended and prices the total with the plan's pricing for that meter.
  </Step>

  <Step title="Saasybill invoices it">
    The usage is billed in arrears, as lines on an invoice in Xero. Each line is a [`usage` line](#usage-on-an-invoice).
  </Step>
</Steps>

A plan can bill a fixed fee, usage, or both.

| Plan shape | `type` | What is billed |
| - | - | - |
| Fixed fee only | `flat_fee`, `per_unit`, `tiered`, `volume` or `stairstep` | The fee, in advance. |
| Fixed fee plus usage | Any of the above, with `meters` | The fee in advance, and usage in arrears. |
| Usage only | `no_base_fee` | Usage in arrears. It has no units and no fee. |

<Note>
  Usage is always billed after the period, and the recurring fee always before it. You don't choose the timing.
</Note>

***

## Before you report usage

You need three things.

1. **A key with the `usage:write` scope.** See [Authentication](/api-reference/authentication#scopes). A key that only reports usage needs no other scope.
2. **The subscription's id.** Save it when you [create the subscription](/api-reference/guides/create-a-subscription). Events name the subscription by its Saasybill id, not by a customer code.
3. **The meter.** Meters are created in the app and can't be created through the API.

### Find the meter

[List plans](/api-reference/plans/list-plans) and read each plan's `meters`. Each entry has a `meter` (the Saasybill id) and a `meter_code` (the meter's ID as it appears in the app).

```json theme={null}
{
  "meter": "c4d8e2f6-7a19-4b30-95e1-3f6a8b0d2c55",
  "meter_code": "api_calls",
  "name": "API Calls",
  "pricing_model": "per_unit",
  "price": "0.0200",
  "included_units": 10000,
  "minimum_units": 0
}
```

An event names the meter with either `meter` or `meter_code`. Send exactly one. A `meter_code` is matched exactly, so `API_Calls` and `api_calls` are two different meters.

<Tip>
  Cache the meter codes with the rest of your catalogue. They only change when someone edits a meter in the app.
</Tip>

***

## Report usage

`POST /v1/usage_events` takes a batch of up to 100 events.

```json theme={null}
{
  "events": [
    {
      "event_id": "evt_20261012_0001",
      "subscription": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "meter_code": "api_calls",
      "quantity": "1250",
      "timestamp": "2026-10-12T03:15:00Z"
    },
    {
      "event_id": "evt_20261012_0002",
      "subscription": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "meter_code": "storage_gb",
      "quantity": "12.5",
      "timestamp": "2026-10-12T03:15:00Z"
    }
  ]
}
```

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.saasybill.com/v1/usage_events \
    -H "Authorization: Bearer $SAASYBILL_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "events": [
        {
          "event_id": "evt_20261012_0001",
          "subscription": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
          "meter_code": "api_calls",
          "quantity": "1250",
          "timestamp": "2026-10-12T03:15:00Z"
        }
      ]
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.saasybill.com/v1/usage_events", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAASYBILL_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      events: [
        {
          event_id: "evt_20261012_0001",
          subscription: "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
          meter_code: "api_calls",
          quantity: "1250",
          timestamp: "2026-10-12T03:15:00Z",
        },
      ],
    }),
  });

  const batch = await response.json();
  if (batch.rejected > 0) {
    // Some events were stored and some were not. See "Check every result".
  }
  ```
</CodeGroup>

### Event fields

| Field | Type | Description |
| - | - | - |
| `event_id` | string | Your own id for the event. Required. Unique across your organisation. 1 to 255 printable characters with no spaces at either end. See [Resend safely](#resend-safely). |
| `subscription` | string | Required. The Saasybill subscription id the usage is billed to. |
| `meter` or `meter_code` | string | Required. Exactly one. The meter's Saasybill id, or its ID as shown in the app. |
| `quantity` | string or number | Required. How much was used. Greater than zero, with at most four decimal places and twelve whole digits. No sign, exponent or thousands separators. |
| `timestamp` | string | Required. When the usage happened. ISO 8601 with a time zone, such as `2026-10-12T03:15:00Z` or `2026-10-12T14:15:00+11:00`. |

Quantities can be fractions, such as `12.5` gigabytes. Send a string if your language can't represent the number exactly.

### What a timestamp can be

An event is sorted into a billing period by its `timestamp`, not by when it arrives.

* It can't be more than five minutes in the future. An event dated further ahead is usually a unit mistake, such as milliseconds sent as seconds.
* It can't be before the subscription started, or after it ended.
* It can be any age otherwise. See [Late usage](#late-usage).

***

## Check every result

Reporting usage can partly succeed, so a `2xx` doesn't mean every event was stored.

| Problem | Response |
| - | - |
| An event is **malformed**: a field is missing, a quantity isn't a number, or both `meter` and `meter_code` are given. | `422` for the whole request. Nothing is stored. `error.param` names the field, such as `events.3.quantity`. |
| An event is well formed but **can't be accepted**: the subscription doesn't exist, or its plan doesn't bill the meter. | `207`. That event is rejected on its own and the rest are stored. |
| Every event is stored or already stored. | `200`. |

The body is a [usage event batch](/api-reference/objects#usage-event-batch). It counts what happened and gives one result per event, in the order you sent them.

```json theme={null}
{
  "object": "usage_event_batch",
  "created": 1,
  "duplicates": 1,
  "rejected": 1,
  "results": [
    { "event_id": "evt_20261012_0001", "status": "created" },
    { "event_id": "evt_20261012_0002", "status": "duplicate" },
    {
      "event_id": "evt_20261012_0004",
      "status": "rejected",
      "error": {
        "code": "meter_not_on_plan",
        "message": "This subscription's plan does not bill for this meter."
      }
    }
  ]
}
```

<Warning>
  If you ignore the body, you lose rejected events silently. Always read `rejected`. Branch on `error.code`, never on `error.message`.
</Warning>

A rejected event is not stored. Fix the cause and send it again.

### Rejection codes

| Code | Meaning | What to do |
| - | - | - |
| `subscription_not_found` | No subscription with this id exists. | Check the id. A subscription from another organisation, or from the live organisation when you use a test key, is not found. |
| `meter_not_found` | No meter with this ID exists. | Check `meter` or `meter_code`. A code isn't accepted as an id, or the other way round. |
| `meter_inactive` | The meter is inactive and takes no new events. | Ask the organisation to make the meter active again. A **legacy** meter still takes events from plans that bill it. |
| `meter_not_on_plan` | The subscription's plan doesn't bill this meter. | Use a meter from the plan's `meters`. |
| `timestamp_in_future` | The timestamp is more than five minutes ahead of the clock. | Check the clock and the unit of your timestamp. |
| `before_subscription_start` | The timestamp is before the subscription started. | The usage has no billing period to fall in. |
| `after_subscription_end` | The timestamp is after the subscription ended. | The usage has no billing period to fall in. |
| `event_id_conflict` | An event with this `event_id` was already reported with different details. | Use a new `event_id` for a different event. The first one is kept. |

***

## Resend safely

The `event_id` is what makes a resend safe. If a request times out, send the same batch again.

| You send | Result |
| - | - |
| An `event_id` Saasybill hasn't seen. | <Badge color="green">created</Badge>. The event is stored. |
| An `event_id` already stored, with the same subscription, meter, quantity and timestamp. | <Badge>duplicate</Badge>. It is counted once, so a retry can't double count. |
| An `event_id` already stored, with different details. | <Badge color="red">rejected</Badge> as `event_id_conflict`. The first event is kept. |

Use an id from your own system that is the same on every attempt, such as your meter reading's id. Two quantities that look the same, like `12.5` and `12.50`, are the same quantity.

<Info>
  An [`Idempotency-Key`](/api-reference/idempotency) is accepted but optional here, because every event carries its own `event_id`.
</Info>

***

## How usage is priced

At the end of the period, Saasybill totals each meter's events and prices the total with the plan's pricing for that meter.

1. **Total.** A meter's events are summed over the period, which runs from the subscription's start date to its renewal date, in the subscription's timezone.
2. **Minimum.** The period bills the larger of the total and the meter's [minimum](#meter-minimums), so a period with no events still bills the minimum.
3. **Round up.** The result is rounded up to whole units, so a customer is never billed for less than they used.
4. **Price.** The whole-unit quantity is priced by the meter's `pricing_model`.
5. **Discount.** The subscription's discount comes off the result.

| `pricing_model` | How it prices |
| - | - |
| `per_unit` | `(units − included_units) × price`, never below zero. |
| `tiered` | Each band of units is priced at its own rate, and the bands add up. |
| `volume` | The band the total falls in sets one rate for every unit. |
| `stairstep` | The band the total falls in sets one price for the whole quantity. |

For example, 14,000 calls on a `per_unit` meter with 10,000 included at `0.0200` bill `4,000 × 0.02 = 80.00`.

Rates can have up to four decimal places, because a unit is often worth less than a cent. No usage is no charge under every model, and a meter that prices to nothing gets no line. A period where no meter prices to anything raises no invoice.

### Meter minimums

A meter can have a minimum: the least a period bills for it. The plan sets it in `meters[].minimum_units`. A subscription can raise it for itself, but never lower it.

Set it when you create the subscription, with `meter_minimums`. Name each meter by `meter` or `meter_code`.

```json theme={null}
{
  "customer_code": "ACME-001",
  "plan_code": "API-USAGE",
  "meter_minimums": [
    { "meter_code": "api_calls", "minimum_units": 10000 }
  ]
}
```

| Rule | Detail |
| - | - |
| Only meters the plan bills | Any other meter is refused with `422`. |
| Each meter once | A meter named twice is refused. |
| Not below the plan's | A minimum below the plan's own is refused. A figure equal to the plan's isn't stored. |
| After creation | The API can't change them on an existing subscription. Edit the subscription in the app. |

The `422` names the field, such as `meter_minimums.0.meter`. The minimum in force for each meter is in the subscription's `meters`, as `minimum_units`, beside the plan's own in `plan_minimum_units`.

***

## Usage on an invoice

Each meter that prices to something becomes one line on the invoice. Read it with [`GET /v1/invoices`](/api-reference/invoices/list-invoices).

```json theme={null}
{
  "id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c77",
  "type": "usage",
  "revenue_type": "usage",
  "meter": "c4d8e2f6-7a19-4b30-95e1-3f6a8b0d2c55",
  "usage_quantity": "14000.0000",
  "name": "API Calls",
  "description": "API Calls: Requests made through the API. 14,000 Calls Used (Sep 16, 2026 - Oct 16, 2026). 10,000 Calls Included.",
  "units": 14000,
  "amount": "80.0000",
  "discount_amount": "0.0000",
  "charge_amount": "80.0000",
  "effective_date": "2026-10-16T13:00:00.000Z"
}
```

| Field | Value on a usage line |
| - | - |
| `type` | `usage` |
| `revenue_type` | `usage`. Metered usage is never `renewal`. |
| `meter` | The Saasybill meter id. |
| `usage_quantity` | The quantity the line was priced for, after the meter's minimum. |
| `units` | `usage_quantity` rounded up. |

Depending on the organisation's invoicing settings, the usage is either lines on the renewal invoice or an invoice of its own. Either way, the invoice follows the organisation's usual rules for status and approval, and it reaches you through the same [webhooks](/api-reference/webhooks/events) as any other invoice.

<Info>
  The organisation can set a **usage grace period** of 0 to 28 days. It delays the usage invoice after the period closes, for senders that can only report once a period is over. Events reported during the grace days are on time.
</Info>

<Note>
  Once an organisation has defined a meter, it must map a **Usage Revenue** account in Xero. Until it does, creating a subscription or changing units answers `409 xero_accounts_not_mapped`, whatever plan is sold.
</Note>

***

## Late usage

An event for a period that has already been invoiced is **late**. Saasybill accepts it and doesn't reject it. The response doesn't flag it as late.

In the app, the subscription asks a person what to do with late usage that costs more:

* Amend the usage invoice, while it can still be changed in Xero.
* Raise a new invoice for the late usage.
* Add it to the next invoice.
* Ignore it, which bills nothing.

If nobody acts, the late usage is added to the next renewal invoice, so nothing is lost. Late usage is priced at the marginal price: the price of the new total less what was already billed.

A volume meter can price lower when late usage takes the total into a cheaper band. Saasybill then credits the customer automatically with a credit note, which you can read with [`GET /v1/credit_notes`](/api-reference/credit-notes/list-credit-notes).

<Tip>
  If your system can only report once a period is over, ask the organisation to set a usage grace period, so each period's usage arrives on time and not late.
</Tip>

***

## Usage on a subscription

A subscription shows two figures for its meters, and keeps them apart.

| Field | What it is |
| - | - |
| `renewal_value` | What the next period is certain to bill. It includes the minimum of each meter, after discount, and never the usage above them. |
| `minimum_usage_value` | The part of `renewal_value` that is the meter minimums. |
| `usage_value` | The average, over the last three closed periods, of what each billed above the minimums. An estimate, never part of `renewal_value`. |
| `usage_value_annual` | `usage_value` times the periods in a year. |

`usage_value` is `0.0000` until a period has closed. It is priced at the plan's current meter prices, so treat it as a forecast and not as what was billed.

***

## Ending a subscription

A subscription on a plan that bills meters gets a final usage invoice for the usage up to its end date.

* **End at period end.** The final period closes at the end date, and its usage is invoiced after any grace period.
* **End now.** The usage so far is closed at that moment. Send `invoice_usage_now: true` to raise the invoice at once, and not after the grace period. See [End and restore](/api-reference/guides/end-and-restore#end-now).

Events dated after the end are rejected as `after_subscription_end`. Restoring a subscription drops a closing period that hasn't been invoiced, so nothing is billed twice.

***

## Things to know

<AccordionGroup>
  <Accordion title="Reported usage can't be read back" icon="eye-slash">
    The API takes usage events and doesn't return them. Keep your own record of what you sent. The subscription's `usage_value` and the usage lines on its invoices show what Saasybill billed.
  </Accordion>

  <Accordion title="Meters are read-only in the API" icon="lock">
    You can read a plan's `meters`, but you can't create or edit a meter or its pricing. A plan's meters and prices also lock once any subscription uses the plan.
  </Accordion>

  <Accordion title="Units don't apply to usage" icon="sliders">
    A plan with no recurring fee has no units, and a unit change on it is refused. Usage is reported as events, not set as a total.
  </Accordion>

  <Accordion title="Usage is billed at the rates of the period it was used in" icon="percent">
    With `cpi_enabled`, the rates of a plan's meters rise by CPI at each annual renewal. Usage is billed in arrears, so the year gone by is priced at the rate in force during it. Included units, tier boundaries and meter minimums don't change.
  </Accordion>

  <Accordion title="Test with a sandbox" icon="flask">
    Report events with a test key, then advance the sandbox's clock past the end of the period. See [Testing](/api-reference/testing).
  </Accordion>
</AccordionGroup>


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