Webhooks send near real-time notifications when events happen in your MVMNT account. Instead of polling the API for changes, MVMNT delivers an HTTP POST to your server as soon as an event is processed.
A webhook is an HTTP callback: an HTTP POST request sent to a URL you configure. When something changes in MVMNT (for example, a shipment status update), MVMNT sends a JSON payload to your webhook endpoint.
┌──────────────┐ Event Occurs ┌──────────────┐
│ MVMNT │ (Order Booked) │ Your MVMNT │
│ System │────────────────────────────▶│ Account │
└──────────────┘ └──────────────┘
│
│
┌──────────────▼──────────────┐
│ Event Processing Pipeline │
│ (EventBridge + Lambda) │
└──────────────┬──────────────┘
│
│ HTTP POST
│
┌──────────────▼──────────────┐
│ Your Webhook Endpoint │
│ https://your-app.com/hooks │
└──────────────────────────────┘- Real-Time Updates: Get notified within seconds of events occurring
- Reduced API Calls: No need to poll for changes (saves on rate limits)
- Better Performance: Lower latency than polling
- Event-Driven Architecture: Build reactive, event-driven integrations
- Bandwidth Efficient: Only receive data when something changes
Without webhooks, you’d need to poll, which increases latency and burns API calls:
// ❌ Don't do this - wastes API calls and has high latency
setInterval(async () => {
const shipments = await api.get('/v1/shipments?status=in_transit');
shipments.forEach(checkForUpdates);
}, 30000); // Poll every 30 seconds// ✅ Do this - real-time, efficient
app.post('/webhooks/mvmnt', (req, res) => {
const { events } = req.body;
events.forEach((event) => {
if (event.event === 'SHIPMENT_DELIVERED') {
handleShipmentUpdate(event.data);
}
});
res.status(200).send('OK');
});Set up your webhook in the MVMNT application:
- URL: Your endpoint (must be HTTPS)
- Token: Secret token that MVMNT will send in the
x-api-keyheader - Events: Select which event types to receive (e.g.,
SHIPMENT_DELIVERED,SHIPMENT_BOOKED)
The webhook configuration is managed through the MVMNT UI settings page.
When an event occurs, MVMNT sends an HTTP POST request to your URL:
POST /webhooks/mvmnt HTTP/1.1
Host: your-app.com
Content-Type: application/json
x-api-key: your-webhook-token-from-ui
{
"sentAt": "2025-01-15T14:30:00Z",
"events": [
{
"event": "SHIPMENT_DELIVERED",
"timestamp": "2025-01-15T14:30:00Z",
"data": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"friendlyId": "SHP-12345",
"key": "ERP-SHIP-789"
}
}
]
}Always verify the x-api-key header matches your configured webhook token:
app.post('/webhooks/mvmnt', (req, res) => {
const receivedToken = req.headers['x-api-key'];
const expectedToken = process.env.WEBHOOK_TOKEN; // Token you configured in MVMNT UI
if (receivedToken !== expectedToken) {
return res.status(401).send('Invalid token');
}
// Token is valid - process webhook
console.log('Valid webhook:', req.body);
res.status(200).send('OK');
});Respond with 200 OK within 5 seconds. If you need to do heavier work (database writes, external calls, long-running workflows), acknowledge the webhook first and process asynchronously.
app.post('/webhooks/mvmnt', async (req, res) => {
// Verify token
if (req.headers['x-api-key'] !== process.env.WEBHOOK_TOKEN) {
return res.status(401).send('Invalid token');
}
// Respond quickly
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body);
});If your endpoint fails or doesn’t respond, MVMNT retries deliveries with exponential backoff:
- 1st retry: 1 minute
- 2nd retry: 5 minutes
- 3rd retry: 15 minutes
- 4th retry: 1 hour
- 5th retry: 4 hours
After 5 failed attempts, the webhook delivery is abandoned.
All webhook deliveries follow this structure:
{
"sentAt": "2025-01-15T14:30:00Z",
"events": [
{
"event": "SHIPMENT_DELIVERED",
"timestamp": "2025-01-15T14:30:00Z",
"data": {
// Event-specific data
// Always includes: id, friendlyId, key (if set)
},
"diff": null
}
]
}Every webhook delivery includes:
| Field | Type | Description |
|---|---|---|
sentAt | string (ISO 8601) | When the webhook was sent from MVMNT |
events | array | Array of events (typically one event) |
Each event in the events array includes:
| Field | Type | Description |
|---|---|---|
event | string | Event type (e.g., SHIPMENT_DELIVERED) |
timestamp | string (ISO 8601) | When the event occurred |
data | object | Event-specific data |
data.id | string (UUID) | Resource ID |
data.friendlyId | string | Human-readable ID (e.g., SHP-12345) |
data.key | string | null | Your system's reference ID (if set) |
diff | array | null | Changes made (only for UPDATE events, else null) |
Each event type includes additional relevant fields in the data object. The exact structure varies by event type.
Example: SHIPMENT_DELIVERED
{
"sentAt": "2025-01-15T14:30:00Z",
"events": [
{
"event": "SHIPMENT_DELIVERED",
"timestamp": "2025-01-15T14:30:00Z",
"data": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"friendlyId": "SHP-12345",
"key": "ERP-SHIP-789"
},
"diff": null
}
]
}Example: CARRIER_UPDATED (with diff)
{
"sentAt": "2025-01-15T14:30:00Z",
"events": [
{
"event": "CARRIER_UPDATED",
"timestamp": "2025-01-15T14:30:00Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"friendlyId": "CAR-12345",
"key": "ERP-CARRIER-789",
"status": "INACTIVE"
},
"diff": [
{
"type": "UPDATE",
"key": "status",
"value": "INACTIVE",
"oldValue": "ACTIVE"
}
]
}
]
}Note: See the API Reference for the complete schema of each webhook payload type.
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/mvmnt', (req, res) => {
const { sentAt, events } = req.body;
console.log(`Received ${events.length} event(s) sent at ${sentAt}`);
events.forEach((event) => {
console.log('Event:', event.event, 'Data:', event.data);
});
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});ngrok http 3000Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
- Go to MVMNT Settings → Webhooks
- Add your ngrok URL:
https://abc123.ngrok.io/webhooks/mvmnt - Generate and copy a webhook token
- Select event types (e.g.,
SHIPMENT_DELIVERED,SHIPMENT_BOOKED) - Save configuration
// Use the token from MVMNT UI
const WEBHOOK_TOKEN = 'your-token-from-mvmnt-ui';Create or update a shipment in the MVMNT UI, and watch your endpoint receive the webhook!
- Use HTTPS (required for production)
- Verify token on all webhook requests using the
x-api-keyheader - Respond quickly (< 5 seconds) with 200 OK
- Process asynchronously - don't block the response
- Use
keyfor easy correlation with your system - Handle duplicate events (webhooks may be delivered more than once)
- Log webhook deliveries for debugging
- Test thoroughly before going to production
- Don't use HTTP (only HTTPS allowed in production)
- Don't perform long operations before responding
- Don't return errors for duplicate events (return 200)
- Don't rely on event ordering (events may arrive out of order)
- Don't skip token verification (security risk)
All webhook configuration is managed through the MVMNT UI:
- Settings → Webhooks - Configure URL, token, and event types
- Enable/Disable - Toggle webhook delivery on/off
- Update Events - Add or remove event types to receive
- Rotate Token - Generate a new token for security
- View Logs - Monitor webhook deliveries and failures
Token verification fails:
- Ensure you're using the correct token from MVMNT UI
- Check that
x-api-keyheader is being read correctly - Verify no extra whitespace in token comparison
Endpoint unreachable:
- Verify URL and firewall settings
- Ensure HTTPS certificate is valid
- Check that your server is publicly accessible
Timeout errors:
- Endpoint must respond within 5 seconds
- Move processing to background jobs
- Return 200 OK immediately
Duplicate webhooks:
- This is expected behavior due to retries
- Implement idempotency (for example, using a unique event/delivery identifier if your integration stores one)
- Always return 200 even for duplicates
- API Reference - View webhook schemas and event types