Webhooks
Webhooks push events to your endpoint instead of making you poll. You register an HTTPS URL per app in the developer console, choose which events to receive, and verify each delivery with the endpoint’s signing secret.
The secret (whsec_…) is shown once, when the webhook is created. Store it
with your other credentials.
Events
Section titled “Events”| Event | Fires when | Gating scope |
|---|---|---|
invoice.created |
An invoice is created | invoices:read |
invoice.updated |
An invoice changes | invoices:read |
payment.received |
A customer payment is recorded | payments:read |
customer.created |
A customer is created | customers:read |
sales_return.created |
A sales return (credit note) is created | sales-returns:read |
Deliveries are scope-gated twice: your app must hold the event’s gating scope, and the business the event happened in must have granted that scope to your app. You only ever receive events for businesses that connected your app.
Delivery format
Section titled “Delivery format”Each delivery is an HTTP POST with a JSON body:
{ "id": "delivery-id", "type": "invoice.created", "businessId": "business-id", "created": 1765465600, "data": { "…": "event-specific payload" }}| Header | Value |
|---|---|
Content-Type |
application/json |
X-Webhook-Id |
Delivery ID (same as body id; stable across retries) |
X-Webhook-Event |
Event name, e.g. invoice.created |
X-Webhook-Signature |
t=<unix-seconds>,v1=<hex HMAC-SHA256> |
businessId tells you which connected business the event belongs to — this is
the one place the platform hands you a business identifier, since a single
webhook endpoint serves all businesses connected to your app.
Verifying signatures
Section titled “Verifying signatures”The signature is HMAC-SHA256(secret, "<t>.<raw body>"), hex-encoded, where
t is the timestamp from the header. Verify against the raw request bytes
(before any JSON parsing), and use a constant-time comparison:
import crypto from 'node:crypto';import express from 'express';
const app = express();const WEBHOOK_SECRET = process.env.ONEBOOKS_WEBHOOK_SECRET; // whsec_…const TOLERANCE_SECONDS = 300;
app.post('/webhooks/onebooks', express.raw({ type: 'application/json' }), // keep the raw body (req, res) => { const header = req.get('X-Webhook-Signature') ?? ''; const parts = Object.fromEntries( header.split(',').map((kv) => kv.split('=', 2)), ); const { t, v1 } = parts; if (!t || !v1) return res.status(400).send('malformed signature');
// Reject stale timestamps to blunt replay attacks. if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) { return res.status(400).send('timestamp out of tolerance'); }
const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(`${t}.${req.body}`) // req.body is a Buffer here .digest('hex');
const a = Buffer.from(v1, 'hex'); const b = Buffer.from(expected, 'hex'); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(400).send('bad signature'); }
const event = JSON.parse(req.body.toString('utf8')); // Acknowledge fast; process asynchronously. res.status(200).end(); queueForProcessing(event); });Each retry is re-signed with a fresh timestamp, so a delivery that arrives late still verifies.
Retries and timeouts
Section titled “Retries and timeouts”A delivery succeeds on any 2xx response. Your endpoint has 10 seconds to
respond; redirects are not followed. Anything else — non-2xx, timeout,
connection error — schedules a retry:
| Attempt | Delay after previous failure |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
After the 6th failed attempt the delivery is marked failed and not retried. The console shows recent deliveries per webhook — status, attempts, response code and last error — so you can diagnose a misbehaving endpoint.
Testing
Section titled “Testing”From the console you can fire a test delivery at any webhook. It carries a
synthetic payload (data is { "test": true, "message": "This is a test event" })
and is signed and delivered exactly like a real event — use it to verify your
signature code end-to-end before going live. Pair it with a
sandbox business to generate real events safely.