API ReferenceBilling

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 APIBilling API
What you sendOne paymentCustomers, plans, invoices, transactions, lifecycle events
Revenue reportsYesYes
MRR, subscribers, churn, plan changesNoYes
Refunds and chargebacksNoYes
Invoices and line itemsNoYes
Bulk history importNoYes

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/json

The 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

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:

  1. A customer is who pays.
  2. A plan says how often a subscription bills — monthly, yearly, every 3 months. It has no price.
  3. An invoice says what was billed. Each subscription or trial line names a subscription, a plan, a price and the service period it covers. That line is what creates and extends the subscription.
  4. 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.
  5. 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 events

An 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_1042

That 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 sendWhat happens
field absentthe stored value is left alone
field set to nullthe stored value is cleared
field set to a valuethe 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 — true only when the object did not exist before.
  • data — the stored object, read back. The same shape a GET returns.
  • warnings — things worth knowing that did not stop the write. Each has a code, a message and sometimes a field.
  • request_id — also returned as the X-Request-Id header, on success and on failure. Quote it to support.

A GET answers with the same envelope without created and warnings.

Warnings

CodeMeaning
IGNORED_FIELDA ChartMogul-compatible field we accept for compatibility and do not store.
VISITOR_ALREADY_LINKEDThat visitor already belongs to another customer. The object was saved; the visitor link was not moved.
VISITOR_PENDINGThe 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.99 for USD, 2499 for JPY — with no more decimal places than the currency has. They are always positive; the object's type carries the direction, so a refund is a refund of 10.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, BRL and others).
  • Timestamps are ISO 8601. 2026-03-01 is midnight UTC; 2026-03-01T10:00:00 is read as UTC, never as your server's zone; 2026-03-01T10:00:00+05:30 is that instant. Nothing before 2000-01-01 is 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_amount is the portion of its amount that 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 limit25 requests/second per site, bursting to 100
Request body1 MB (5 MB on the bulk import routes)
Line items per invoice250
Items per bulk upload call1,000
external_id length255 characters
metadata10 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