Webhooks
Get notified when something happens to your orders - like they get picked up or delivered. Instead of constantly checking your panel, we'll tell you.
How to set up
Here's what you need to do:
- Create an endpoint in your app to receive notifications
- Register your URL in your 4N NextDay panel
- Verify signatures to make sure they're really from us
- Respond quickly with a 2xx status
Respond within 10 seconds. If you need to do something slow, save the data and process it later.
What we'll notify you about
You'll get notified when:
- Name
order.received- Description
Your package was scanned at our warehouse.
- Name
order.status_changed- Description
Order status changed (picked up, in transit, delivered, etc.).
When you'll hear from us
| Event | When it fires |
|---|---|
order.received | Package arrives at our warehouse |
order.status_changed | Status updates - same as what you see in your panel |
Event type example
{
"event": "order.status_changed",
"timestamp": "2025-02-04T11:30:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"delivery_state": "delivered",
"previous_state": "out_for_delivery"
}
}
What you'll receive
Headers we send
Every webhook includes:
| Header | Description |
|---|---|
Content-Type | application/json |
X-4Nortes-Event | The event type (e.g., order.status_changed) |
X-4Nortes-Signature | HMAC signature for verification |
X-4Nortes-Timestamp | Unix timestamp of the event |
When we receive your package
Sent when your package is scanned at our warehouse.
- Name
event- Type
- string
- Description
Event type:
order.received
- Name
timestamp- Type
- string
- Description
ISO 8601 timestamp of the event.
- Name
data.tracking_number- Type
- string
- Description
The order tracking number.
- Name
data.external_reference- Type
- string
- Description
Your custom order reference.
- Name
data.received_at- Type
- string
- Description
Timestamp when package was received.
- Name
data.warehouse- Type
- string
- Description
Warehouse location that received the package.
order.received payload
{
"event": "order.received",
"timestamp": "2025-02-03T14:30:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"external_reference": "ORDER-001",
"received_at": "2025-02-03T14:30:00.000000Z",
"warehouse": "Santiago Distribution Center"
}
}
When order status changes
Sent whenever the delivery status updates - same events you'd see in your panel.
- Name
event- Type
- string
- Description
Event type:
order.status_changed
- Name
timestamp- Type
- string
- Description
ISO 8601 timestamp of the event.
- Name
data.tracking_number- Type
- string
- Description
The order tracking number.
- Name
data.external_reference- Type
- string
- Description
Your custom order reference.
- Name
data.delivery_state- Type
- string
- Description
New delivery state.
- Name
data.previous_state- Type
- string
- Description
Previous delivery state.
- Name
data.estimated_delivery_date- Type
- string
- Description
Updated estimated delivery date.
- Name
data.delivery_date- Type
- string
- Description
Actual delivery date (if delivered).
- Name
data.timeline- Type
- array
- Description
Recent timeline events.
- Name
data.delivery_proof- Type
- object
- Description
Proof of delivery (only for
deliveredstate).
order.status_changed payload
{
"event": "order.status_changed",
"timestamp": "2025-02-04T11:30:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"external_reference": "ORDER-001",
"delivery_state": "delivered",
"previous_state": "out_for_delivery",
"estimated_delivery_date": "2025-02-04",
"delivery_date": "2025-02-04",
"timeline": [
{
"state": "delivered",
"timestamp": "2025-02-04T11:30:00.000000Z",
"type": "state_change",
"failure_info": null
}
],
"delivery_proof": {
"recipient_name": "Jane Doe",
"recipient_rut": "12345678-9",
"recipient_role": "recipient",
"notes": null,
"location": {
"latitude": -33.4289,
"longitude": -70.6093
},
"photos": [
{
"url": "https://storage.example.com/photos/abc123.jpg",
"preview_url": "https://storage.example.com/photos/thumb/abc123.jpg"
}
]
}
}
}
Make sure it's really from us
Verify the signature in the X-4Nortes-Signature header to confirm we sent it.
How to verify
We sign every webhook with your secret key. Check it like this:
Verifying webhook signatures
const crypto = require('crypto')
function verifyWebhook(payload, signature, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(signature)
)
}
// Express.js example
app.post('/webhooks/nextday', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-4nortes-signature']
const payload = req.body.toString()
if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse(payload)
// Process the event...
res.status(200).send('OK')
})
Always use constant-time comparison functions (like crypto.timingSafeEqual or hash_equals) to prevent timing attacks.
If your server is down
Don't worry - we'll retry if you don't respond with a 2xx:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 24 hours |
After 5 tries, we'll give up on that webhook.
Tips
- Respond fast: Return 2xx within 10 seconds
- Process later: Save the data and handle it in the background
- Handle duplicates: You might get the same webhook twice
- Keep logs: Save events for debugging
- Monitor failures: Set up alerts if webhooks stop working
Async processing example
app.post('/webhooks/nextday', async (req, res) => {
// Acknowledge receipt immediately
res.status(200).send('OK')
// Process asynchronously
const event = req.body
await queue.add('processWebhook', event)
})