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:

  1. Create an endpoint in your app to receive notifications
  2. Register your URL in your 4N NextDay panel and pick which events you want
  3. Verify signatures to make sure they're really from us
  4. Respond quickly with a 2xx status

Event types

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

    All packages were delivered successfully. Also fires order.status_changed.

  • Name
    order.partially_delivered
    Description

    Some packages were delivered, others failed. Also fires order.status_changed.

  • Name
    order.delivery_failed
    Description

    Every package in the attempt failed. Also fires order.status_changed.

When each event fires

EventWhen it fires
order.receivedPackage arrives at our warehouse
order.status_changedAny delivery state update
order.deliveredAll packages were delivered successfully
order.partially_deliveredAt least one package was delivered and at least one failed
order.delivery_failedEvery package in the attempt failed (a retry may follow)

order.delivered event

{
  "event": "order.delivered",
  "timestamp": "2026-02-04T11:30:00.000000Z",
  "data": {
    "tracking_number": "4N000000012345",
    "external_reference": "ORDER-001",
    "delivery_state": "delivered",
    "previous_state": "out_for_delivery",
    "delivery_date": "2026-02-04T11:30:00.000000Z",
    "timeline": ["..."],
    "delivery_attempts": ["..."],
    "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:

PendingPicked UpIn TransitOut for DeliveryDeliveredPartially DeliveredFailedCancelled
StateTerminalDescription
pendingnoOrder created, awaiting pickup
picked_upnoPackage collected from sender
in_transitnoIn transport to destination area
out_for_deliverynoOn delivery vehicle for final delivery
deliveredyesAll packages delivered successfully
partially_deliveredyesSome packages delivered, others failed
failedyesEvery package in the delivery attempt failed
nulledyesOrder cancelled

What you'll receive

Headers we send

Every webhook request includes:

HeaderDescription
Content-Typeapplication/json
X-4Nortes-EventThe event type (e.g., order.status_changed)
X-4Nortes-SignatureHMAC-SHA256 signature of the raw body

Envelope

Every event uses the same top-level shape:

{
  "event": "order.status_changed",
  "timestamp": "2026-02-04T11:30:00.000000Z",
  "data": { "..." }
}
  • event — the event type, matches the X-4Nortes-Event header.
  • timestamp — when the event was emitted (ISO 8601).
  • data — event payload, described below.

Common fields in data

These order-level fields are included on every event. Event-specific fields (like failure_info or packages[]) are added on top.

  • Name
    tracking_number
    Type
    string
    Description

    The order tracking number.

  • Name
    external_reference
    Type
    string|null
    Description

    Your custom reference for the order.

  • Name
    cost_center
    Type
    string|null
    Description

    Cost center associated with the order.

  • Name
    purchase_order
    Type
    string|null
    Description

    Purchase order number.

  • Name
    bill_of_lading
    Type
    string|null
    Description

    Bill of lading reference.

  • Name
    delivery_state
    Type
    string
    Description

    Current delivery state. See Delivery states.

  • Name
    estimated_delivery_date
    Type
    string
    Description

    Expected delivery date (ISO 8601).

  • Name
    delivery_date
    Type
    string|null
    Description

    Actual delivery date (ISO 8601). null until the order reaches delivered or partially_delivered.

  • Name
    timeline
    Type
    array
    Description

    Chronological list of state changes. See shape below.

  • Name
    delivery_attempts
    Type
    array
    Description

    One entry per physical delivery attempt (success or failure), with recipient info, photos and per-package outcomes. Empty until the first attempt happens. See shape below.

timeline[] shape

Each entry marks a state transition or a failed delivery attempt:

[
  { "state": "pending", "timestamp": "2026-02-03T10:00:00.000000Z" },
  { "state": "in_transit", "timestamp": "2026-02-03T18:00:00.000000Z" },
  {
    "state": "failed",
    "timestamp": "2026-02-04T14:00:00.000000Z",
    "type": "failure",
    "failure_info": {
      "type": "Intento de entrega fallido",
      "reason": "No está en casa",
      "notes": null
    }
  },
  { "state": "out_for_delivery", "timestamp": "2026-02-05T08:00:00.000000Z" },
  { "state": "delivered", "timestamp": "2026-02-05T11:30:00.000000Z" }
]

Entries with type: "failure" represent an individual failed attempt (the order may still complete successfully on a later attempt) and carry failure_info. The strings inside are localized and meant for display — see Failure reasons for machine-readable values.

delivery_attempts[] shape

Every physical delivery attempt — successful or not — becomes an entry here:

  • Name
    attempt
    Type
    integer
    Description

    1-based attempt number for this order.

  • Name
    outcome
    Type
    string
    Description

    success or failed.

  • Name
    recipient_name
    Type
    string|null
    Description

    Who received the packages. null on failed attempts.

  • Name
    recipient_rut
    Type
    string|null
    Description

    Recipient's Chilean RUT, when captured.

  • Name
    recipient_role
    Type
    string|null
    Description

    Relationship of the recipient (e.g., recipient, family, security, other).

  • Name
    location
    Type
    object|null
    Description

    { latitude, longitude } captured at proof time.

  • Name
    notes
    Type
    string|null
    Description

    Free-text notes from the driver.

  • Name
    photos
    Type
    array
    Description

    Signed photo URLs (url and preview_url). URLs expire after 2 hours — re-fetch the tracking endpoint if you need fresh URLs later.

  • Name
    packages
    Type
    array
    Description

    Per-package outcome for the attempt: { tracking_number, delivered, failure_reason }. failure_reason uses the machine-readable enum on failed packages, null on successful ones.

  • Name
    created_at
    Type
    string
    Description

    When the attempt was recorded (ISO 8601).


order.received

Sent when your package is first scanned at our warehouse.

Adds the following on top of the common fields:

  • Name
    event
    Type
    string
    Description

    Always order.received.

  • Name
    reception.received_at
    Type
    string
    Description

    When the package was scanned (ISO 8601).

  • Name
    reception.warehouse_id
    Type
    integer
    Description

    ID of the warehouse that received the package.

order.received payload

{
  "event": "order.received",
  "timestamp": "2026-02-03T14:30:00.000000Z",
  "data": {
    "tracking_number": "4N000000012345",
    "external_reference": "ORDER-001",
    "cost_center": "CC-001",
    "purchase_order": "PO-2026-100",
    "bill_of_lading": null,
    "delivery_state": "pending",
    "estimated_delivery_date": "2026-02-05T23:59:59.000000Z",
    "delivery_date": null,
    "timeline": [
      { "state": "pending", "timestamp": "2026-02-03T10:00:00.000000Z" }
    ],
    "delivery_attempts": [],
    "reception": {
      "received_at": "2026-02-03T14:30:00.000000Z",
      "warehouse_id": 1
    }
  }
}

order.status_changed

Sent on every delivery state transition — the same updates you'd see in your panel.

Adds the following on top of the common fields:

  • Name
    event
    Type
    string
    Description

    Always order.status_changed.

  • Name
    previous_state
    Type
    string
    Description

    State the order was in before this update.

  • Name
    delivery_proof
    Type
    object|null
    Description

    Populated once the order reaches a terminal state (delivered or partially_delivered). null for intermediate transitions and for failed.

order.status_changed payload (delivered)

{
  "event": "order.status_changed",
  "timestamp": "2026-02-04T11:30:00.000000Z",
  "data": {
    "tracking_number": "4N000000012345",
    "external_reference": "ORDER-001",
    "cost_center": "CC-001",
    "purchase_order": "PO-2026-100",
    "bill_of_lading": null,
    "delivery_state": "delivered",
    "previous_state": "out_for_delivery",
    "estimated_delivery_date": "2026-02-04T23:59:59.000000Z",
    "delivery_date": "2026-02-04T11:30:00.000000Z",
    "timeline": ["..."],
    "delivery_attempts": ["..."],
    "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": "2026-02-04T11:30:00.000000Z"
    }
  }
}

order.delivered

Sent when all packages in the order were delivered successfully. Fires in addition to order.status_changed.

Adds the following on top of the common fields:

  • Name
    event
    Type
    string
    Description

    Always order.delivered.

  • Name
    previous_state
    Type
    string
    Description

    Previous delivery state (e.g., out_for_delivery).

  • Name
    delivery_proof
    Type
    object
    Description

    Recipient info and signed photo URLs for the successful delivery. Same fields as one delivery_attempts[] entry (without attempt, outcome or packages).

order.delivered payload

{
  "event": "order.delivered",
  "timestamp": "2026-02-04T11:30:00.000000Z",
  "data": {
    "tracking_number": "4N000000012345",
    "external_reference": "ORDER-001",
    "cost_center": "CC-001",
    "purchase_order": "PO-2026-100",
    "bill_of_lading": null,
    "delivery_state": "delivered",
    "previous_state": "out_for_delivery",
    "estimated_delivery_date": "2026-02-04T23:59:59.000000Z",
    "delivery_date": "2026-02-04T11:30:00.000000Z",
    "timeline": [
      { "state": "pending", "timestamp": "2026-02-03T10:00:00.000000Z" },
      { "state": "in_transit", "timestamp": "2026-02-03T18:00:00.000000Z" },
      { "state": "out_for_delivery", "timestamp": "2026-02-04T08:00:00.000000Z" },
      { "state": "delivered", "timestamp": "2026-02-04T11:30:00.000000Z" }
    ],
    "delivery_attempts": [
      {
        "attempt": 1,
        "outcome": "success",
        "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"
          }
        ],
        "packages": [
          { "tracking_number": "SH000000012345", "delivered": true, "failure_reason": null }
        ],
        "created_at": "2026-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": "2026-02-04T11:30:00.000000Z"
    }
  }
}

order.partially_delivered

Sent when a delivery attempt ends with some packages delivered and others failed. Fires in addition to order.status_changed.

Adds the following on top of the common fields:

  • Name
    event
    Type
    string
    Description

    Always order.partially_delivered.

  • Name
    previous_state
    Type
    string
    Description

    Previous delivery state (e.g., out_for_delivery).

  • Name
    packages_count
    Type
    integer
    Description

    Total packages in the order.

  • Name
    delivered_packages_count
    Type
    integer
    Description

    Packages delivered across all attempts so far.

  • Name
    delivery_progress
    Type
    integer
    Description

    Percentage of packages delivered (0–100).

  • Name
    packages
    Type
    array
    Description

    Per-package rollup: { tracking_number, delivery_state, failure_reason, delivered_at }. failure_reason uses the machine-readable enum; delivered_at is null for packages that haven't been delivered yet.

  • Name
    delivery_proof
    Type
    object
    Description

    Recipient info and photos for the latest successful sub-delivery. Same shape as on order.delivered.

order.partially_delivered payload

{
  "event": "order.partially_delivered",
  "timestamp": "2026-02-04T11:30:00.000000Z",
  "data": {
    "tracking_number": "4N000000012345",
    "external_reference": "ORDER-001",
    "cost_center": "CC-001",
    "purchase_order": "PO-2026-100",
    "bill_of_lading": null,
    "delivery_state": "partially_delivered",
    "previous_state": "out_for_delivery",
    "estimated_delivery_date": "2026-02-04T23:59:59.000000Z",
    "delivery_date": "2026-02-04T11:30:00.000000Z",
    "packages_count": 3,
    "delivered_packages_count": 2,
    "delivery_progress": 67,
    "packages": [
      {
        "tracking_number": "SH000000012345",
        "delivery_state": "delivered",
        "failure_reason": null,
        "delivered_at": "2026-02-04T11:30:00.000000Z"
      },
      {
        "tracking_number": "SH000000012346",
        "delivery_state": "delivered",
        "failure_reason": null,
        "delivered_at": "2026-02-04T11:30:00.000000Z"
      },
      {
        "tracking_number": "SH000000012347",
        "delivery_state": "failed",
        "failure_reason": "refused",
        "delivered_at": null
      }
    ],
    "timeline": ["..."],
    "delivery_attempts": ["..."],
    "delivery_proof": {
      "recipient_name": "Jane Doe",
      "recipient_rut": "12345678-9",
      "recipient_role": "recipient",
      "location": { "latitude": -33.4289, "longitude": -70.6093 },
      "notes": "Customer refused one of the boxes",
      "photos": [
        {
          "url": "https://storage.example.com/photos/abc123.jpg",
          "preview_url": "https://storage.example.com/photos/thumb/abc123.jpg"
        }
      ],
      "created_at": "2026-02-04T11:30:00.000000Z"
    }
  }
}

order.delivery_failed

Sent when every package in a delivery attempt failed. Fires in addition to order.status_changed. A retry may follow — if it does, you'll receive a new order.status_changed when the order goes back out for delivery, and another terminal event for the new outcome.

Adds the following on top of the common fields:

  • Name
    event
    Type
    string
    Description

    Always order.delivery_failed.

  • Name
    previous_state
    Type
    string
    Description

    Previous delivery state (e.g., out_for_delivery).

  • Name
    failure_info
    Type
    object
    Description

    Human-readable summary of the failure — { type, reason, notes }. All values are localized Spanish strings meant for display.

order.delivery_failed payload

{
  "event": "order.delivery_failed",
  "timestamp": "2026-02-04T14:00:00.000000Z",
  "data": {
    "tracking_number": "4N000000012345",
    "external_reference": "ORDER-001",
    "cost_center": "CC-001",
    "purchase_order": "PO-2026-100",
    "bill_of_lading": null,
    "delivery_state": "failed",
    "previous_state": "out_for_delivery",
    "estimated_delivery_date": "2026-02-04T23:59:59.000000Z",
    "delivery_date": null,
    "timeline": [
      { "state": "pending", "timestamp": "2026-02-03T10:00:00.000000Z" },
      { "state": "in_transit", "timestamp": "2026-02-03T18:00:00.000000Z" },
      { "state": "out_for_delivery", "timestamp": "2026-02-04T08:00:00.000000Z" },
      {
        "state": "failed",
        "timestamp": "2026-02-04T14:00:00.000000Z",
        "type": "failure",
        "failure_info": {
          "type": "Intento de entrega fallido",
          "reason": "No está en casa",
          "notes": null
        }
      }
    ],
    "delivery_attempts": [
      {
        "attempt": 1,
        "outcome": "failed",
        "recipient_name": null,
        "recipient_rut": null,
        "recipient_role": null,
        "location": { "latitude": -33.4289, "longitude": -70.6093 },
        "notes": null,
        "photos": [],
        "packages": [
          { "tracking_number": "SH000000012345", "delivered": false, "failure_reason": "not_home" }
        ],
        "created_at": "2026-02-04T14:00:00.000000Z"
      }
    ],
    "failure_info": {
      "type": "Intento de entrega fallido",
      "reason": "No está en casa",
      "notes": null
    }
  }
}

Failure reasons

Webhooks surface failures in two places, with different audiences:

  • failure_info — appears at the top level of order.delivery_failed and inside timeline[].failure_info. Localized Spanish labels, great for emails, SMS or dashboards. Don't write business logic against these strings; wording may change between locales or releases.
  • failure_reason — appears on delivery_attempts[].packages[] (every event) and on packages[] (on order.partially_delivered). Stable machine-readable enum — use this for retries, conditional routing, reporting.

Machine-readable values

ValueMeaning
not_homeNobody was at the address to receive the package
refusedRecipient refused delivery
wrong_addressAddress is incorrect or cannot be located
inaccessibleAddress exists but the driver couldn't reach it (locked gate, off-grid, etc.)
business_closedDestination is a business that was closed at delivery time
pending_stock_breakInternal stock issue prevented delivery
otherAnything else (check notes on the attempt for context)

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:

AttemptDelay
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

  1. Respond fast: Return 2xx within 10 seconds
  2. Process later: Save the data and handle it in the background
  3. Handle duplicates: You might get the same webhook twice
  4. Keep logs: Save events for debugging
  5. 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)
})

Was this page helpful?