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}/statusRequires 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 beforeinvoiced_at.invoice_number(string): Your human-facing number, if it differs from the id. At most 255 characters.invoice_url(string): Anhttps://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_timeortrial.amount(number, required): The line total, tax included, in the invoice's currency. May be0. It is not multiplied byquantity— send the total you billed.quantity(integer): Seats, units, licences. Defaults to1. Recorded for reporting; it never multipliesamount.discount_amount(number): Discount already reflected inamount. Defaults to0.tax_amount(number): The portion ofamountthat is tax. Defaults to0and may not exceedamount. Recurring value is computed fromamountminus 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 tofalse.proration_type(string):differential(the default whenproratedis true),full,differential_mrrordifferential_always. Only allowed whenproratedistrue.
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 asubscription_external_id.type: "usage_based_recurring"— send usage charges asone_timelines. 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:
| Status | When |
|---|---|
open | No succeeded payment yet |
partially_paid | Succeeded payments total less than the invoice |
paid | Succeeded payments cover the invoice |
partially_refunded | Some of what was collected has been given back |
refunded | All of it has been given back |
voided | You voided it — see below |
written_off | You 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 it | What this period is |
|---|---|
| nothing, or the subscription had ended | a start |
| a different plan | a plan change |
| the same plan at a different price or quantity | an update |
| the same plan, price and quantity | a 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.errorsnames every one with its path, for exampleline_items[2].amount.401 Unauthorized— missing or invalid API key.404 Not Found— (GET, andPUT .../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_FOUNDorCURRENCY_NOT_SUPPORTED: the request is well-formed but names something that is not there.429 Too Many Requests— over the rate limit.