Skip to main content

Shipment Journey Webhooks

BELL sends an HTTP POST to your URL when an order has a new journey milestone. You can update shipment progress in your own system without continuously polling the tracking API.

This page covers configuration, the order.journey.created event, signature verification and delivery history. Forwarding events share the same URL and secret.


1. Quick start​

  1. Prepare a public HTTPS endpoint that accepts POST requests with a JSON body.
  2. Obtain an API token for an account associated with your customer.
  3. Configure the URL and enable webhooks using the API below, or open Preferences → Webhook, the Webhook hành trình đơn section in BELL. Enter the Webhook URL, enable Bật đẩy hành trình về webhook and click Lưu (Save).
  4. Save the secret returned or displayed when it is generated. Configure it on your receiving endpoint.
  5. Call the test API or click Gửi test (Send test), then check Lịch sử giao (Delivery history).
Save the secret when it is generated

The secret is returned only when newly generated. The configuration GET API does not return it. If you lose the secret, generate a new one and update your receiving endpoint.

Your API token authenticates configuration requests to BELL; your webhook secret verifies requests from BELL to you. They are different credentials, are not interchangeable and must not be shared publicly.

2. Webhook configuration API​

Base URL: https://expressbe.bellvn.com.

The APIs in this section require Authorization: Bearer <token>. BELL determines the customer from the token's account; you do not need to send customer_id.

MethodEndpointPurpose
GET/api/customers/webhookRead the current configuration
PUT / PATCH/api/customers/webhookUpdate the URL, enable/disable webhooks or generate a new secret
POST/api/customers/webhook/testSend a test webhook
GET/api/customers/webhook/deliveriesRead delivery history

Read the configuration​

curl 'https://expressbe.bellvn.com/api/customers/webhook' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'

Example response:

{
"status": "success",
"data": {
"customer_id": 12,
"webhook_url": "https://example.com/webhooks/bell",
"webhook_enabled": true,
"has_secret": true
}
}

webhook_url can be null when not configured. has_secret indicates whether a secret exists, not its value.

Configure the URL and enable webhooks​

curl -X PATCH 'https://expressbe.bellvn.com/api/customers/webhook' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"webhook_url": "https://example.com/webhooks/bell",
"webhook_enabled": true
}'
FieldTypeMeaning
webhook_urlstring / nullA valid URL, up to 500 characters. HTTPS is recommended. Send null to remove the URL when webhooks are disabled, or send webhook_enabled: false in the same request
webhook_enabledbooleanEnable or disable webhooks
regenerate_secretbooleanSend true to generate a new secret

Send only the fields you want to change. Webhooks cannot be enabled without a URL. BELL automatically generates a secret when enabling webhooks if none exists.

Example response when a secret has just been generated (the secret below is illustrative only):

{
"status": "success",
"message": "Webhook configuration updated",
"data": {
"customer_id": 12,
"webhook_url": "https://example.com/webhooks/bell",
"webhook_enabled": true,
"has_secret": true,
"secret": "whsec_example_not_a_real_secret"
}
}

Real secrets have a whsec_ prefix followed by 48 random characters. If an update does not generate a new secret, data.secret is null; keep your existing saved secret.

Disable webhooks or generate a new secret​

Disable webhooks while keeping the URL and secret:

curl -X PATCH 'https://expressbe.bellvn.com/api/customers/webhook' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"webhook_enabled": false}'

Generate a new secret:

curl -X PATCH 'https://expressbe.bellvn.com/api/customers/webhook' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"regenerate_secret": true}'
Changing the URL or secret

BELL uses the current URL and secret at send time, including retries of an existing delivery. The old secret is no longer used to sign new requests. Coordinate updates to your receiving endpoint when rotating the secret, and send a test after the change.

3. Send a test webhook​

curl -X POST 'https://expressbe.bellvn.com/api/customers/webhook/test' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'

No body is required. BELL sends a request to your saved URL with the webhook.test event. It has the same data structure as a journey webhook but contains sample data, not a real order. For example: order_code = BELL_TEST_0000000001, journey.id = 0.

This API sends synchronously and does not use journey webhook retries. A saved URL is sufficient to send a test, even when webhooks are disabled; generate a secret first to test signature verification fully.

Response when your endpoint returns 2xx:

{
"status": "success",
"message": "Test webhook delivered",
"data": {
"delivery_id": 12345,
"http_status": 200
}
}

If your endpoint returns a non-2xx response or a connection error/timeout occurs, the BELL API returns HTTP 502 with status: "error". Use data.delivery_id to find the delivery in history; data.http_status is present when an HTTP response was received from your endpoint.

4. Requests from BELL to your endpoint​

PropertyValue
MethodPOST
URLYour configured webhook URL
BodyJSON, UTF-8
Timeout per send attempt15 seconds

Headers​

Content-Type: application/json
User-Agent: BellVN-Webhook/1.0
X-Bell-Event: order.journey.created
X-Bell-Delivery: 12345
X-Bell-Signature: sha256=<hex-hmac-sha256>
HeaderMeaning
X-Bell-EventEvent name
X-Bell-DeliveryDelivery ID; unchanged across retries, use it for deduplication
X-Bell-SignatureHMAC-SHA256 signature of the raw body, prefixed with sha256=

This request does not use your API token as a Bearer token. Your receiving endpoint must authenticate it using X-Bell-Signature.

Events​

EventWhen it is sent
order.journey.createdAn order belonging to the customer has a new journey milestone, and webhooks are enabled with a URL when the milestone is scanned
webhook.testYou call the test API or click Gửi test (Send test)
fwd.status_changedA Forwarding request changes its public status
fwd.tracking.createdA Forwarding request has a new progress milestone

The two fwd.* events have different payloads; see Forwarding webhooks. A shared receiving endpoint must distinguish events by event; do not assume every request contains data.journey.

5. Shipment journey payload​

Each order.journey.created event contains one journey milestone, not the order's entire tracking history.

{
"event": "order.journey.created",
"timestamp": "2026-10-08T10:30:00+07:00",
"data": {
"order_code": "BELL0000000123",
"tracking_code": "TRACK123456",
"order_status": 4,
"journey": {
"id": 987654,
"code": "SCN",
"type": "SHIP",
"note": "Arrived at sorting center",
"city": "TAUNTON",
"state": "MASSACHUSETTS",
"country": "US",
"postcode": "02780",
"date": "2026-10-08 10:20:00"
}
}
}
FieldTypeMeaning
eventstringEvent name
timestampstringPayload creation time, ISO 8601 with a timezone offset; not the time the milestone occurred
data.order_codestring / nullBELL order code
data.tracking_codestring / nullCarrier tracking code
data.order_statusinteger / nullNumeric order status at payload creation time; not the status string returned by the order detail API
data.journey.idinteger / nullJourney milestone ID
data.journey.codestring / nullJourney event code, for example SCN; codes may differ by shipping source
data.journey.typestring / nullJourney type, for example START, SHIP, END
data.journey.notestring / nullJourney description
data.journey.citystring / nullCity
data.journey.statestring / nullState/province
data.journey.countrystring / nullCountry
data.journey.postcodestring / nullPostal code; keep it as a string to preserve leading zeros
data.journey.datestring / nullMilestone time from the source data, usually YYYY-MM-DD HH:mm:ss without a timezone offset

Data fields can be null when the source has no value. Do not assume journey.date is UTC or apply the offset from timestamp to it. If you need normalized timestamps or status-code mappings, confirm the shipping source's conventions with BELL.

6. Verify the signature​

Compute HMAC-SHA256(raw_body, webhook_secret) as hex and prefix it with sha256=. Compare it with X-Bell-Signature using a constant-time comparison.

Use the exact raw body

Do not parse JSON and re-encode it to calculate the signature. Changing whitespace, Unicode, field order or character escaping can cause a mismatch.

The following PHP example only demonstrates signature verification and JSON decoding; it does not include event persistence or deduplication:

<?php

$secret = getenv('BELL_WEBHOOK_SECRET');
if (!is_string($secret) || $secret === '') {
http_response_code(500);
exit('Webhook secret is not configured');
}

$rawBody = file_get_contents('php://input');
if ($rawBody === false) {
http_response_code(400);
exit('Cannot read request body');
}

$signature = $_SERVER['HTTP_X_BELL_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}

$payload = json_decode($rawBody, true);
$deliveryId = $_SERVER['HTTP_X_BELL_DELIVERY'] ?? '';
if (!is_array($payload) || json_last_error() !== JSON_ERROR_NONE || $deliveryId === '') {
http_response_code(400);
exit('Invalid payload or missing delivery ID');
}

BELL_WEBHOOK_SECRET is a configuration variable on your receiving system containing the secret issued by BELL. After verification, persist the delivery and event durably, deduplicate by delivery ID, then return 2xx. A valid signature alone does not mean your business processing has succeeded.

7. Responses, retries and event order​

  • Return HTTP 2xx within 15 seconds after durably storing the event or placing it on your own durable processing queue. No specific response body is required.
  • For business event webhooks, BELL retries non-2xx responses and connection errors/timeouts. The current configuration allows up to 5 attempts in total, including the first attempt and up to 4 retries, with successive retry delays of 10 seconds, 30 seconds, 2 minutes and 10 minutes. Actual timing depends on queue processing.
  • webhook.test is sent once within the test request and is not automatically retried.
  • Use X-Bell-Delivery as a deduplication key, backed by a unique constraint or equivalent mechanism. If a delivery has already been received and stored, return 2xx on retries without repeating business processing.
  • Events are not guaranteed to arrive in chronological order. Do not overwrite newer state with an older event solely based on arrival order; use the tracking API when you need to reconcile history.
Delivery scope and historical data

Webhooks are asynchronous and are not guaranteed to arrive immediately when a carrier records a milestone. Milestones already scanned while webhooks were disabled or no URL was configured are not automatically replayed when you enable webhooks. Use the tracking API for your initial history synchronization.

If webhooks are disabled or the URL is removed before a waiting delivery is sent, that delivery may become skipped.

8. Read delivery history​

curl 'https://expressbe.bellvn.com/api/customers/webhook/deliveries?status=failed&per_page=20&page=1' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
QueryMeaning
statusOptional; filter by pending, success, failed or skipped
per_pageRecords per page; defaults to 20, limited to 1–100
pagePage to retrieve

The response has status: "success" and a paginated object in data. Records are in data.data; metadata includes data.current_page, data.per_page, data.total, data.last_page. Newest records appear first.

Each record contains id (matching X-Bell-Delivery), order_id, order_journey_id, event, webhook_url, status, http_status, attempts, last_error, delivered_at, created_at. This API does not return raw payloads, your endpoint's response body or secrets. The historical webhook_url is the URL at delivery creation; if you later change it, the actual send-time URL can differ.

StatusMeaning
pendingThe delivery has been created and no send result has been recorded
successYour endpoint returned 2xx
failedThe latest send attempt failed; a retry may still be pending, so this does not necessarily mean attempts are exhausted
skippedSkipped because webhooks were disabled, the URL was missing or the customer no longer existed at send time

attempts counts HTTP send attempts. For fwd.* events, order_id and order_journey_id are null; identify the Forwarding request using its payload as described in the separate documentation.

9. Common errors and endpoint launch checklist​

HTTPCase
400Token has been revoked (Token is Blacklisted)
401Token has expired or the account could not be authenticated
403Invalid token (Token is Invalid)
404Missing token (Authorization Token not found) or customer not found when reading/updating configuration
422Invalid URL/field types; enabling webhooks without a URL; no update fields; account not associated with a customer; testing without a URL
502The test API did not receive 2xx from your endpoint or encountered a connection error/timeout

See API authentication for token issues. Business errors typically include status: "error" and message. Validation errors may return message and per-field errors; token errors may only return a message in status. Do not assume all errors use the same shape.

Before launching your receiving endpoint:

  1. Ensure the public HTTPS URL accepts JSON POST requests from BELL.
  2. Store the secret securely and verify the signature on the raw body before trusting the data.
  3. Deduplicate by X-Bell-Delivery and persist events durably before returning 2xx.
  4. Distinguish webhook.test, order.journey.created and fwd.* events; never update real orders from test data.
  5. Send a test, inspect delivery history and monitor errors on your receiving system.
  6. Synchronize initial history through the tracking API; contact BELL for reconciliation or assistance with a failed delivery.