FreightCake
Walkthroughs

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:

StatusPoll interval
scheduledEvery 30 minutes
picked_up, in_transitEvery 15 minutes
out_for_deliveryEvery 5 minutes
delivered, exception, cancelledStop 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))
  }
}

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_at on 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

On this page