Getting Data Out
Webhooks
Have Trackberry call your own system the moment a shipment changes status, its ETA moves, its quality checks finish or an alert opens.
5 min readPublished
Goal
Get told when something happens to a shipment, instead of asking the API every few minutes. Trackberry sends a signed HTTPS request to a URL you choose, with the shipment as it looks right after the change.
Webhooks are managed by admins and owners, on every plan, under Settings → API tokens → Webhooks. An organization can have up to five endpoints.
Add an endpoint
- Open Settings → API tokens and choose Manage webhooks.
- Choose Add endpoint, enter the URL, an optional description, and tick the events you want. At least one is required.
- Open the endpoint and reveal its signing secret. Keep it somewhere safe: your server uses it to check that a request really comes from Trackberry.
- Choose Send test event and check that your server received it.
Events
| Event | Sent when | previous_attributes |
|---|---|---|
shipment.status_changed |
A shipment moves to a new status. If several changes are saved together, you get one event from the first status to the last | status |
shipment.eta_changed |
The shipment’s eta field changes: the ETA from the carrier or the documents. Changes to effective_eta and predicted_eta don’t send an event yet |
eta |
shipment.checks_completed |
A run of the quality checks finishes, after any automatic validation | none |
alert.created |
A new alert opens on a shipment | none |
webhook.test |
You choose Send test event; it is never sent otherwise | none |
Alerts are the ones GET /api/v1/alerts returns. A shipment still waiting for
approval, or one that was identified as someone else’s, sends nothing.
What a request looks like
The body is JSON, in the shape of the Standard Webhooks envelope:
{
"id": "evt_9f2c4e1b7a6d4c0f8e3b5a1d2c7e6f90",
"type": "shipment.status_changed",
"created_at": "2026-10-12T09:30:00Z",
"api_version": "2026-10-01",
"data": {
"object": { "object": "shipment", "id": "shp_...", "status": "in_transit" },
"previous_attributes": { "status": "validated" }
}
}
data.objectis exactly whatGET /api/v1/shipments/:idreturns (or whatGET /api/v1/alertsreturns foralert.created), in the API version your organization is pinned to.api_versiontells you which.shipment.checks_completedaddsdata.checks: asummarywith the count of checks by status, andresults, a list of{ check_type, key, status }.ididentifies the event. It is the same for every endpoint, and the same on a retry or a resend, so use it to ignore duplicates.- Events can arrive more than once and out of order. Compare timestamps such as
updated_atbefore overwriting newer data.
Every request carries these headers:
| Header | Value |
|---|---|
webhook-id |
The event id |
webhook-timestamp |
Unix time in seconds, when this attempt was made |
webhook-signature |
v1, followed by a base64 HMAC-SHA256 signature |
Content-Type |
application/json |
User-Agent |
Trackberry-Webhooks/1.0 |
Verify the signature
Always verify before trusting a request. The signature is an HMAC-SHA256 of
webhook-id.webhook-timestamp.body, using your secret (base64-decoded, without
the whsec_ prefix). The official Standard Webhooks libraries do this and also
reject old timestamps.
Ruby, with the standardwebhooks gem:
require "standardwebhooks"
def receive(request)
payload = request.body.read # the raw body, not parsed JSON
headers = request.headers.to_h.slice("webhook-id", "webhook-timestamp", "webhook-signature")
event = StandardWebhooks::Webhook.new(ENV.fetch("TRACKBERRY_WEBHOOK_SECRET")).verify(payload, headers)
# event is the parsed JSON; raises StandardWebhooks::WebhookVerificationError if invalid
end
Node, with the standardwebhooks package:
import { Webhook } from "standardwebhooks"
app.post("/webhooks/trackberry", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = new Webhook(process.env.TRACKBERRY_WEBHOOK_SECRET).verify(req.body, req.headers)
// handle event.type
res.sendStatus(204)
} catch (err) {
res.sendStatus(400)
}
})
Use the raw request body. A body that was parsed and re-serialized no longer matches the signature.
Delivery and retries
Answer with any 2xx status within 10 seconds (we stop waiting after 15 seconds in all). Anything else counts as a
failure: another status, a timeout, a connection error, and a redirect (we never
follow redirects, so give the final URL). Trackberry reads and keeps only the
first 2 KB of your response and shows it on the endpoint page.
A failed delivery is tried again up to 8 times in all: right away, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, about a day in total. Each attempt is recorded on the endpoint page for 30 days, and Resend sends the same event again with a fresh timestamp.
If deliveries to an endpoint keep failing for more than 3 days, Trackberry switches it off, drops the deliveries still waiting, and emails your organization’s admins. The count only runs while failures keep coming: a success, or a day without any failed delivery, starts it over. After you fix your server, open the endpoint and choose Re-enable endpoint.
Send test event sends the event once and never retries it. A failed test doesn’t count toward switching the endpoint off. A delivery that was lost on our side, for example in a deploy, is picked up and sent again within an hour.
Security and networking
- Endpoints must use HTTPS and be reachable from the public internet. We refuse URLs whose host resolves to a private, loopback, link-local, shared or otherwise reserved address, IPv6 included, both when you save the endpoint and before every delivery. trackberry.com and its subdomains are refused too.
- We connect to the address we checked, so DNS changes in between cannot send a request somewhere else.
- We don’t publish fixed source IP addresses, so don’t rely on an IP allow-list. The signature is how you know a request is ours.
- To replace a leaked secret, choose Roll secret. The old one stops working immediately (there is no overlap period), so update your server at the same time.
Troubleshooting
| You see | What to do |
|---|---|
| “must point to a public address” when saving | The host resolves to a private or internal address. Use a public URL, for example through a tunnel while testing |
| Deliveries show “Redirects are not followed” | Your server answers with a 3xx. Use the final URL |
| Deliveries show a timeout | Answer first and do the work afterwards. You have 10 seconds |
| The signature never verifies | Check you pass the raw body and the right endpoint’s secret, and that your server’s clock is correct |
| The endpoint says disabled | It failed for more than 3 days. Fix the server, then re-enable it |