> ## 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. 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. |
| `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://app.saasybill.com/api/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://app.saasybill.com/api/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://app.saasybill.com/api/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.

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

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