API ReferenceBilling

Invoices

Send what you billed, line by line. Subscription lines are what create and extend subscriptions.

Invoices

PUT https://api.faurya.com/api/v1/billing/invoices/{external_id}
GET https://api.faurya.com/api/v1/billing/invoices/{external_id}
PUT https://api.faurya.com/api/v1/billing/invoices/{external_id}/status

Requires Bearer Token authentication with your Faurya API key.

An invoice says what was billed. It is the centre of the billing API: its line items carry the prices, and its subscription and trial lines are what bring subscriptions into being.

There is no create-subscription endpoint. You never tell Faurya "this subscription exists" — you bill for a period, and the subscription follows from the bill.

An invoice says what was billed, not what was collected. Record the money separately with transactions.

Create or update an invoice

const response = await fetch(
  "https://api.faurya.com/api/v1/billing/invoices/inv_1042",
  {
    method: "PUT",
    headers: {
      Authorization: `Bearer ${process.env.FAURYA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      customer_external_id: "cus_42",
      currency: "USD",
      invoiced_at: "2026-03-01T00:00:00Z",
      due_at: "2026-03-15T00:00:00Z",
      invoice_number: "INV-2026-0042",
      invoice_url: "https://billing.example.com/invoices/1042",
      faurya_visitor_id: req.cookies.faurya_visitor_id,
      line_items: [
        {
          external_id: "li_1",
          type: "subscription",
          subscription_external_id: "sub_900",
          plan_external_id: "plan_pro_monthly",
          amount: 49,
          quantity: 1,
          tax_amount: 0,
          service_period_start: "2026-03-01T00:00:00Z",
          service_period_end: "2026-04-01T00:00:00Z",
        },
        {
          external_id: "li_2",
          type: "one_time",
          amount: 150,
        },
      ],
    }),
  },
);

Invoice fields

Required

  • customer_external_id (string): The customer being billed. Must already exist.
  • currency (string): ISO 4217 code. Every amount on the invoice, and every transaction against it, is in this currency.
  • invoiced_at (string): When the invoice was issued, ISO 8601. May not be more than 5 minutes in the future.
  • line_items (array): 1 to 250 lines. See below.

Optional

  • due_at (string): When payment is due. Must not be before invoiced_at.
  • invoice_number (string): Your human-facing number, if it differs from the id. At most 255 characters.
  • invoice_url (string): An https:// link to the invoice in your system. At most 2048 characters.
  • tax_amount (number): Document-level tax. This does not add to the total — it restates the lines' tax so Faurya can flag a disagreement between the two.
  • faurya_visitor_id / faurya_session_id (string): The visit this revenue should be attributed to. See Customers for how linking behaves.

collection_method is accepted for ChartMogul compatibility and dropped, with an IGNORED_FIELD warning.

Line item fields

Every line

  • external_id (string, required): Your id for this line. Must be unique within the invoice.
  • type (string, required): subscription, one_time or trial.
  • amount (number, required): The line total, tax included, in the invoice's currency. May be 0. It is not multiplied by quantity — send the total you billed.
  • quantity (integer): Seats, units, licences. Defaults to 1. Recorded for reporting; it never multiplies amount.
  • discount_amount (number): Discount already reflected in amount. Defaults to 0.
  • tax_amount (number): The portion of amount that is tax. Defaults to 0 and may not exceed amount. Recurring value is computed from amount minus this.
  • event_order (integer): A tie-breaker when two lines take effect at the same instant and you know which came first.

subscription and trial lines

These lines are what create and extend a subscription, so they must say what they bought:

  • subscription_external_id (string, required): Your id for the subscription. The first line naming it brings it into being.
  • plan_external_id (string, required): The plan in force for this period. Must already exist.
  • service_period_start (string, required): When the period begins. May be in the future.
  • service_period_end (string, required): When it ends. Must be after the start.
  • prorated (boolean): Whether this is a part-period charge. Defaults to false.
  • proration_type (string): differential (the default when prorated is true), full, differential_mrr or differential_always. Only allowed when prorated is true.

service_period_end may be omitted on one case only: a subscription line whose amount is 0. There Faurya derives the end from the plan's cadence, because nothing was charged for the period, and the response marks the line service_period_derived: true. A line you charged for must say what it bought.

one_time lines

A one_time line is a setup fee, an add-on, a one-off purchase. It may not carry subscription_external_id, plan_external_id, service_period_start or service_period_end — sending any of them is a 400. It counts as revenue, never as MRR.

These line fields are accepted for ChartMogul compatibility and dropped with an IGNORED_FIELD warning: description, discount_code, discount_description, account_code, balance_transfer, cancelled_at.

Two ChartMogul concepts are refused rather than dropped, because silently dropping them would lose MRR you believe you sent:

  • subscription_set_external_id — send the bundle as one subscription: several line items sharing a subscription_external_id.
  • type: "usage_based_recurring" — send usage charges as one_time lines. They count as revenue but not as MRR.

A PUT states the whole document

While the invoice is still open, a PUT replaces it:

  • a line you leave out is removed, and the subscription period it claimed is withdrawn;
  • an optional field you leave out is cleared.

So to correct an invoice, send it again in full — not just the part that changed.

Once the invoice is no longer open

The moment any payment, refund, void or write-off touches the invoice, its billed content is frozen. After that only due_at, invoice_number and invoice_url may change; anything else is refused with 409 INVOICE_LOCKED:

{
  "message": "This invoice has a payment, refund, void or write-off and can no longer be changed. Void it and send a new invoice.",
  "error": "INVOICE_LOCKED",
  "request_id": "req_01JQ8Z0Y5N6W3K2M4P7R9T1V3X"
}

To fix a paid invoice, void it and send a corrected one.

An invoice's currency can never change once the invoice exists, even while it is open — that is 409 INVOICE_CURRENCY_LOCKED.

Status is calculated, not sent

You do not set an invoice's status. Faurya computes it from the transactions recorded against it:

StatusWhen
openNo succeeded payment yet
partially_paidSucceeded payments total less than the invoice
paidSucceeded payments cover the invoice
partially_refundedSome of what was collected has been given back
refundedAll of it has been given back
voidedYou voided it — see below
written_offYou wrote it off — see below

Failed and pending transactions move nothing: a failed charge is dunning and a pending one has not settled, so neither changes the status.

Terminal statuses

PUT /api/v1/billing/invoices/{external_id}/status
{ "status": "voided" }

status is the only field, and the only two values are:

  • voided — the document never counted. Its subscription periods are withdrawn, its cash stops counting, and it drops out of every report.
  • written_off — the document stays billed; you are recording that no cash will ever come for it.

Both are final. Re-sending the same one is a replay and answers 200. Asking for the other one, or for either on an invoice that already has one, is 409 INVOICE_LOCKED. The response is the full invoice, as a GET returns it.

A voided invoice also refuses new transactions, with 409 INVOICE_VOIDED.

What a subscription line actually does

A subscription line claims that a subscription was in force, on a plan, at a price, for a period. Faurya walks every line and event for that subscription in order and works out what each period means:

The period before itWhat this period is
nothing, or the subscription had endeda start
a different plana plan change
the same plan at a different price or quantityan update
the same plan, price and quantitya renewal

Because of this you can send invoices in any order — a back-dated invoice re-derives whatever came after it.

A bill is not proof of service. An unpaid first invoice is a checkout that may never complete, so a subscription's first period stays pending — it does not count as a subscriber — until something proves service commenced: a collected payment, a trial line, a period with no positive charge, or a subscription_start event. Later renewals stay active whether or not they are paid yet; that is dunning, not a subscription that never started.

If a subscription is not counting and you expected it to, GET /billing/subscriptions/{id} tells you exactly why.

Read an invoice

const response = await fetch(
  "https://api.faurya.com/api/v1/billing/invoices/inv_1042",
  { headers: { Authorization: `Bearer ${process.env.FAURYA_API_KEY}` } },
);

The invoice comes back with its lines and every transaction recorded against it, oldest first:

{
  "object": "invoice",
  "external_id": "inv_1042",
  "data": {
    "external_id": "inv_1042",
    "customer_external_id": "cus_42",
    "currency": "USD",
    "status": "paid",
    "invoiced_at": "2026-03-01T00:00:00.000Z",
    "due_at": "2026-03-15T00:00:00.000Z",
    "invoice_number": "INV-2026-0042",
    "invoice_url": "https://billing.example.com/invoices/1042",
    "tax_amount": null,
    "line_items": [
      {
        "external_id": "li_1",
        "type": "subscription",
        "amount": 49,
        "quantity": 1,
        "discount_amount": 0,
        "tax_amount": 0,
        "subscription_external_id": "sub_900",
        "plan_external_id": "plan_pro_monthly",
        "service_period_start": "2026-03-01T00:00:00.000Z",
        "service_period_end": "2026-04-01T00:00:00.000Z",
        "service_period_derived": false,
        "prorated": false,
        "proration_type": null,
        "event_order": null
      }
    ],
    "transactions": [
      {
        "external_id": "txn_7781",
        "invoice_external_id": "inv_1042",
        "type": "payment",
        "result": "succeeded",
        "amount": 49,
        "currency": "USD",
        "occurred_at": "2026-03-01T09:14:00.000Z",
        "original_transaction_external_id": null,
        "fee": 1.72,
        "fee_currency": "USD",
        "settlement_amount": null,
        "settlement_currency": null,
        "is_partial_refund": null,
        "metadata": null
      }
    ]
  },
  "request_id": "req_01JQ8Z0Y5N6W3K2M4P7R9T1V3X"
}

Response codes

  • 200 OK — invoice updated, or the request matched what was stored.
  • 201 Created — the invoice did not exist before.
  • 400 Bad Request — invalid fields, an unknown field, or a field not allowed for the line type. errors names every one with its path, for example line_items[2].amount.
  • 401 Unauthorized — missing or invalid API key.
  • 404 Not Found — (GET, and PUT .../status) no invoice with that id.
  • 409 Conflict — INVOICE_LOCKED, INVOICE_CURRENCY_LOCKED, SUBSCRIPTION_BELONGS_TO_ANOTHER_CUSTOMER, or a concurrent write.
  • 422 Unprocessable Entity — CUSTOMER_NOT_FOUND, PLAN_NOT_FOUND or CURRENCY_NOT_SUPPORTED: the request is well-formed but names something that is not there.
  • 429 Too Many Requests — over the rate limit.