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
- You configure a URL for each event you care about, in the dashboard.
- When that event happens on one of your reservations, Bob Box sends a
POSTwith a JSON body to that URL. - Your endpoint returns
200 OKto 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
- In the left menu, click Settings (cog icon).
- Open the Webhook auth credentials tab.
- Click Create webhook secret. (If one already exists, the button says Regenerate webhook secret.)
- A popup shows your new secret. Click the copy icon.
- Save it somewhere safe now — you cannot get it back. If you lose it you must regenerate, and the old one stops working immediately.
- 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
- Still in Settings, open the General tab.
- Under Webhooks, fill in the URLs you want. Leave a field blank to switch that event off.
- 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
Recommended: verify X-Webhook-Signature
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_pinand the locker address. - On a
client-dropoffreservation, 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-dropoffreservation, 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_idto 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_numbermay 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 OKreliably and quickly. A slow or failing endpoint gets retried.