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.).
- Name
order.delivered- Description
Order was successfully delivered. Also fires
order.status_changed.
- Name
order.delivery_failed- Description
A delivery attempt failed. Also fires
order.status_changed.
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 |
order.delivered | Package was delivered successfully (also fires order.status_changed) |
order.delivery_failed | Delivery attempt failed (also fires order.status_changed) |
order.delivered and order.delivery_failed are fired in addition to order.status_changed. If you already handle order.status_changed, these are optional - use them to simplify your logic for delivery outcomes.
order.delivered event
{
"event": "order.delivered",
"timestamp": "2025-02-04T11:30:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"external_reference": "ORDER-001",
"cost_center": "CC-001",
"purchase_order": "PO-2025-100",
"bill_of_lading": null,
"delivery_state": "delivered",
"previous_state": "out_for_delivery",
"estimated_delivery_date": "2025-02-04T23:59:59.000000Z",
"delivery_date": "2025-02-04T11:30:00.000000Z",
"timeline": ["..."],
"delivery_proof": {"..."}
}
}
Delivery states
The delivery_state and previous_state fields in webhook payloads use these values - the same statuses you see in your panel:
| State | Description |
|---|---|
pending | Order created, awaiting pickup |
picked_up | Package collected from sender |
in_transit | In transport to destination area |
out_for_delivery | On delivery vehicle for final delivery |
delivered | Successfully delivered (final) |
failed | Delivery attempt failed |
nulled | Order cancelled (final) |
The previous_state field uses the same values. It tells you what the order was before the current update.
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 |
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.cost_center- Type
- string|null
- Description
Cost center associated with the order.
- Name
data.purchase_order- Type
- string|null
- Description
Purchase order number.
- Name
data.bill_of_lading- Type
- string|null
- Description
Bill of lading reference.
- Name
data.delivery_state- Type
- string
- Description
Current delivery state.
- Name
data.estimated_delivery_date- Type
- string
- Description
Estimated delivery date (ISO 8601).
- Name
data.delivery_date- Type
- string
- Description
Actual delivery date,
nulluntil delivered.
- Name
data.timeline- Type
- array
- Description
Timeline events up to this point.
- Name
data.reception.received_at- Type
- string
- Description
Timestamp when the package was received at the warehouse.
- Name
data.reception.warehouse_id- Type
- integer
- Description
ID of the warehouse 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",
"cost_center": "CC-001",
"purchase_order": "PO-2025-100",
"bill_of_lading": null,
"delivery_state": "pending",
"estimated_delivery_date": "2025-02-05T23:59:59.000000Z",
"delivery_date": null,
"timeline": [
{
"state": "pending",
"timestamp": "2025-02-03T10:00:00.000000Z"
}
],
"reception": {
"received_at": "2025-02-03T14:30:00.000000Z",
"warehouse_id": 1
}
}
}
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.cost_center- Type
- string|null
- Description
Cost center associated with the order.
- Name
data.purchase_order- Type
- string|null
- Description
Purchase order number.
- Name
data.bill_of_lading- Type
- string|null
- Description
Bill of lading 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
Full timeline of state changes. Each entry has
stateandtimestamp. Failure entries also includetypeandfailure_info.
- Name
data.delivery_proof- Type
- object
- Description
Proof of delivery (only for
deliveredstate). Includesrecipient_name,recipient_rut,recipient_role,location,notes,photos, andcreated_at.
order.status_changed payload
{
"event": "order.status_changed",
"timestamp": "2025-02-04T11:30:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"external_reference": "ORDER-001",
"cost_center": "CC-001",
"purchase_order": "PO-2025-100",
"bill_of_lading": null,
"delivery_state": "delivered",
"previous_state": "out_for_delivery",
"estimated_delivery_date": "2025-02-04T23:59:59.000000Z",
"delivery_date": "2025-02-04T11:30:00.000000Z",
"timeline": [
{
"state": "pending",
"timestamp": "2025-02-03T10:00:00.000000Z"
},
{
"state": "in_transit",
"timestamp": "2025-02-03T18:00:00.000000Z"
},
{
"state": "out_for_delivery",
"timestamp": "2025-02-04T08:00:00.000000Z"
},
{
"state": "delivered",
"timestamp": "2025-02-04T11:30:00.000000Z"
}
],
"delivery_proof": {
"recipient_name": "Jane Doe",
"recipient_rut": "12345678-9",
"recipient_role": "recipient",
"location": {
"latitude": -33.4289,
"longitude": -70.6093
},
"notes": null,
"photos": [
{
"url": "https://storage.example.com/photos/abc123.jpg",
"preview_url": "https://storage.example.com/photos/thumb/abc123.jpg"
}
],
"created_at": "2025-02-04T11:30:00.000000Z"
}
}
}
When order is delivered
Sent when an order is successfully delivered. This event fires in addition to order.status_changed, so you don't need to handle both - use whichever fits your integration.
- Name
event- Type
- string
- Description
Event type:
order.delivered
- 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.cost_center- Type
- string|null
- Description
Cost center associated with the order.
- Name
data.purchase_order- Type
- string|null
- Description
Purchase order number.
- Name
data.bill_of_lading- Type
- string|null
- Description
Bill of lading reference.
- Name
data.delivery_state- Type
- string
- Description
Always
delivered.
- Name
data.previous_state- Type
- string
- Description
Previous delivery state (e.g.,
out_for_delivery).
- Name
data.estimated_delivery_date- Type
- string
- Description
Estimated delivery date (ISO 8601).
- Name
data.delivery_date- Type
- string
- Description
Actual delivery date (ISO 8601).
- Name
data.timeline- Type
- array
- Description
Full timeline of state changes.
- Name
data.delivery_proof- Type
- object
- Description
Proof of delivery. Includes
recipient_name,recipient_rut,recipient_role,location,notes,photos, andcreated_at.
order.delivered payload
{
"event": "order.delivered",
"timestamp": "2025-02-04T11:30:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"external_reference": "ORDER-001",
"cost_center": "CC-001",
"purchase_order": "PO-2025-100",
"bill_of_lading": null,
"delivery_state": "delivered",
"previous_state": "out_for_delivery",
"estimated_delivery_date": "2025-02-04T23:59:59.000000Z",
"delivery_date": "2025-02-04T11:30:00.000000Z",
"timeline": [
{
"state": "pending",
"timestamp": "2025-02-03T10:00:00.000000Z"
},
{
"state": "in_transit",
"timestamp": "2025-02-03T18:00:00.000000Z"
},
{
"state": "out_for_delivery",
"timestamp": "2025-02-04T08:00:00.000000Z"
},
{
"state": "delivered",
"timestamp": "2025-02-04T11:30:00.000000Z"
}
],
"delivery_proof": {
"recipient_name": "Jane Doe",
"recipient_rut": "12345678-9",
"recipient_role": "recipient",
"location": {
"latitude": -33.4289,
"longitude": -70.6093
},
"notes": null,
"photos": [
{
"url": "https://storage.example.com/photos/abc123.jpg",
"preview_url": "https://storage.example.com/photos/thumb/abc123.jpg"
}
],
"created_at": "2025-02-04T11:30:00.000000Z"
}
}
}
When delivery fails
Sent when a delivery attempt fails. This event fires in addition to order.status_changed, so you don't need to handle both - use whichever fits your integration.
- Name
event- Type
- string
- Description
Event type:
order.delivery_failed
- 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.cost_center- Type
- string|null
- Description
Cost center associated with the order.
- Name
data.purchase_order- Type
- string|null
- Description
Purchase order number.
- Name
data.bill_of_lading- Type
- string|null
- Description
Bill of lading reference.
- Name
data.delivery_state- Type
- string
- Description
Always
failed.
- Name
data.previous_state- Type
- string
- Description
Previous delivery state (e.g.,
out_for_delivery).
- Name
data.estimated_delivery_date- Type
- string
- Description
Estimated delivery date (ISO 8601).
- Name
data.delivery_date- Type
- string
- Description
Always
null(delivery did not complete).
- Name
data.timeline- Type
- array
- Description
Full timeline of state changes. The last entry includes
failure_info.
- Name
data.failure_info- Type
- object
- Description
Details about the failure. Includes
type,reason, andnotes.
order.delivery_failed payload
{
"event": "order.delivery_failed",
"timestamp": "2025-02-04T14:00:00.000000Z",
"data": {
"tracking_number": "4N000000012345",
"external_reference": "ORDER-001",
"cost_center": "CC-001",
"purchase_order": "PO-2025-100",
"bill_of_lading": null,
"delivery_state": "failed",
"previous_state": "out_for_delivery",
"estimated_delivery_date": "2025-02-04T23:59:59.000000Z",
"delivery_date": null,
"timeline": [
{
"state": "pending",
"timestamp": "2025-02-03T10:00:00.000000Z"
},
{
"state": "in_transit",
"timestamp": "2025-02-03T18:00:00.000000Z"
},
{
"state": "out_for_delivery",
"timestamp": "2025-02-04T08:00:00.000000Z"
},
{
"state": "failed",
"timestamp": "2025-02-04T14:00:00.000000Z",
"type": "delivery_failed",
"failure_info": {
"type": "recipient_not_found",
"reason": "No se encontró al destinatario",
"notes": null
}
}
],
"failure_info": {
"type": "recipient_not_found",
"reason": "No se encontró al destinatario",
"notes": null
}
}
}
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 | ~10 seconds |
| 2nd retry | ~2 minutes |
| 3rd retry | ~17 minutes |
| 4th retry | ~3 hours |
After 5 total attempts (1 initial + 4 retries), 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)
})