Documentation

The Billr API

A small REST API for the two things worth automating: invoices and clients. JSON goes in, JSON comes out, and a webhook tells you the moment an invoice is created, sent or paid.

The API comes with a subscription. Everything on this page is readable without an account.

Start here

Two minutes to your first request

Every endpoint lives under one base URL, speaks JSON, and needs one header. There is no SDK to install and nothing to configure. If you can send an HTTP request, you are ready.

Base URL
https://billr-app.com/api/v1

Create a key on the developers page inside your account, then list your five most recent documents:

curl https://billr-app.com/api/v1/invoices?limit=5 \
  -H "Authorization: Bearer billr_live_YOUR_KEY"

That is the whole learning curve. The rest of this page is reference material for when you need a specific field.

Authentication

One header, one key

Send your key as a bearer token on every request. Keys start with billr_live_ and are tied to one account, so the API never has to be told which organisation you mean.

Authorization: Bearer billr_live_YOUR_KEY

A key is shown to you exactly once, at the moment you create it. Only a hash of it is stored, which means a lost key can be replaced but never recovered. Revoking one takes effect on the next request.

Keep it server side

A key can read and create everything in your account. It belongs in your backend, never in a browser or a mobile app.

Use one key per system

Separate keys for your site, your scripts and your staging environment mean you can revoke one without breaking the others.

Comes with a subscription

Requests from an account without one return 402 with the code plan_required. Nothing else about the request changes.

Conventions

How values are written

Money is never a decimal number. Every amount is an integer, so nothing is ever off by a cent after a currency conversion or a JSON round trip. Three unit conventions follow from that, and they hold everywhere in the API and in every webhook.

Units
…Centsinteger
A whole number of cents. 1250 is 12.50, and a negative value is a credit. Divide by 100 to display it.
…Bpinteger
Basis points. 2100 is a tax rate of 21 percent, 600 is 6 percent, 0 is exempt. Divide by 100 to display it.
…Milliinteger
Thousandths of a unit, so quantities can carry three decimals. 1000 is one item, 2500 is two and a half hours.
Everything else
Dates and timesstring
Returned as ISO 8601 in UTC. When you send one, YYYY-MM-DD is enough and is the format to prefer.
Currencystring
A three letter ISO code such as EUR or USD. Sent in upper case and stored that way.
Idsstring
Opaque strings. Treat them as text, never parse them, and store them as given.
Missing valuesnull
A field with no value is null rather than absent, so the shape of an object never changes between records.

Responses are sent with Cache-Control: no-store. List endpoints wrap their results in an envelope, single records are returned on their own:

{
  "data": [ { … }, { … } ],
  "limit": 50,
  "count": 2
}
Errors

One shape, whatever went wrong

Every failure returns the same envelope, with a stable machine readable code and a message written for whoever has to read the log at two in the morning.

{
  "error": {
    "code": "invalid_body",
    "message": "Every line needs a description (lines.0.description)"
  }
}
Billr API error codes
StatusCodeWhat it means
400invalid_bodyThe JSON was missing a required field or a value was out of range. The message names the field.
400invalid_statusThe status filter was not one of the known statuses. The message lists them all.
401unauthorizedThe key was missing, malformed, unknown or revoked. All four look the same from outside.
402plan_requiredThe account has no subscription, or its monthly allowance for that action is used up.
404not_foundNo record with that id. A record belonging to another account answers the same way, on purpose.

A 401 also carries WWW-Authenticate: Bearer realm="Billr API", so a generic HTTP client knows what to ask for.

Endpoints

Invoices

GET/api/v1/invoices

Lists your invoices and credit notes, newest first.

Query parameters
limitinteger
How many to return. Defaults to 50, never goes above 100. Anything unparseable falls back to the default.
statusstring
Optional filter. One of DRAFT, SENT, VIEWED, PARTIALLY_PAID, PAID, OVERDUE, CANCELLED.
curl "https://billr-app.com/api/v1/invoices?status=PAID&limit=20" \
  -H "Authorization: Bearer billr_live_YOUR_KEY"

POST/api/v1/invoices

Creates a draft and returns it with status 201. The number is assigned for you, in sequence, using the same counter the app uses, so an invoice made through the API sits in the same unbroken series as one made by hand.

Creating is deliberately separate from sending. Nothing is e-mailed to your client until you send it from the app, which means a script with a bug cannot mail a stranger.

Body
linesarray, required
At least one line. Each line is described below.
typestring
INVOICE or CREDIT_NOTE. Defaults to INVOICE.
clientIdstring
The id of a stored client. Fills in the bill-to block and the currency from that client.
billToNamestring
Required unless you pass a clientId. Set it alongside a clientId to override just this once.
billToCompanystring
Company name on the invoice.
billToEmailstring
Where the invoice would be sent from the app.
billToVatstring
The client VAT or tax number.
billToAddressstring
The address block as one string. Line breaks are kept.
issueDatestring
YYYY-MM-DD. Defaults to today.
dueDatestring
YYYY-MM-DD. Defaults to the issue date plus your account payment term.
currencystring
Three letters. Defaults to the client currency, then to your account currency.
discountCentsinteger
A discount on the whole invoice, spread across the lines before tax. Zero or more.
notesstring
The note block under the totals.
termsstring
Your payment terms, printed below the notes.
Each line
descriptionstring, required
What you are charging for.
unitPriceCentsinteger, required
Price of one unit, in cents. Negative is allowed for a correction line.
quantityMilliinteger
Quantity in thousandths. Defaults to 1000, which is one.
unitstring
What one unit is called: hour, day, item. Defaults to item.
taxRateBpinteger
Tax on this line in basis points. Defaults to your account tax rate.
curl -X POST https://billr-app.com/api/v1/invoices \
  -H "Authorization: Bearer billr_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "cl_example",
    "dueDate": "2026-09-30",
    "lines": [
      {
        "description": "Design work, September",
        "quantityMilli": 12500,
        "unit": "hour",
        "unitPriceCents": 8500,
        "taxRateBp": 2100
      }
    ]
  }'

Totals are computed for you. A line amount is the quantity times the unit price, tax is summed once per rate rather than once per line, and any discount is spread across the lines in proportion so the printed tax base always reconciles with the total charged.

GET/api/v1/invoices/{id}

Returns one invoice with its lines. An id that belongs to another account answers 404, exactly as one that does not exist, so the API never confirms that a record is out there somewhere.

Endpoints

Clients

GET/api/v1/clients

Lists your clients, sorted by name.

Query parameters
limitinteger
How many to return. Defaults to 50, never goes above 100.
archivedstring
Pass 1 to list archived clients instead of active ones.

POST/api/v1/clients

Creates a client and returns it with status 201. Only a name is required, so a client captured from a form with one field is still a valid client.

Body
namestring, required
The person or business you invoice.
companystring
Company name, if it differs from the name.
emailstring
Where invoices are sent.
phonestring
Contact number.
vatNumberstring
VAT or tax number.
addressL1string
Street and number.
addressL2string
A second address line.
postalCodestring
Postal or zip code.
citystring
City.
countrystring
Country.
currencystring
Three letters. Used as the default on invoices for this client.
notesstring
Private notes. Never printed on an invoice.
curl -X POST https://billr-app.com/api/v1/clients \
  -H "Authorization: Bearer billr_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Amelie Janssens",
    "company": "Studio Noord",
    "email": "[email protected]",
    "city": "Antwerp",
    "country": "Belgium",
    "currency": "EUR"
  }'
Reference

What comes back

There is one definition of an invoice as JSON and one of a client, shared by the endpoints and the webhooks. Anything that can read a webhook can read the API, and the other way round.

The invoice object

{
  "id": "inv_example",
  "number": "2026-0042",
  "type": "INVOICE",
  "status": "SENT",
  "clientId": "cl_example",
  "billTo": {
    "name": "Amelie Janssens",
    "company": "Studio Noord",
    "email": "[email protected]",
    "vatNumber": "BE0123456789",
    "address": "Kloosterstraat 12\n2000 Antwerp"
  },
  "issueDate": "2026-09-01T00:00:00.000Z",
  "dueDate": "2026-09-30T00:00:00.000Z",
  "currency": "EUR",
  "subtotalCents": 106250,
  "discountCents": 0,
  "taxCents": 22313,
  "totalCents": 128563,
  "paidCents": 0,
  "notes": null,
  "terms": "Payment within 30 days.",
  "publicUrl": "https://billr-app.com/i/…",
  "sentAt": "2026-09-01T09:12:44.201Z",
  "viewedAt": null,
  "paidAt": null,
  "createdAt": "2026-09-01T09:11:02.884Z",
  "updatedAt": "2026-09-01T09:12:44.204Z",
  "lines": [
    {
      "id": "ln_example",
      "position": 0,
      "description": "Design work, September",
      "quantityMilli": 12500,
      "unit": "hour",
      "unitPriceCents": 8500,
      "taxRateBp": 2100,
      "amountCents": 106250
    }
  ]
}
Worth knowing
statusstring
DRAFT, SENT, VIEWED, PARTIALLY_PAID, PAID, OVERDUE or CANCELLED.
paidCentsinteger
How much has been recorded against the invoice so far. Compare it with totalCents rather than reading status alone.
publicUrlstring
The link your client opens to view and download the invoice. Safe to send, and it needs no account.
viewedAtstring or null
The first time that link was opened. Null until it is.
linesarray
Always in the order they appear on the document, by position.

The client object

{
  "id": "cl_example",
  "name": "Amelie Janssens",
  "company": "Studio Noord",
  "email": "[email protected]",
  "phone": null,
  "vatNumber": "BE0123456789",
  "address": {
    "line1": "Kloosterstraat 12",
    "line2": null,
    "postalCode": "2000",
    "city": "Antwerp",
    "country": "Belgium"
  },
  "notes": null,
  "currency": "EUR",
  "archived": false,
  "portalUrl": "https://billr-app.com/p/…",
  "createdAt": "2026-08-14T15:02:19.771Z",
  "updatedAt": "2026-08-14T15:02:19.771Z"
}

portalUrl is the client portal: one page holding every invoice you ever sent that client, which you can share once instead of re-sending links.

Webhooks

Be told, instead of asking

Add an endpoint on the developers page, choose the events you care about, and Billr posts JSON to your URL as things happen. Nothing has to poll.

Events
invoice.createdevent
An invoice or credit note was created, whether in the app or through the API.
invoice.sentevent
An invoice was e-mailed to a client.
invoice.paidevent
An invoice was paid in full.

The body is the same every time. data holds the invoice object described above, so you rarely need a second call to find out what happened.

{
  "id": "4e1c2f8a-…",
  "event": "invoice.paid",
  "createdAt": "2026-09-14T11:04:52.317Z",
  "data": { … the invoice object … }
}
Headers on every delivery
X-Billr-Eventstring
The event name, so you can route before parsing.
X-Billr-Signaturestring
Hex HMAC-SHA256 of the raw body, keyed with that endpoint signing secret.
X-Billr-Timestampstring
Unix seconds at the moment we sent it.
User-Agentstring
Billr-Webhooks/1.0

Verifying a delivery

Sign the raw bytes you received, before any JSON parsing, and compare in constant time. Reformatting the body first will change the signature.

import crypto from "node:crypto";

export function isFromBillr(rawBody, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signature, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Answer first, work afterwards

We wait five seconds for your reply and send each delivery once. Reply 200 straight away and queue the real work on your side. Every delivery carries its own id, so if you ever see one twice it is easy to recognise. The status of the last attempt, including any error, is shown next to the endpoint on your developers page.

Honest notes

What is not there yet

A short list, kept accurate on purpose. Knowing the edges up front is worth more than finding them in production.

  • Invoices and clients can be listed and created through the API. Updating and deleting stay in the app for now.
  • Sending an invoice by e-mail is an action you take in the app, so a script can never mail a client by accident.
  • A webhook is delivered once, with a five second timeout and no retry queue. The last status is recorded on the endpoint so a failure is visible rather than silent.
  • There is no rate limit today. Keep your request rate sensible, and if a limit is ever added it will be announced before it takes effect.
  • The API is versioned in the path. A change that would break existing code lands on a new version rather than on v1.

Missing something you need? Say so and it goes on the list. The API grows in the direction people actually push it.

Wire it up this afternoon.

Start free, see whether Billr makes the invoice you want, and turn on the API when your own system needs to do the asking.