Webhooks
Bob Go pushes events to a URL you own, so you do not have to poll. Subscribe per topic, per delivery URL.
Topics
| Topic | Fires when |
|---|---|
tracking/updated |
A tracking event is received for a shipment. |
fulfillment/created |
A fulfilment on an order has been created successfully. Not sent for returns. |
shipment_submission_status/updated |
A shipment's submission status changes. |
shipment_charged_amount/updated |
The amount charged for a shipment changes. |
shipment_charged_weight/updated |
The weight charged for a shipment changes. |
shipment_health_status/updated |
A shipment's health status changes. |
order/updated |
An existing order changes. Creating an order does not fire it. |
Endpoints
POST /v2/webhooks |
Subscribe, or change a subscription. |
GET /v2/webhooks |
List your subscriptions. |
DELETE /v2/webhooks |
Remove subscriptions. |
How delivery works
When the event happens, Bob Go sends a POST to your delivery URL with the
payload as JSON. Every delivery carries these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-BobGroup-Topic |
The topic, e.g. tracking/updated. |
X-BobGroup-Type |
webhook |
bobgo-webhook-signature |
The signature, once your account has a webhook secret. See below. |
Deliveries stop while the account is inactive or closed.
Answer with a 2xx. Anything else counts as a failure. Bob Go retries a
429 or a 5xx every 15 minutes. It does not retry any other 4xx, or a
delivery it could not complete at all, such as a refused connection or a
timeout.
Answer within 20 seconds. A delivery still waiting after that is abandoned and not retried. Acknowledge first, then do the work: queue the payload and return, rather than processing inline.
Failing deliveries switch the subscription off. The first failure sets
first_failed_time on the subscription, and a delivery that succeeds clears it.
If deliveries are still failing three days later, the next failure sets the
subscription inactive and the account gets an alert in the dashboard and by
email. Set it back to
active with POST /v2/webhooks once your endpoint is
fixed.
Expect repeats. A webhook may arrive more than once. Key your processing on
something stable, either tracking_reference plus the checkpoint time or the
fulfilment id, so a repeat is a no-op rather than a duplicate shipment in
your system.
Keep a log of what you received and how you answered. When something is missing weeks later, that log is the only way to tell a webhook that never arrived from one that arrived and was dropped.
Verifying the signature
Generate a webhook secret in the dashboard under Settings → Webhook
subscriptions. From then on every delivery carries bobgo-webhook-signature:
the HMAC-SHA256 of the raw request body, keyed with the secret, and
base64-encoded.
To check it, compute the same HMAC over the body bytes exactly as you received them, before any JSON parsing, using the secret string as the key as it is (do not hex-decode it). Compare the base64 result with the header, and reject a delivery that does not match.
Regenerating the secret takes effect immediately, with no overlap, so deploy the new secret as soon as you create it. Deleting it stops the signature being sent.
Even with a signature, treat the URL as public, and do not act on a payload without confirming it against your own records.
Payloads
tracking/updated
Sent whenever a tracking event is received. The payload is the tracking object
that GET /v2/tracking returns, as a
single object rather than an array, plus a shipment object with the contact
details. It carries the full history for the shipment, not just the new
checkpoint, so you can replace your copy rather than merging.
{
"shipment_tracking_reference": "UAEG4Z55",
"status": "delivered",
"status_friendly": "Delivered",
"checkpoints": [{ "...": "..." }],
"shipment_movement_events": { "...": "..." },
"shipment": {
"collection_contact_name": "John",
"collection_contact_surname": "Doe",
"collection_contact_email": "[email protected]",
"collection_contact_mobile_number": "+27123456789",
"delivery_contact_name": "Jane",
"delivery_contact_surname": "Doe",
"delivery_contact_email": "[email protected]",
"delivery_contact_mobile_number": "+27123456789",
"tracking_url": "https://..."
}
}
The fields are described on Get tracking events.
fulfillment/created
Sent once a fulfilment on an order has been created successfully: a shipment
or a buyer collection. An order can produce several. A return is a fulfilment
too, but it does not send this webhook. Follow a return through its shipment
instead, with shipment_submission_status/updated and tracking/updated.
{
"id": 6432,
"account_id": 2,
"channel_ref_id": "",
"order_id": 22837,
"channel_order_number": "22837",
"type": "original",
"method": "shipment",
"method_id": 14273,
"method_reference": "UADPWTTF",
"method_status": "pending-collection",
"status": "success",
"failed_reason": "",
"time_created": "2022-10-20T19:42:00.642739Z",
"time_modified": "2022-10-20T19:42:02.397912Z",
"order_items": [
{
"id": 31119,
"description": "Product 1",
"fulfilled_qty": 1,
"qty": 1,
"sku": "",
"total_price": 599,
"total_weight_kg": 0.1,
"unit_price": 599,
"unit_weight_kg": 0.1,
"vendor": "GoPugMe",
"channel_ref_id": 12116233191527
}
]
}
type is original, or replacement when the fulfilment replaces an earlier
one. qty and fulfilled_qty on each line are the order line's totals so far,
not the quantity in this fulfilment.
order/updated
Sent when an existing order changes: an update, a status or payment change, a tag added or removed, archiving or unarchiving. The payload is the order as it now stands.
{
"id": 22837,
"account_id": 1234,
"channel_order_number": "ABC123",
"channel_ref_id": "",
"currency": "ZAR",
"customer_name": "John",
"customer_surname": "Doe",
"customer_email": "[email protected]",
"customer_phone": "0123456789",
"date_placed_on_channel": "2026-09-05T08:30:00Z",
"delivery_address": { "...": "..." },
"fulfillment_status": "unfulfilled",
"order_items": [{ "...": "..." }],
"note": "",
"payment_status": "paid",
"status": "processing",
"tags": [{ "name": "VIP", "color": "#ffcc00" }],
"total_discount": 0,
"total_price": 1500,
"total_qty": 15,
"total_tax": 195.65,
"total_weight_kg": 1.5,
"total_fulfilled_qty": 0,
"buyer_selected_shipping_cost": 49.99,
"buyer_selected_shipping_method": "Standard Shipping",
"pickup_point_location_id": null,
"pickup_point_provider_slug": null,
"time_created": "2026-09-05T08:30:12Z",
"time_modified": "2026-09-05T09:12:40Z"
}
order_items has the same fields as on fulfillment/created. The status and
payment values are listed on Get orders.
Shipment topics
shipment_submission_status/updated, shipment_charged_amount/updated,
shipment_charged_weight/updated and shipment_health_status/updated all send
the whole shipment, in the same shape. Which topic fired tells you what
changed; the payload tells you what the shipment now looks like. The shape is
close to, but not the same as, a shipment from GET /v2/shipments.
{
"id": 644,
"account_id": 68,
"tracking_reference": "UASDX44J",
"custom_tracking_reference": "631",
"channel_order_number": "12345",
"provider_slug": "demo",
"provider_shipment_id": "123456789",
"service_level_code": "ECO",
"service_level_cut_off_time": "16:30:00",
"status": "pending-collection",
"submission_status": "success",
"health_status": "healthy",
"health_status_reason": "",
"failed_reason": null,
"rate": 117.3,
"charged_amount": 117.3,
"charged_weight_kg": 1,
"submitted_weight_kg": 1,
"submitted_vol_weight_kg": 0.2,
"declared_value": 0,
"parcel_count": 1,
"parcels": [
{
"description": "Watch box",
"submitted_length_cm": 1,
"submitted_width_cm": 1,
"submitted_height_cm": 1,
"submitted_weight_kg": 1,
"actual_length_cm": null,
"actual_width_cm": null,
"actual_height_cm": null,
"actual_weight_kg": null,
"custom_parcel_reference": null,
"tracking_reference": "UASDX44J/1"
}
],
"collection_address": {
"id": 410,
"entered_address": "22 Ealing Crescent, Bryanston, Johannesburg, 2191, Gauteng, South Africa",
"type": "business",
"company": "Bob Go",
"street_address": "22 Ealing Crescent",
"local_area": "Bryanston",
"city": "Johannesburg",
"zone": "GP",
"code": "2191",
"country": "South Africa",
"lat": -26.044926,
"lng": 28.024245
},
"delivery_address": { "...": "..." },
"collection_contact_name": "John",
"collection_contact_surname": "Doe",
"collection_contact_email": "[email protected]",
"collection_contact_mobile_number": "+27123456789",
"delivery_contact_name": "Jane",
"delivery_contact_surname": "Doe",
"delivery_contact_email": "[email protected]",
"delivery_contact_mobile_number": "+27123456789",
"collection_after": "09:00:00",
"collection_before": "15:00:00",
"collection_min_date": "2022-10-13T11:22:17.521043Z",
"collection_reference": "UASDX44J",
"collected_date": null,
"delivered_date": null,
"instructions_collection": "",
"instructions_delivery": "",
"meta": {
"collection_reference": "UASDX44J",
"estimated_collection_date": "2022-10-13T10:52:18.190469Z",
"estimated_delivery_date": "2022-10-18T06:00:00Z"
},
"manifest_id": 0,
"order_fulfillment": null,
"tags": null,
"tracking_expiry_date": null,
"tracking_last_event_time": null,
"tracking_url": "https://...",
"time_created": "2022-10-13T10:52:17.533949Z",
"time_modified": "2022-10-13T10:52:18.72622Z"
}
What each topic is for:
shipment_submission_status/updated: the shipment reached, or failed to reach, the courier. Submission time varies, so this is the reliable signal that a booking is real. Readsubmission_statusandfailed_reason.shipment_charged_amount/updated: the courier re-priced the shipment after measuring it.charged_amountis what you actually pay, and it can differ from what you quoted the buyer.shipment_charged_weight/updated: the courier's measured weight changed. Usually arrives with, or just before, a charged-amount change.shipment_health_status/updated: Bob Go's own assessment of whether the shipment is moving as it should, against its collection and delivery timelines. Readhealth_statusandhealth_status_reasonto find shipments that are stalled, at risk, or have hit a collection or delivery problem, without waiting for a customer to complain.