Webhooks

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. Read submission_status and failed_reason.
  • shipment_charged_amount/updated: the courier re-priced the shipment after measuring it. charged_amount is 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. Read health_status and health_status_reason to find shipments that are stalled, at risk, or have hit a collection or delivery problem, without waiting for a customer to complain.