Webhooks

Webhooks

Instead of polling the Get reservations endpoint, you can have Bob Box push a POST to your own URL whenever a reservation's status changes or its PINs are reissued.

How it works

  1. You configure a URL for each event you care about, in the dashboard.
  2. When that event happens on one of your reservations, Bob Box sends a POST with a JSON body to that URL.
  3. Your endpoint returns 200 OK to acknowledge it. Anything else is retried.

The events

Each one's exact trigger and payload is in section 5 below.

Event Fires when Setting name
Parcel dropped The parcel is put into the locker webhook_reservation_dropoff_url
Parcel collected The recipient collects it webhook_reservation_pickup_url
Reservation returned The parcel is removed to go back to the sender webhook_reservation_return_url
Reservation cleared Bob Box empties the compartment webhook_reservation_cleared_url
Pin reissued Any PIN changes webhook_reservation_pins_reissued_url

There is no event_type field in the body. You know which event it is from which of your URLs received the request, so give each event a distinct URL.

Every payload shares these four fields, plus one timestamp specific to the event:

Field Type Description
reservation_id integer The reservation's ID.
custom_reference string Your own reference for this reservation, if you set one.
custom_data object Any custom data you attached to the reservation.
reservation_status string The reservation's status at the time of the event.

A "manual update" — a status changed through the API rather than by locker hardware — fires the same webhook as the hardware path. Handle both identically.

1. Set up webhooks in the dashboard

This is the only way to generate your signing secret.

You must be signed in as the Bob Box account owner. An account user cannot do this. If you are a super admin, you cannot do it from the internal Accounts pages either — impersonate the account owner first.

Step 1 — Create and save the webhook secret

  1. In the left menu, click Settings (cog icon).
  2. Open the Webhook auth credentials tab.
  3. Click Create webhook secret. (If one already exists, the button says Regenerate webhook secret.)
  4. A popup shows your new secret. Click the copy icon.
  5. Save it somewhere safe now — you cannot get it back. If you lose it you must regenerate, and the old one stops working immediately.
  6. Close the popup. From now on you only see a masked version.

This secret is the key for the X-Webhook-Signature HMAC-SHA256 signature on every webhook we send you.

Step 2 — Add your webhook URLs

  1. Still in Settings, open the General tab.
  2. Under Webhooks, fill in the URLs you want. Leave a field blank to switch that event off.
  3. Click Save. Each URL field saves independently, so one bad URL will not block the others.

Example URL format: https://example.com/reservation-dropoff

A blank URL means the event is simply not sent — nothing fails or errors on the reservation side.

Step 3 — Check it works

Do a real dropoff on a test reservation. We POST to your "Parcel dropped off" URL about 2 seconds later. Verify the signature using the secret from Step 1.

If webhooks stop right after you regenerate a secret, check that your endpoint was updated — it is still checking the old one.

2. The webhook request

Method: POST Content-Type: application/json

Every request carries these headers:

Content-Type: application/json
X-BobGroup-Type: webhook
X-Webhook-Timestamp: 1756288800000
X-Webhook-Signature: v1=5d41402abc4b2a76b9719d911017c592...
X-Auth-Hash: c592a76b9719d911017...

X-Webhook-Timestamp is a Unix timestamp in milliseconds.

X-Webhook-Signature and X-Auth-Hash are only sent if you have created a webhook secret. Without one, the request arrives unsigned and you cannot verify it — so set one up.

3. Verifying the request came from Bob Box

An HMAC-SHA256 signature, sent as v1=<hex signature>, computed over the timestamp and the raw request body:

signature = hex( HMAC-SHA256( secret, timestamp + "." + raw_request_body ) )

Example — Node.js verification:

const crypto = require("crypto");

function verifyWebhookSignature(rawBody, timestamp, signatureHeader, secret) {
  // signatureHeader looks like "v1=<hex>"
  const receivedSignature = signatureHeader.replace(/^v1=/, "");

  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(receivedSignature, "hex"),
    Buffer.from(expectedSignature, "hex")
  );
}

Use the raw, unparsed request body. Re-serializing parsed JSON can change field order or spacing and break the comparison. This is the single most common webhook integration bug.

X-Auth-Hash is an older scheme kept for backwards compatibility (SHA1(hex(MD5(body)) + secret)). Prefer X-Webhook-Signature for new integrations.

4. Responding to webhooks

Return 200 OK as soon as you have received and stored the webhook. Do slow processing afterwards, asynchronously.

If your endpoint does not return 200 OK, Bob Box retries the same request roughly once a minute, up to 9 times. There is no exponential backoff — it is a flat one-minute gap. After that the delivery is handed back to the queue, so a small number of further attempts is possible before it stops.

Make your endpoint idempotent. Use reservation_id plus the event's timestamp field to detect a duplicate you have already handled.

5. The events in detail

Parcel dropped

Sent when a parcel is put into the locker.

Dashboard field: Parcel dropped off Setting name: webhook_reservation_dropoff_url

When it fires

The locker door closes after a dropoff_pin was used, and the reservation is now waiting-for-pickup.

It also fires when a reservation is manually updated to waiting-for-pickup through the API. Treat both the same way.

Payload

{
  "reservation_id": 8931,
  "custom_reference": "ORDER-1452",
  "custom_data": {},
  "reservation_status": "waiting-for-pickup",
  "dropoff_time": "2026-08-27T09:12:00+02:00"
}
Field Type Description
reservation_id integer The reservation's ID.
custom_reference string Your own reference for this reservation, if you set one.
custom_data object Any custom data you attached to the reservation.
reservation_status string Always waiting-for-pickup for this event.
dropoff_time string (datetime) When the parcel was dropped off.

What to do with it

  • Send the recipient their pickup_pin and the locker address.
  • On a client-dropoff reservation, this is your signal to dispatch a driver — the parcel is now actually in the locker.

Parcel collected

Sent when the parcel is taken out of the locker by its recipient.

Dashboard field: Parcel collected Setting name: webhook_reservation_pickup_url

When it fires

The locker door opens with a pickup_pin, and the reservation is now complete.

It also fires on a manual update to complete through the API. Treat both the same way.

Payload

{
  "reservation_id": 8931,
  "custom_reference": "ORDER-1452",
  "custom_data": {},
  "reservation_status": "complete",
  "pickup_time": "2026-08-27T14:03:00+02:00"
}
Field Type Description
reservation_id integer The reservation's ID.
custom_reference string Your own reference for this reservation, if you set one.
custom_data object Any custom data you attached to the reservation.
reservation_status string Always complete for this event.
pickup_time string (datetime) When the parcel was collected.

What to do with it

  • Mark the delivery complete on your side and stop any collection reminders.
  • On a client-dropoff reservation, this fires when your driver collects — the parcel is now on your network, not the customer's doorstep.

Reservation returned

Sent when the parcel is taken out of the locker to go back to the sender.

Dashboard field: Parcel returned Setting name: webhook_reservation_return_url

When it fires

The locker door opens with a return_pin, and the reservation is now returned.

It also fires on a manual update to returned through the API. Treat both the same way.

Payload

{
  "reservation_id": 8931,
  "custom_reference": "ORDER-1452",
  "custom_data": {},
  "reservation_status": "returned",
  "return_time": "2026-08-27T16:40:00+02:00"
}
Field Type Description
reservation_id integer The reservation's ID.
custom_reference string Your own reference for this reservation, if you set one.
custom_data object Any custom data you attached to the reservation.
reservation_status string Always returned for this event.
return_time string (datetime) When the parcel was removed for return.

What to do with it

  • Route the parcel back to the sender on your own network.
  • This is a distinct outcome from complete — the recipient never got the parcel. Do not close the order as delivered.

Reservation cleared

Sent when Bob Box empties a compartment to free it up. This happens when a parcel is left in the locker long past its intervention_time.

Dashboard field: Reservation cleared Setting name: webhook_reservation_cleared_url

When it fires

A compartment is cleared and the reservation's status becomes cleared.

It also fires on a manual update to cleared through the API. Treat both the same way.

A reservation only becomes eligible for clearing after its intervention_time, which sits after flat_fee_end_time. Both are on the reservation, so you can see it coming. Clearing is an operations decision, not an automatic job, so it happens some time after intervention_time rather than at it.

Payload

{
  "reservation_id": 8931,
  "custom_reference": "ORDER-1452",
  "custom_data": {},
  "reservation_status": "cleared",
  "clear_time": "2026-08-27T18:00:00+02:00"
}
Field Type Description
reservation_id integer The reservation's ID.
custom_reference string Your own reference for this reservation, if you set one.
custom_data object Any custom data you attached to the reservation.
reservation_status string Always cleared for this event.
clear_time string (datetime) When the compartment was cleared.

What to do with it

  • The parcel is no longer in the locker. Contact Bob Box support with the reservation_id to find out where it went.
  • Treat this as an exception, not a normal ending. If you are seeing it regularly, your collection reminders are firing too late.

Pin reissued

Sent when a reservation's PINs change. Uncommon — it only happens when there is a problem at a locker and Bob Box has to intervene.

Dashboard field: Reservation pins reissued Setting name: webhook_reservation_pins_reissued_url

When it fires

Any of the dropoff, pickup, or return PINs change — for example when a reservation is moved to a different compartment because the original door will not open.

Payload

This event carries more fields than the others, including the live PINs. Treat it as sensitive and keep your endpoint secure.

{
  "reservation_id": 8931,
  "custom_reference": "ORDER-1452",
  "custom_data": {},
  "reservation_status": "waiting-for-pickup",
  "compartment_number": 14,
  "dropoff_pin": "482150",
  "pickup_pin": "903417",
  "return_pin": "118736",
  "time": "2026-08-27T10:15:00+02:00"
}
Field Type Description
reservation_id integer The reservation's ID.
custom_reference string Your own reference for this reservation, if you set one.
custom_data object Any custom data you attached to the reservation.
reservation_status string The reservation's status at the time of the change.
compartment_number integer The compartment the reservation now occupies. It may have moved.
dropoff_pin string The new dropoff PIN.
pickup_pin string The new pickup PIN.
return_pin string The new return PIN.
time string (datetime) When the PINs were reissued.

What to do with it

  • Any PIN you have already sent a customer is now dead. Send the new one, and say why.
  • compartment_number may have changed too. If you told the customer a compartment number, correct it.
  • Handle this event even though it is rare. Skipping it means a customer standing at a locker with a PIN that does not work.

6. Best practices

  • Secure your endpoint. Verify the signature on every request; do not trust the body alone.
  • Respond fast. Acknowledge, queue, then process.
  • Be resilient. Queue and retry on your side too, so a downstream failure does not lose the event.
  • Keep an audit log. Record every webhook received and the status you returned. It makes debugging possible.

7. Troubleshooting

  • Not receiving webhooks? Confirm the URL for that event is filled in and saved under Settings → General → Webhooks. An empty URL means the event is silently skipped.
  • No signature headers? You have not created a webhook secret yet — see Step 1.
  • Signature never matches? You are almost certainly hashing a re-serialized body. Capture the raw bytes before your JSON parser touches them.
  • Getting the same event twice? Make sure your endpoint returns 200 OK reliably and quickly. A slow or failing endpoint gets retried.