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

# Change units

> Keep a subscription's units in step with the seats on your platform

<Info>
  This page covers the API. For the billing logic behind it, see [Billing changes](/core/billing-changes). For the same change in the app, see [Subscriptions](/billing-workspace/subscriptions-workspace).
</Info>

`POST /v1/subscriptions/{id}/units` sets a subscription's unit count. It needs the `subscriptions:write` scope. Send the **new total**, not the difference.

Saasybill decides what the change costs and where to bill it, using the same rules as the Update Units dialog in the app. You choose whether to prorate.

<Note>
  Units are for a plan's recurring fee. A plan with no recurring fee (`no_base_fee`) has no units, and the request is refused. Metered usage isn't set as units. [Report it as events](/api-reference/guides/usage-based-billing) instead.
</Note>

***

## Request fields

| Field | Type | Description |
| - | - | - |
| `units` | integer | Required. The new total number of units. |
| `prorate` | boolean | Charge for the rest of the current period. Defaults to `false`. |
| `proration_behaviour` | string | With `prorate: true`. `charge_immediately` (the default) or `charge_on_renewal`. |
| `effective_date` | string | `YYYY-MM-DD`. Required with `prorate: true`. Must fall within the current billing period. |
| `document` | string | `amend` or `separate`. Used only [after the renewal invoice is created](#after-the-renewal-invoice-is-created). Optional: when you leave it out, the organisation's setting decides. |

`proration_behaviour` and `effective_date` are refused unless `prorate` is `true`.

***

## What happens

Saasybill classifies the new total against the units already paid for this period.

### Increasing units

| You send | What happens | In the response |
| - | - | - |
| `prorate: true`, `charge_immediately` | A prorated invoice is raised now for the extra units, from the effective date to the next renewal. It joins the subscription's draft invoice if there is one. | `change.invoice` is set. |
| `prorate: true`, `charge_on_renewal` | The prorated amount is added as a line to the renewal invoice. | `change.deferred_to_renewal` is `true`. |
| `prorate: false` | The subscription's `units` update straight away, but the extra units are billed in full at the next renewal. No document is raised now. | `change.invoice` is `null`. |

Prorated amounts follow the [proration formula](/core/billing-changes#how-the-prorated-amount-is-calculated): the difference in value, multiplied by the days remaining, divided by the days in the period.

<Warning>
  If you increase units without prorating, proration is unavailable for further increases until the next renewal. A prorated request in that state is refused with `422`.
</Warning>

### Decreasing units

A decrease never raises a document. The subscription's `units` update straight away, but the customer has paid for the period, so the lower amount is billed from the next renewal. Send `prorate: false`, or omit it.

### Volume plans

On a volume plan, a unit increase that crosses into a cheaper band re-prices every unit, and the total can fall. Saasybill raises a prorated **credit note** instead of an invoice.

* `prorate` can't be `false` for this change. It answers `422 proration_required`.
* The credit is always issued immediately, so `proration_behaviour` is fixed to `charge_immediately`.
* The credit note is held as a credit balance on the subscription and applied to future invoices. `change.credit_note` holds its id.

### Units already paid for

Units at or below the number already paid for this period are never charged twice. If a customer drops from 10 to 8 units and then returns to 10, nothing more is billed until renewal.

### When proration is fixed

`proration_behaviour` is fixed to `charge_immediately` when:

* the subscription is in its final period, because there is no renewal to charge at
* the renewal invoice has already been created, because there is no renewal invoice left to add to

***

## After the renewal invoice is created

Saasybill creates a renewal invoice ahead of the renewal date, set by **Invoice Creation Date** in [Company Settings](/settings/account#company-settings). Once that has happened, a unit change touches two periods: the current one, and the next one that's already been invoiced.

Then a document has to carry the change. You choose it per request with `document`, or once for the organisation.

| `document` | What happens |
| - | - |
| `amend` | The renewal invoice is updated. Its plan line is re-priced at the new units, and the current period's prorated charge is added to it. |
| `separate` | The change is billed on its own invoice. The renewal invoice isn't touched. |

Before the renewal invoice exists, `document` is refused. After it exists, `document` is optional.

### Set a default for the organisation

Amending changes a document the customer may already hold, so the default is `separate`. An owner or admin can change it under **Developer Settings**, then **Configuration**. The setting is called **Changes After Invoicing**.

| Setting | What a request without `document` does |
| - | - |
| <Badge>Raise a separate invoice</Badge> | Bills the change on its own. This is the default. |
| <Badge>Amend the renewal invoice</Badge> | Updates the renewal invoice. |

A `document` in the request always wins over the setting.

<Note>
  If the setting is Amend and the renewal invoice can no longer be changed, for example because it has a payment or credit on it, Saasybill raises a separate invoice instead. It doesn't refuse the request. The response says so in `change.document_fallback_reason`. A request that names `amend` itself is refused in that case, and the error says why.
</Note>

A sandbox doesn't copy the setting. Set it again in the sandbox.

```json theme={null}
{
  "units": 15,
  "prorate": true,
  "effective_date": "2026-10-08",
  "document": "amend"
}
```

With `amend`, `change.renewal_invoice` is the updated renewal invoice. With `separate`, `change.invoice` is the new invoice. `change.document` tells you which happened.

***

## The response

The response is the updated [subscription](/api-reference/objects#subscription) with a `change` object.

| `change` field | Description |
| - | - |
| `units` | The new total. |
| `invoice` | The invoice raised or updated by this change, or `null`. |
| `renewal_invoice` | The renewal invoice that was amended, or `null`. |
| `deferred_to_renewal` | `true` when the prorated amount was added to the next renewal invoice. |
| `document` | `amend` or `separate`: where the change went once the renewal invoice exists. `null` before then. |
| `document_source` | `request` when you named `document`, or `organisation_default` when the setting chose it. `null` before the renewal invoice exists. |
| `document_fallback_reason` | Set when the setting is Amend but the change was billed separately. The reason. Otherwise `null`. |
| `credit_note` | The id of the credit note raised, or `null`. |

<Tabs>
  <Tab title="Charged now">
    ```json theme={null}
    {
      "id": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "object": "subscription",
      "units": 15,
      "renewal_value": "162.0000",
      "change": {
        "units": 15,
        "invoice": { "id": "6a9b4d0e-2c37-4f15-8d8a-7e3f1c5b9a01", "number": "INV-0043", "total": "43.1100", "…": "…" },
        "renewal_invoice": null,
        "deferred_to_renewal": false,
        "credit_note": null
      }
    }
    ```
  </Tab>

  <Tab title="Charged at renewal">
    ```json theme={null}
    {
      "id": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "object": "subscription",
      "units": 15,
      "renewal_value": "162.0000",
      "change": {
        "units": 15,
        "invoice": null,
        "renewal_invoice": null,
        "deferred_to_renewal": true,
        "credit_note": null
      }
    }
    ```
  </Tab>

  <Tab title="No document">
    ```json theme={null}
    {
      "id": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "object": "subscription",
      "units": 8,
      "renewal_value": "86.4000",
      "change": {
        "units": 8,
        "invoice": null,
        "renewal_invoice": null,
        "deferred_to_renewal": false,
        "credit_note": null
      }
    }
    ```
  </Tab>
</Tabs>

<Note>
  `renewal_value` in the response is what the next renewal will bill at the new units.
</Note>

***

## Example

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.saasybill.com/v1/subscriptions/9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33/units \
    -H "Authorization: Bearer $SAASYBILL_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: seats_acme_2026-10-08_15" \
    -d '{
      "units": 15,
      "prorate": true,
      "proration_behaviour": "charge_immediately",
      "effective_date": "2026-10-08"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    `https://api.saasybill.com/v1/subscriptions/${subscriptionId}/units`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SAASYBILL_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `seats_${accountId}_${changeId}`,
      },
      body: JSON.stringify({
        units: 15,
        prorate: true,
        proration_behaviour: "charge_immediately",
        effective_date: "2026-10-08",
      }),
    },
  );
  ```

  ```python Python theme={null}
  response = requests.post(
      f"https://api.saasybill.com/v1/subscriptions/{subscription_id}/units",
      headers={
          "Authorization": f"Bearer {os.environ['SAASYBILL_API_KEY']}",
          "Idempotency-Key": f"seats_{account_id}_{change_id}",
      },
      json={
          "units": 15,
          "prorate": True,
          "proration_behaviour": "charge_immediately",
          "effective_date": "2026-10-08",
      },
  )
  ```
</CodeGroup>

<Tip>
  Send an `Idempotency-Key` built from your own change event. The key is optional here, but a retried seat change without one could be applied twice.
</Tip>

***

## When a change is refused

A unit change is refused with `422` when it breaks a rule. See the [unit change codes](/api-reference/errors#unit-changes) for each.

| Rule | Code |
| - | - |
| The organisation is in Setup Mode. | `setup_mode_on` |
| The subscription has ended or completed. | `subscription_not_changeable` |
| The plan is a flat fee plan, which has no units. | `plan_has_no_units` |
| `units` is below the minimum. | `units_below_minimum` |
| `units` matches the current total. | `units_unchanged` |
| `prorate` is `true` without `effective_date`. | `effective_date_required` |
| `effective_date` is outside the current period. | `effective_date_out_of_period` |

<Tip>
  If your platform sends the current total on every sync, treat `units_unchanged` as success. The subscription already has the units you wanted.
</Tip>

A `409 xero_accounts_not_mapped` means an admin needs to map the Xero accounts on the Integrations tab before units can change.


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