Implement Flexport webhook event handling for shipment milestones, booking updates, purchase order events, and invoice notifications. Trigger: "flexport webhooks", "flexport events", "flexport milestones", "flexport shipment tracking webhook".
Flexport sends webhook notifications for shipment milestones, booking confirmations, PO updates, invoice events, and document availability. Webhooks are configured in Portal > Settings with a secret token for HMAC-SHA256 signature verification via the X-Hub-Signature header.
| Category | Events | Use Case |
|---|---|---|
| Milestones | cargo_ready, departed, arrived, customs_cleared, delivered | Shipment tracking |
| Transit | estimated_departure, estimated_arrival, actual_departure | ETA updates |
| Bookings | booking_confirmed, booking_amended | Booking lifecycle |
| Purchase Orders | po_created, , |
po_updatedpo_archived| PO management |
| Invoices | invoice_created, freight_invoice_ready | Billing |
| Documents | document_uploaded, bill_of_lading_ready | Document management |
| Container | container_loaded, container_unloaded | Container tracking |
Navigate to Portal > Settings > Webhooks > Add Endpoint:
https://your-app.com/webhooks/flexportimport crypto from 'crypto';
import express from 'express';
const app = express();
// IMPORTANT: Use raw body for signature verification
app.post('/webhooks/flexport', express.raw({ type: '*/*' }), async (req, res) => {
// Step 1: Verify signature
const signature = req.headers['x-hub-signature'] as string;
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.FLEXPORT_WEBHOOK_SECRET!)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Step 2: Parse and route event
const event = JSON.parse(req.body.toString());
console.log(`Webhook: ${event.type} | ID: ${event.data?.id}`);
try {
await routeEvent(event);
res.status(200).send('OK');
} catch (err) {
console.error('Webhook processing failed:', err);
res.status(500).send('Processing error');
// Dead letter: store for retry
}
});
// Step 3: Route events to handlers
async function routeEvent(event: { type: string; data: any }) {
switch (event.type) {
case 'shipment.milestone':
await handleMilestone(event.data);
break;
case 'shipment.eta_updated':
await handleETAUpdate(event.data);
break;
case 'booking.confirmed':
await handleBookingConfirmed(event.data);
break;
case 'invoice.created':
await handleInvoice(event.data);
break;
case 'document.uploaded':
await handleDocument(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
}
async function handleMilestone(data: {
shipment_id: string;
milestone: string;
occurred_at: string;
location?: { name: string; country: string };
}) {
console.log(`Milestone: ${data.milestone} for ${data.shipment_id}`);
console.log(` At: ${data.occurred_at} | Location: ${data.location?.name}`);
// Update your database
await db.shipments.update({
where: { flexportId: data.shipment_id },
data: {
status: data.milestone,
lastMilestoneAt: new Date(data.occurred_at),
currentLocation: data.location?.name,
},
});
// Notify stakeholders for key milestones
if (['departed', 'arrived', 'delivered'].includes(data.milestone)) {
await notifyStakeholders(data.shipment_id, data.milestone);
}
}
// Flexport may retry webhooks — ensure idempotent handling
async function processWebhookIdempotently(event: any) {
const eventId = event.id || crypto.createHash('sha256')
.update(JSON.stringify(event)).digest('hex');
// Check if already processed
const exists = await db.webhookLog.findUnique({ where: { eventId } });
if (exists) {
console.log(`Duplicate webhook ${eventId}, skipping`);
return;
}
await db.$transaction([
db.webhookLog.create({ data: { eventId, type: event.type, processedAt: new Date() } }),
routeEvent(event),
]);
}
| Issue | Cause | Solution |
|---|---|---|
| 401 signature mismatch | Wrong secret or body parsing | Use express.raw(), verify secret matches Portal |
| Duplicate events | Flexport retries on timeout | Implement idempotency with event ID dedup |
| Missing events | Endpoint unreachable | Monitor uptime, use dead letter queue |
| Slow processing | Complex handler logic | Acknowledge fast (200), process async |
For performance optimization, see flexport-performance-tuning.