Billing API
Send your whole billing model to Faurya — customers, plans, invoices, transactions and subscription lifecycle events.
Billing API
https://api.faurya.com/api/v1/billing
The billing API is how you tell Faurya what you bill, who you bill it to, and what money actually moved. It is the full-model sibling of the payments API: instead of one flat payment event, you send the objects your own billing system already has — customers, plans, invoices with line items, transactions and subscription events — and Faurya builds MRR, subscriber counts, churn, refunds and revenue attribution from them.
Use it when you bill from your own code, from a provider Faurya does not connect to directly, or when you are migrating history out of another analytics tool.
If you have connected your payment provider, you do not need this API for that provider: Faurya already reads its customers, invoices and subscriptions. Sending the same billing data here as well would count it twice.
Which API do I want?
| Payments API | Billing API | |
|---|---|---|
| What you send | One payment | Customers, plans, invoices, transactions, lifecycle events |
| Revenue reports | Yes | Yes |
| MRR, subscribers, churn, plan changes | No | Yes |
| Refunds and chargebacks | No | Yes |
| Invoices and line items | No | Yes |
| Bulk history import | No | Yes |
You can use both on the same site — they share one customer space and are joined by the visitor they link to. A transaction id used in one cannot be reused in the other.
Authentication
Every billing endpoint requires a Faurya API key. It is the same site API key the rest of the public API uses — generate it from your Faurya dashboard — sent as a Bearer token:
Authorization: Bearer YOUR_FAURYA_API_KEY
Content-Type: application/jsonThe key identifies the site the data belongs to, so there is no site id anywhere in a billing request. A missing or unrecognised key is 401 UNAUTHORIZED.
A body that is not sent as application/json is refused with 415, so an empty body is never read as {}.
The objects
Customers
Who pays. The person or company every other object hangs off.
Plans
A billing cadence with a name. Plans carry no price.
Invoices
What was billed, line by line. Subscription lines are what create subscriptions.
Transactions
What money moved: payments, refunds, chargebacks and their reversals.
Subscription events
Cancellations, pauses, scheduled changes and their retractions.
Subscriptions
Read-only: what your invoices and events add up to, and why.
Bulk import
Send a whole history in one job instead of a request per object.
Errors
Every status and error code, and what to do about each.
How the model fits together
There is no create-subscription endpoint. A subscription comes into being because an invoice line says it was in force:
- A customer is who pays.
- A plan says how often a subscription bills — monthly, yearly, every 3 months. It has no price.
- An invoice says what was billed. Each
subscriptionortrialline names a subscription, a plan, a price and the service period it covers. That line is what creates and extends the subscription. - A transaction says what money moved against an invoice. It is what makes the invoice paid — and what turns a billed period into service that counts.
- A subscription event records what an invoice cannot express: a cancellation, a pause, a resumption, a change that takes effect later, or the withdrawal of any of those.
Because objects reference each other, send them in this order:
customers, plans → invoices → transactions → subscription eventsAn invoice is refused with CUSTOMER_NOT_FOUND or PLAN_NOT_FOUND if what it names is not stored yet, and a transaction is refused with INVOICE_NOT_FOUND. (Inside a bulk import you may upload in any order — the import runs them in this order for you.)
Requests are PUTs, keyed by your own id
Every write is a PUT to the object's own id, which you choose:
PUT /api/v1/billing/invoices/inv_1042That id — external_id throughout these docs — is your key in your system. It may be 1–255 characters of A–Z a–z 0–9 _ - . : @. It is compared exactly: never trimmed, and case-sensitive.
This means writes are idempotent. Sending the same body twice is a replay: the second request writes nothing and answers 200. There is no POST to create and PATCH to change — a PUT states the object as it is now.
201 Created— the object did not exist before this request.200 OK— the object existed; it was updated, or the request matched what was stored.
A PUT states the whole object
For invoices, a PUT is the document, not a patch. While an invoice is still open:
- a line item you leave out is removed;
- an optional field you leave out is cleared.
Customers are the exception, because a partial update of contact details is the normal thing to want. For a customer:
| What you send | What happens |
|---|---|
| field absent | the stored value is left alone |
field set to null | the stored value is cleared |
| field set to a value | the stored value is replaced |
Some facts can never change once recorded, because changing them would rewrite history. Those are refused with 409: an invoice's currency, a plan's cadence once anything is priced on it, a transaction's type or amount, and a subscription event. See Errors.
Success responses
Every successful write answers in the same envelope:
{
"object": "invoice",
"external_id": "inv_1042",
"created": true,
"data": {
"external_id": "inv_1042",
"customer_external_id": "cus_42",
"currency": "USD",
"status": "open",
"invoiced_at": "2026-03-01T00:00:00.000Z",
"line_items": [],
"transactions": []
},
"warnings": [],
"request_id": "req_01JQ8Z0Y5N6W3K2M4P7R9T1V3X"
}object— which kind of object this is.external_id— the id in the path.created—trueonly when the object did not exist before.data— the stored object, read back. The same shape aGETreturns.warnings— things worth knowing that did not stop the write. Each has acode, amessageand sometimes afield.request_id— also returned as theX-Request-Idheader, on success and on failure. Quote it to support.
A GET answers with the same envelope without created and warnings.
Warnings
| Code | Meaning |
|---|---|
IGNORED_FIELD | A ChartMogul-compatible field we accept for compatibility and do not store. |
VISITOR_ALREADY_LINKED | That visitor already belongs to another customer. The object was saved; the visitor link was not moved. |
VISITOR_PENDING | The tracker has not recorded that visitor yet. The link completes automatically when it does. |
A warning is never a failure. If a field is genuinely unknown — a typo like tax_ammount — the request is refused with 400 instead, so a mistyped field never silently becomes a default.
Errors
Failures answer with the status, a stable error code you can branch on, a fixed human message, and the request id:
{
"message": "The request has invalid fields.",
"error": "VALIDATION_FAILED",
"errors": [
{
"field": "line_items[2].amount",
"code": "TOO_MANY_DECIMALS",
"message": "USD amounts have at most 2 decimal places"
}
],
"request_id": "req_01JQ8Z0Y5N6W3K2M4P7R9T1V3X"
}Validation reports every problem in one response, each naming the exact path into your body, so one round trip tells you everything to fix. The full catalogue is in Errors.
Conventions
- Amounts are in major units —
24.99for USD,2499for JPY — with no more decimal places than the currency has. They are always positive; the object'stypecarries the direction, so a refund is arefundof10.00, never a payment of-10.00. - Currency is a three-letter ISO 4217 code. Faurya accepts any currency it holds a daily exchange rate for (the European Central Bank reference set:
USD,EUR,GBP,INR,JPY,AUD,CAD,BRLand others). - Timestamps are ISO 8601.
2026-03-01is midnight UTC;2026-03-01T10:00:00is read as UTC, never as your server's zone;2026-03-01T10:00:00+05:30is that instant. Nothing before2000-01-01is accepted. A timestamp for something that happened —invoiced_at,occurred_at,event_date— may not be more than 5 minutes in the future. Service periods and effective dates may be in the future. - Tax is inside the amount you send. A line's
tax_amountis the portion of itsamountthat is tax, and may not exceed it. Recurring value is the amount with its tax removed. - Country codes are ISO 3166-1 alpha-2.
Limits
| Rate limit | 25 requests/second per site, bursting to 100 |
| Request body | 1 MB (5 MB on the bulk import routes) |
| Line items per invoice | 250 |
| Items per bulk upload call | 1,000 |
external_id length | 255 characters |
metadata | 10 entries; keys 1–64 characters; values string (≤500 characters), number, boolean or null |
Over the rate limit you get 429 with a Retry-After header. If you are sending a backlog fast enough to hit it, use the bulk import instead of asking for a higher limit.
Visitor attribution
Customers and invoices both accept faurya_visitor_id and faurya_session_id from the Faurya browser cookies. Sending them links the revenue to the visit that produced it, so your billing data shows up in attribution reports next to the traffic source that earned it.
The visitor does not have to exist yet. A payment recorded moments after a first page view links up as soon as the visit is processed — you get a VISITOR_PENDING warning in the meantime.
Next steps
- Start with Customers, then Plans.
- Send what you bill with Invoices and what you collect with Transactions.
- Migrating a history? Go straight to Bulk import.