API & webhooks
Settings → Developer is where you wire weBiller into something else — a shop, a spreadsheet script, an accounting tool you already run.
Two halves, and they point in opposite directions:
- API keys let your code call weBiller.
- Webhooks let weBiller call your code when something happens.
This page is owner-only. Members and accountants don’t see it — and an API key can’t reach it either, so a leaked key can never mint another one.
API keys

Create a key
New API key, then give it a name you’ll recognise in six months (“Warehouse sync” beats “test”).
Choose the access level
Read only can fetch data and change nothing. Read and write can also create and edit invoices, clients, and payments.
Set an expiry (optional)
Leave it empty for a key that never expires.
Copy it now
The full key is shown once. Store it in whatever your code reads secrets from.
weBiller keeps only a hash of your key, so there is no “show it again” button and support can’t recover it. Lose it and you create a new one.
Using a key
Send it as a bearer token, or in X-API-Key — whichever your HTTP client makes easier.
curl https://api.webiller.com/api/v1/invoices \
-H "Authorization: Bearer wb_live_7f3a9c21_..."A key belongs to one business — the one that was active when you created it — and stays there no matter which business you switch to afterwards.
What a key can’t do
Keys are for your business’s data, not its administration. These stay owner-only and are refused with a 403:
- Team members and roles
- Business profile, logo, and invoice template
- API keys and webhooks themselves
Rate limit
120 requests per minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; go over and you get a 429 with Retry-After.
Revoking
Revoke stops a key working immediately — do this the moment one might have leaked. The row stays in the list (greyed out) so you can still tell what it was; Delete removes it once you no longer need the record.
Webhooks
Instead of polling weBiller for changes, let it call you.

Add an endpoint
New webhook, then paste the https URL weBiller should POST to.
Pick your events
Tick the ones you care about. Subscribing to everything and filtering on your side works, but it’s more traffic than you need.
Verify the first delivery
Send test posts a ping and tells you what your endpoint answered. Check the signature before you trust anything else.
Events
| Event | Fires when |
|---|---|
invoice.created | An invoice is created (including from a subscription) |
invoice.sent | A draft is sent — in-app or by email. Not on a resend |
invoice.paid | The last of the balance is settled |
invoice.overdue | An invoice passes its due date |
invoice.cancelled | An invoice is cancelled |
payment.recorded | Any payment is recorded, full or partial |
quote.sent | A quote is sent |
quote.accepted | A client accepts — in-app or via a shared link |
quote.declined | A client declines |
client.created | A client is added |
payment.recorded and invoice.paid both fire on a payment that settles an invoice. Take invoice.paid if you only care about the invoice closing; take payment.recorded if you’re reconciling every part-payment.
The payload
{
"id": "3f9c…",
"event": "invoice.paid",
"businessId": "b1a2…",
"createdAt": "2026-07-28T09:14:02.117Z",
"data": {
"payment": { "id": "p_…", "amount": 120000, "currency": "EUR", "method": "BANK_TRANSFER", "date": "2026-07-28T00:00:00.000Z" },
"invoice": { "id": "i_…", "number": "INV-014", "status": "PAID", "total": 120000, "amountDue": 0, "client": { "id": "c_…", "name": "Ada Lovelace" } }
}
}All money is in minor units (cents) in the document’s own currency — 120000 is €1,200.00. Fields get added over time; they’re never removed or retyped, so parse leniently.
Verifying a delivery
Every request carries X-Webiller-Signature: t=<unix seconds>,v1=<hex>, where v1 is an HMAC-SHA256 of <t>.<raw body> using your webhook’s signing secret.
Check it on the raw body, before any JSON parsing — re-serialising changes the bytes and the signature won’t match.
import { createHmac, timingSafeEqual } from 'crypto'
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
// Reject anything older than 5 minutes — the timestamp is signed, so a
// replayed request can't have its `t` swapped for a fresh one.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
return parts.v1.length === expected.length &&
timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
}Alongside it you also get X-Webiller-Event, X-Webiller-Delivery, and X-Webiller-Attempt.
Rotate secret issues a new one immediately. Deliveries already in flight were signed with the old secret, so update your side promptly — or accept both for a few minutes during the switch.
Retries
Answer with any 2xx and the delivery is done. Anything else — an error status, a timeout after 10 seconds, a connection failure — is retried five times, at roughly 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours. After that it’s marked Failed.
Retries re-send the original body, not a fresh reading of the record, so a retry an hour later still describes what happened at the time.
Deliveries can arrive more than once — a timeout after your code succeeded still counts as a failure. Make your handler idempotent: X-Webiller-Delivery is stable across a retry, and the payload’s id is stable across a redelivery.
When something doesn’t arrive
Recent deliveries at the bottom of the page lists every attempt with its status and the HTTP code your endpoint returned. Send again re-queues one.
Pause stops weBiller calling an endpoint without deleting it — useful while you’re deploying.
Endpoints must be https and publicly reachable. Private addresses (localhost, 10.x, 192.168.x, link-local) are rejected. To develop locally, use a tunnelling service that gives you a public https URL.
Limits
- 20 active API keys per business
- 10 webhooks per business
- 120 API requests per minute, per key
The full endpoint reference lives in the API’s own Swagger documentation at /api/docs. This page covers the parts that are specific to keys and webhooks.