We tell you, instead of you asking us
Events
These are all of them. Subscribing to a name we do not send is refused rather than accepted and silently ignored, so a typo fails at the point you make it.
| Event | Fires when | What arrives |
|---|---|---|
| job.offered | A new job lands in your inbox | Sent the moment we ask you to quote a job, with the pickup, the date and the group size — no customer contact details, the same as your inbox. |
| booking.confirmed | A customer pays and the job is yours | Sent when the money clears, with the booking reference and what you will be paid. |
Subscribing
A key with the webhook scope creates the subscription. The secret comes back exactly once, in the response to that call — store it then, because no later request will show it to you again.
curl -X POST https://hireabus.com/api/v1/webhooks \
-H "Authorization: Bearer $HIREABUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Dispatch system",
"endpoint_url": "https://your-system.example.com/hooks/hireabus",
"events": ["job.offered", "booking.confirmed"]
}'| Endpoint | What it does |
|---|---|
| GET /api/v1/webhooks | — |
| POST /api/v1/webhooks | Subscribe a URL |
| DELETE /api/v1/webhooks/{id} | — |
| GET /api/v1/webhooks/{id}/deliveries | The delivery log for one subscription — what we sent, when, and what status came back. This is the endpoint an integrator uses to answer "did you send it?" without asking us |
What we send
Every delivery is the same envelope, whatever the event.
| Field | Meaning |
|---|---|
| event | The event name, matching the `X-HireABus-Event` header. |
| operator_ref | Which of your accounts this is about. |
| occurred_at | ISO 8601, when we raised it. |
| data | The event body. Every field in it is something you can already read through the API for the same job. |
And these headers:
| Header | Meaning |
|---|---|
| X-HireABus-Event | The event name, e.g. `booking.confirmed`. |
| X-HireABus-Signature | `sha256=` followed by the hex HMAC. |
| X-HireABus-Timestamp | Unix seconds. Signed with the body, so you can reject anything older than your own tolerance. |
| X-HireABus-Delivery | Stable across retries of the same delivery — dedupe on it. |
Verifying the signature
The signature is HMAC-SHA256, hex, prefixed `sha256=` over <timestamp>.<raw request body> — the timestamp, a full stop, then the raw bytes we sent. Sign the body you received, not a re-encoded copy of it: a different key order or a different escaping produces a different hash and an afternoon of confusion.
Including the timestamp is what stops a captured delivery being replayed at you forever. Reject anything older than your own tolerance — five minutes is a sensible one — and compare the hashes with a constant-time function.
// Node — Express, with the raw body preserved
const crypto = require('crypto');
// The path you gave us as endpoint_url, on your own server.
const HOOK_PATH = '/hooks/hireabus';
app.post(HOOK_PATH, express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-HireABus-Timestamp');
const signature = req.get('X-HireABus-Signature');
const body = req.body.toString('utf8');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.HIREABUS_WEBHOOK_SECRET)
.update(timestamp + '.' + body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) return res.sendStatus(400);
handle(req.get('X-HireABus-Event'), req.get('X-HireABus-Delivery'), JSON.parse(body));
res.sendStatus(200);
});# PHP
$timestamp = $request->header('X-HireABus-Timestamp');
$signature = $request->header('X-HireABus-Signature');
$body = $request->getContent();
abort_if(abs(time() - (int) $timestamp) > 300, 400);
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('HIREABUS_WEBHOOK_SECRET'));
abort_unless(hash_equals($expected, (string) $signature), 400);Retries, and what a bad day looks like
We wait up to 10 seconds for your response and treat any 2xx as delivered. Anything else is retried 6 times in total, on this schedule:
10s → 30s → 2m → 5m → 15m → 1h
The X-HireABus-Delivery header is the same on every attempt of the same delivery, so if you processed one whose 200 we never saw, you can recognise it and answer 200 without doing the work twice. Build your handler to be safe to run twice; at-least-once is what we promise.
After around 20 consecutive failures we park the subscription rather than keep hammering a dead endpoint. It is re-enabled by removing it and subscribing again, and the delivery log on the API tells you what we got back each time.
Answer fast, work later
Acknowledge with a 200 as soon as you have the body somewhere durable, then do the real work off the request. A handler that talks to three of your own systems before answering will eventually exceed our timeout, and everything after that is retries you did not need.
