Settings & accountAPI & webhooks

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

The API keys list showing a key's name, prefix, scope, and last-used date.
One key per integration, so you can revoke them one at a time.

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.

A webhook subscription showing its URL, subscribed events, signing secret, and recent deliveries.
One endpoint, the events it wants, and what actually got delivered.

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

EventFires when
invoice.createdAn invoice is created (including from a subscription)
invoice.sentA draft is sent — in-app or by email. Not on a resend
invoice.paidThe last of the balance is settled
invoice.overdueAn invoice passes its due date
invoice.cancelledAn invoice is cancelled
payment.recordedAny payment is recorded, full or partial
quote.sentA quote is sent
quote.acceptedA client accepts — in-app or via a shared link
quote.declinedA client declines
client.createdA 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.