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 read

Published

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

  1. Open Settings → API tokens and choose Manage webhooks.
  2. Choose Add endpoint, enter the URL, an optional description, and tick the events you want. At least one is required.
  3. 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.
  4. 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:

json
{
  "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.object is exactly what GET /api/v1/shipments/:id returns (or what GET /api/v1/alerts returns for alert.created), in the API version your organization is pinned to. api_version tells you which.
  • shipment.checks_completed adds data.checks: a summary with the count of checks by status, and results, a list of { check_type, key, status }.
  • id identifies 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_at before 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:

ruby
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:

js
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
Tags: webhooks api integration automation