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

# Create a subscription

> Create a subscription and its first invoice from a signup on your platform

<Info>
  This page covers the API. To create a subscription by hand, see [Subscriptions](/billing-workspace/subscriptions-workspace). The API applies the same rules as the New Subscription form.
</Info>

`POST /v1/subscriptions` creates the subscription and its first invoice, and sends the invoice to Xero. It needs the `subscriptions:write` scope and an [`Idempotency-Key`](/api-reference/idempotency).

***

## Choose the customer and the plan

Identify each by id or by code. Send exactly one of each pair.

| Object | By id | By code |
| - | - | - |
| Customer | `customer` | `customer_code` |
| Plan | `plan` | `plan_code` |

Codes are your own references and are unique in the organisation. They mean you don't have to store Saasybill ids. If a customer doesn't exist yet, see [Customers and Xero](/api-reference/guides/customers-and-xero).

***

## Request fields

| Field | Type | Description |
| - | - | - |
| `customer` or `customer_code` | string | The customer. Exactly one. |
| `plan` or `plan_code` | string | The plan. Exactly one. |
| `create_customer` | object | With `customer_code`: create the customer in Xero if no customer has that code. Takes `name` and optional `email`. |
| `units` | integer | The starting units. Required on every plan except a flat fee plan and a plan with no recurring fee. Must be at least the minimum. |
| `flat_fee_price` | string or number | The price, on a flat fee plan that allows its price to be overridden. Required there, ignored elsewhere. |
| `start_date` | string | `YYYY-MM-DD`. Defaults to today in the organisation's timezone. |
| `end_after_cycles` | integer or null | End after this many billing cycles, from 1 to 1,000. `null` or omitted means ongoing. |
| `discount_percent` | string or number | From 0 to 100. Defaults to 0. |
| `label` | string or null | A label for you. Up to 255 characters. |
| `invoice_reference` | string or null | Printed on each invoice, such as a purchase order number. Up to 255 characters. |
| `minimum_units` | integer or null | Raise the plan's minimum for this subscription. It can't go below the plan's. |
| `minimum_invoice_amount` | string or number or null | Overrides the organisation's minimum before an invoice moves to Awaiting Approval. |
| `invoice_due_days` | integer or null | Days until each invoice is due, from 1 to 365. |
| `meter_minimums` | array | For a plan that bills meters: raise the least a period bills for a meter, for this subscription only. See [Meter minimums](/api-reference/guides/usage-based-billing#meter-minimums). |
| `cpi_enabled` | boolean | `true` to raise the plan price, and the rates of any meters the plan bills, by CPI at each renewal. Annual plans only. Defaults to `false`. See [CPI price increases](#cpi-price-increases). |
| `charges` | array | Up to 50 one-off charges. See [Add charges](#add-charges). |

<Note>
  Money and percentages accept up to two decimal places, as a string or a number.
</Note>

***

## Examples

### A per unit plan

The usual signup: find the customer and plan by your own codes.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.saasybill.com/v1/subscriptions \
    -H "Authorization: Bearer $SAASYBILL_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: signup_8f3a2c1d" \
    -d '{
      "customer_code": "ACME-001",
      "plan_code": "TEAM-MONTHLY",
      "units": 10,
      "start_date": "2026-09-30",
      "discount_percent": "10",
      "label": "Acme – Team plan",
      "invoice_reference": "PO-4471"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.saasybill.com/v1/subscriptions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAASYBILL_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": "signup_8f3a2c1d",
    },
    body: JSON.stringify({
      customer_code: "ACME-001",
      plan_code: "TEAM-MONTHLY",
      units: 10,
      start_date: "2026-09-30",
      discount_percent: "10",
      label: "Acme – Team plan",
      invoice_reference: "PO-4471",
    }),
  });

  const subscription = await response.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.post(
      "https://api.saasybill.com/v1/subscriptions",
      headers={
          "Authorization": f"Bearer {os.environ['SAASYBILL_API_KEY']}",
          "Idempotency-Key": "signup_8f3a2c1d",
      },
      json={
          "customer_code": "ACME-001",
          "plan_code": "TEAM-MONTHLY",
          "units": 10,
          "start_date": "2026-09-30",
          "discount_percent": "10",
          "label": "Acme – Team plan",
          "invoice_reference": "PO-4471",
      },
  )

  subscription = response.json()
  ```
</CodeGroup>

### A flat fee plan with a fixed term

A flat fee plan takes no `units`. Here the plan allows its price to be overridden, and the subscription ends after 12 cycles.

```json theme={null}
{
  "customer": "3b9f1c1e-6a52-4c0e-9d7e-1f6f2f0b7a11",
  "plan": "2f6b9d3a-8c14-4e75-a0b2-6d1e7f3c9a48",
  "flat_fee_price": "2400.00",
  "end_after_cycles": 12,
  "invoice_due_days": 14
}
```

### Add charges

A charge is a one-off fee that goes on the first invoice and doesn't recur. Send charges by id, from [the charges list](/api-reference/charges/list-charges).

```json theme={null}
{
  "customer_code": "ACME-001",
  "plan_code": "TEAM-MONTHLY",
  "units": 25,
  "discount_percent": "10",
  "charges": [
    { "charge": "e2a7d5c8-91b3-4d16-a0f4-7c8b6e5d3a22", "apply_discount": false },
    { "charge": "a8c4e1f7-3d92-4b05-8e6a-1c7f9d2b5e30", "price": "300.00", "apply_discount": true }
  ]
}
```

| Charge field | Description |
| - | - |
| `charge` | The charge id. Required. Each charge can be added once. |
| `price` | Overrides the charge's price. Only where the charge allows it. |
| `apply_discount` | `true` to apply the subscription's discount to this charge. Charges are not discounted unless you say so. |

Charges must be active and in the plan's currency.

### CPI price increases

Set `cpi_enabled` to `true` and the plan price rises by the consumer price index at each annual renewal. Saasybill measures the change in CPI over the billing interval, applies it to the plan price, and notes the percentage on the renewal invoice line. Each increase builds on the last. On a plan that bills meters, the meter rates rise by the same factor.

```json theme={null}
{
  "customer_code": "ACME-001",
  "plan_code": "TEAM-ANNUAL",
  "units": 10,
  "cpi_enabled": true
}
```

The response carries `cpi_enabled: true`, and so does every later read of the subscription.

| Rule | Detail |
| - | - |
| Annual plans only | The plan's interval must be one or more years. |
| Organisation switch | An Owner or Admin must turn on CPI price adjustments in Organisation Settings. It is off by default. |
| A figure must exist | The organisation's Xero country needs a CPI figure, or a manual rate must be set in Organisation Settings. |
| Plan and meter rates only | Add-ons and charges are not increased. Meter rates rise with the plan price. Included units, tier boundaries and meter minimums are quantities, not prices, and don't change. |
| Limits | The minimum and maximum increase are set once for the organisation. A subscription can't set its own. |
| After creation | The API can't switch CPI on or off for an existing subscription. Use <Badge>Edit details</Badge> on the subscription in Saasybill. |

<Note>
  If the first renewal invoice is raised as the subscription is created, which happens when the renewal date is within the organisation's Invoice Creation Date, that invoice already includes the increase.
</Note>

<Info>
  A renewal never waits for CPI data. If no figure is available for the organisation's country, the renewal is held until an Owner or Admin sets a manual rate or turns CPI off for the subscription.
</Info>

### Start in the future

Set `start_date` to a later date and the subscription is created as `pending`. It goes live on that date.

The earliest `start_date` is one billing period back, plus a day. In Setup Mode the date is the previous renewal date of a subscription that is already running, so it can't be in the future.

***

## The response

The response is the [subscription](/api-reference/objects#subscription) with the first invoice attached as `invoice`.

```json theme={null}
{
  "id": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
  "object": "subscription",
  "status": "active",
  "units": 10,
  "renewal_date": "2026-10-30",
  "renewal_value": "108.0000",
  "invoice": {
    "id": "5f8a3c9d-7e21-4b04-9c6d-0a2b4e8f1d55",
    "object": "invoice",
    "number": "INV-0042",
    "status": "awaiting_approval",
    "total": "613.8000",
    "xero": {
      "sync_status": "synced",
      "invoice_id": "f0e9d8c7-b6a5-4f43-9210-fedcba987654",
      "sync_error": null
    }
  }
}
```

A new subscription answers `201`. Save the subscription's `id` against your own customer record so you can change its units later.

### Check the invoice

Creating a subscription and pushing its invoice to Xero are two steps. The second can fail without undoing the first, so always read `invoice.xero.sync_status`.

| `sync_status` | What it means | What to do |
| - | - | - |
| <Badge color="green">synced</Badge> | The invoice is in Xero. | Nothing. |
| <Badge color="yellow">pending</Badge> | The invoice hasn't reached Xero yet. | Check again shortly, or wait for the `invoice.updated` webhook. |
| <Badge color="red">failed</Badge> | Xero refused the invoice. `sync_error` says why. | The subscription and invoice exist in Saasybill. Resolve the cause in Saasybill. Don't create the subscription again. |

A failed sync is still a `201`. Sending the same request with the same `Idempotency-Key` returns this same response and doesn't raise a second invoice.

<Note>
  In [Setup Mode](/settings/account) no invoice is raised, so `invoice` is `null`. It is also `null` for a plan with no recurring fee sold with no charges, because there is nothing to bill for its first period. Its usage is billed when the period ends.
</Note>

***

## Validation errors

A field that breaks a rule answers `422`, and `param` names the field.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_parameter",
    "message": "This subscription needs at least 5.",
    "param": "units",
    "request_id": "req_4f1a9c2e7b8d3a605e1c9f02"
  }
}
```

| Cause | `param` |
| - | - |
| Both or neither of `customer` and `customer_code`, or of `plan` and `plan_code` | The first of the pair |
| `create_customer` without `customer_code` | `create_customer` |
| The customer is inactive | `customer` |
| The plan is inactive or legacy. Only active and draft plans can be sold. | `plan` |
| `units` below the minimum, or above what the plan prices | `units` |
| `flat_fee_price` missing where the plan needs one | `flat_fee_price` |
| `start_date` too early, or in the future in Setup Mode | `start_date` |
| `discount_percent` outside 0 to 100 | `discount_percent` |
| A charge repeated, in another currency, or missing a needed price | `charges` |
| `cpi_enabled` on a plan that isn't annual, while CPI is switched off for the organisation, or with no CPI figure or manual rate for its country | `cpi_enabled` |

Other refusals aren't about a single field:

| Status | Code | Cause |
| - | - | - |
| `404` | `customer_not_found` | No customer has that id or code. |
| `404` | `plan_not_found` | No plan has that id or code. |
| `409` | `xero_accounts_not_mapped` | An admin needs to map the Xero accounts on the Integrations tab. |
| `403` | `plan_limit_reached` | The organisation's Saasybill plan doesn't allow more subscriptions. |


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