Track a shipment
Surface live tracking status to your users via polling or webhooks.
Once a shipment is booked, you have two ways to keep your UI in sync with the carrier’s state: polling or webhooks. Use webhooks for production — polling is fine for prototypes.
Polling
For low-volume applications, poll the tracking endpoint every few minutes:
import { fc } from './freightcake'
const tracking = await fc.shipments.tracking(9001)
console.log(`Status: ${tracking.status}`)
console.log(`Last event: ${tracking.events[0]?.description}`)
console.log(`At: ${tracking.events[0]?.location}`)Response shape
{
"object": "tracking",
"shipment_id": 9001,
"tracking_number": "SBFC0009001",
"status": "in_transit",
"expected_delivery_date": "2026-05-25",
"events": [
{
"code": "DEPARTED_TERMINAL",
"description": "Departed Kansas City, MO terminal",
"location": "Kansas City, MO",
"occurred_at": "2026-05-22T03:14:00.000Z"
},
{
"code": "ARRIVED_TERMINAL",
"description": "Arrived Kansas City, MO terminal",
"location": "Kansas City, MO",
"occurred_at": "2026-05-21T22:08:00.000Z"
}
]
}Events are returned newest first. The code field is normalized across
carriers — see the tracking event reference
for the full enum.
Polling cadence
Don’t hammer the tracking endpoint. Recommended cadence:
| Status | Poll interval |
|---|---|
scheduled | Every 30 minutes |
picked_up, in_transit | Every 15 minutes |
out_for_delivery | Every 5 minutes |
delivered, exception, cancelled | Stop polling |
A polling loop that respects this:
async function pollUntilDone(shipmentId: number, signal: AbortSignal) {
const intervals: Record<string, number> = {
scheduled: 30 * 60_000,
picked_up: 15 * 60_000,
in_transit: 15 * 60_000,
out_for_delivery: 5 * 60_000,
}
while (!signal.aborted) {
const t = await fc.shipments.tracking(shipmentId)
onUpdate(t)
const wait = intervals[t.status]
if (wait === undefined) return // terminal status
await new Promise((r) => setTimeout(r, wait))
}
}Webhooks (recommended)
For production traffic, register a webhook endpoint and let FreightCake push status changes:
await fc.webhookEndpoints.create({
url: 'https://your-app.example.com/freightcake/webhooks',
events: [
'shipment.status_changed',
'shipment.exception_raised',
'shipment.delivered',
],
})You’ll receive a signing_secret — store it in your secret manager.
Every webhook delivery is signed; verify it server-side:
import { verifyWebhookSignature } from '@freightcake/sdk'
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get('freightcake-signature')!
const isValid = verifyWebhookSignature({
body,
signature: sig,
secret: process.env.FREIGHTCAKE_WEBHOOK_SECRET!,
})
if (!isValid) {
return new Response('Invalid signature', { status: 401 })
}
const event = JSON.parse(body)
switch (event.type) {
case 'shipment.status_changed':
await onStatusChanged(event.data)
break
case 'shipment.exception_raised':
await onException(event.data)
break
case 'shipment.delivered':
await onDelivered(event.data)
break
}
// Always 200 quickly — process asynchronously.
return new Response('ok')
}See Webhooks for delivery guarantees, retry policy, and signature details.
Handling exceptions
A shipment goes to exception status when the carrier reports a problem
(refused delivery, address correction, weather delay, damage). The webhook
payload includes the carrier’s exception reason and a recommended
action:
{
"type": "shipment.exception_raised",
"data": {
"shipment_id": 9001,
"exception": {
"code": "ADDRESS_CORRECTION_NEEDED",
"carrier_message": "Suite number missing",
"recommended_action": "Contact recipient to confirm suite number, then PATCH /shipments/9001 with the corrected address",
"raised_at": "2026-05-23T11:42:00.000Z"
}
}
}Most exceptions resolve within 24 hours if you respond quickly. Surface them to your ops team or the customer immediately — silent exceptions become late-delivery escalations.
Common gotchas
- Webhook ordering is not guaranteed. The same shipment can deliver two
events out of order if the network jitters. Compare
occurred_aton each event before mutating your state machine. - Test mode emits webhooks. A booking made with a
fk_test_key fires the same webhooks (mocked) so you can develop the receiver end to end. - Webhooks retry for 24 hours. Return any 2xx within 5 seconds. If your endpoint times out repeatedly we’ll back off exponentially and email the account owner after 6 consecutive failures.
Next steps
- Wire status updates into your customer-facing tracking UI
- Configure a Slack channel via the Slack integration
- Set delivery-day SMS notifications via the notifications API