Hire A Bus
Sign in
Webhooks

We tell you, instead of you asking us

Subscribe a URL and we POST to it, signed, when something happens. A webhook is a notification, never a wider window: everything in the body is something the same key could already read through the API.
Hire A Bus operator APIVersion 1.0.042 endpointshttps://hireabus.com

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.

EventFires whenWhat arrives
job.offeredA new job lands in your inboxSent 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.confirmedA customer pays and the job is yoursSent 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"]
  }'
EndpointWhat it does
GET /api/v1/webhooks
POST /api/v1/webhooksSubscribe a URL
DELETE /api/v1/webhooks/{id}
GET /api/v1/webhooks/{id}/deliveriesThe 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.

FieldMeaning
eventThe event name, matching the `X-HireABus-Event` header.
operator_refWhich of your accounts this is about.
occurred_atISO 8601, when we raised it.
dataThe event body. Every field in it is something you can already read through the API for the same job.

And these headers:

HeaderMeaning
X-HireABus-EventThe event name, e.g. `booking.confirmed`.
X-HireABus-Signature`sha256=` followed by the hex HMAC.
X-HireABus-TimestampUnix seconds. Signed with the body, so you can reject anything older than your own tolerance.
X-HireABus-DeliveryStable 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.