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.freight.getTracking('9001')
console.log(`Status: ${tracking.status}`)
console.log(`PRO number: ${tracking.pro_number ?? 'pending'}`)
console.log(`Location: ${tracking.location ?? 'not reported'}`)Response shape
{
"object": "tracking",
"shipment_id": 9001,
"pro_number": "SBFC0009001",
"carrier": "Sandbox Freight Co",
"status": "IN_TRANSIT",
"location": "Kansas City, MO",
"last_checked_at": "2026-07-10T18:14:00.000Z"
}Use the standalone tracking resource when you only have a PRO number and no FreightCake shipment ID.
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:
function normalizeTrackingStatus(status: string | null) {
return status?.trim().toLowerCase().replace(/[\s-]+/g, '_') ?? ''
}
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.freight.getTracking(String(shipmentId))
onUpdate(t)
const status = normalizeTrackingStatus(t.status)
if (['delivered', 'exception', 'cancelled', 'canceled'].includes(status)) return
const wait = intervals[status] ?? 15 * 60_000
await new Promise((r) => setTimeout(r, wait))
}
}Tracking status values can differ in casing and separators by source. Normalize them before comparisons, and fetch current state rather than treating an unknown value as terminal.
Webhooks (recommended)
For production traffic, register a webhook endpoint and let FreightCake push status changes:
const endpoint = await fc.webhooks.create({
url: 'https://your-app.example.com/freightcake/webhooks',
events: [
'shipment.picked_up',
'shipment.in_transit',
'shipment.out_for_delivery',
'shipment.exception',
'shipment.delivered',
],
})
await saveSecret(endpoint.secret)You’ll receive secret only in the create response. 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 signature = req.headers.get('x-freightcake-signature')
if (!signature) return new Response('Missing signature', { status: 401 })
try {
verifyWebhookSignature(body, signature, process.env.FREIGHTCAKE_WEBHOOK_SECRET!)
} catch {
return new Response('Invalid signature', { status: 401 })
}
const event = JSON.parse(body)
switch (event.type) {
case 'shipment.picked_up':
case 'shipment.in_transit':
case 'shipment.out_for_delivery':
await onStatusChanged(event.data)
break
case 'shipment.exception':
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:
{
"id": "evt_0192fbe3",
"type": "shipment.exception",
"created": "2026-07-10T18:14:00.000Z",
"data": {
"shipment": {
"id": 9001,
"proNumber": "SBFC0009001",
"status": "EXCEPTION",
"location": "Kansas City, MO",
"carrier": "Sandbox Freight Co"
}
},
"webhook": {
"id": "whk_12",
"url": "https://your-app.example.com/freightcake/webhooks"
}
}Fetch the latest freight tracking state for carrier-specific context, then surface the exception to your operations team.
Common gotchas
- Webhook ordering is not guaranteed. The same shipment can deliver two
events out of order. Compare
created, then fetch current tracking state 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 about 38 hours. Return any 2xx within 10 seconds. Failed attempts back off from 1 minute through 24 hours; an endpoint is disabled and its owner is emailed after 10 consecutive failed attempts.
Next steps
- Wire status updates into your customer-facing tracking UI
- Review the webhook endpoint reference
- Use the standalone tracking resource for PRO-number tracking