# Guide to Nash Webhooks: Features and Best Practices

Nash webhooks push delivery progress to your systems as it happens: a delivery is created, a driver is assigned, the courier arrives at pickup, the parcel is dropped off, or a provider cancels. If you dispatch local deliveries through Nash and want to update customers, advance orders or alert on problems without polling, webhooks are how Nash tells you what it already knows.

This guide covers how Nash endpoints are set up and signed, the events Nash sends and what their payloads contain, how retries work, and the best practices for running a Nash receiver in production.

## What are Nash webhooks?

Nash sends webhooks through Svix. You register endpoints in the Nash Portal, choose which event types each one receives, and Nash sends a JSON `POST` for every matching event, signed with the Svix headers `svix-id`, `svix-timestamp` and `svix-signature`.

Every payload has three fields:

* `type`: the kind of event, one of `delivery`, `task`, `courier_location`, `courier_name` or `shift`
* `event`: the status within that type, such as `dropoff_complete`
* `data`: for `delivery` and `task` events, the whole Job object, in the same shape as the Create Job response. For `shift` events, a small payload of its own

Together, `type` and `event` name the event, so `delivery` with `dropoff_complete` is `delivery.dropoff_complete`.

## Nash webhook features

| Feature | Details |
| --- | --- |
| Configuration | Nash Portal: **Settings > Webhook Management** |
| Endpoints | Multiple endpoints, each with its own event type filter |
| Environments | Sandbox and production |
| Event Catalog | A Portal tab with every event schema and example payloads for some events |
| Signature headers | `svix-id`, `svix-timestamp`, `svix-signature` |
| Scheme | HMAC-SHA256, base64, over `{svix-id}.{svix-timestamp}.{raw body}` |
| Secret | One per endpoint, `whsec_` prefixed |
| Event name | `type` and `event` in the body |
| Event ID | None in the body. `svix-id` stays the same when a message is resent |
| Payload | A snapshot of the whole Job, not a diff |
| Retries | Immediately, then 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours |
| Recovery | Retry single messages, or recover all failed messages from a date, in the Portal |
| Delivery control | `webhook-delivery: abort-message` stops retries. `webhook-delivery: disable` or `410 Gone` disables the endpoint |
| Source IPs | Svix's published source IP list |

Nash does not state a response timeout for its endpoints.

## Common events

### Delivery events

Delivery events are sent once per state transition. Nash lists 27, including:

* `created`, `assigned_driver`, `not_assigned_driver`, `started`
* `pickup_enroute`, `pickup_arrived`, `pickup_complete`
* `dropoff_enroute`, `dropoff_arrived`, `dropoff_complete`
* `canceled_by_provider`, `canceled_by_customer`, `canceled_by_nash`, `failed`, `expired`
* `return_in_progress`, `returned`
* `proof_of_delivery`, `pincode_verification`, `pickup_label`, and the shipping-only `shipping_label` and `manifested`

Seven of them are guaranteed: `created`, `assigned_driver`, `pickup_enroute`, `pickup_arrived`, `dropoff_enroute`, `dropoff_arrived` and `dropoff_complete`. Nash notes that guaranteed events "may be inferred from future events", so one can arrive later than the change it describes.

### Task events

Task events track each delivery attempt: `created`, `updated`, `running`, `completed`, `failed`, the cancellation events including `canceled_by_reassign` and `canceled_by_auto_reassign`, `dispatch_failure`, `planned_selection`, `refund_created` and `refund_updated`.

### Courier events

`courier_location.updated` is sent every 1 to 2 minutes while a courier location is available. With immediate location webhooks turned on, it is sent as each position arrives, at most once every 20 seconds per delivery, which Nash calls "several times the volume". `courier_name.updated` is sent when the same provider swaps drivers.

### Shift events

`shift.dispatched` has a short payload:

```json
{
  "type": "shift",
  "event": "dispatched",
  "data": {
    "eventSourceId": "act_7xK9mP2nQ4wL",
    "created_job_ids": ["job_8rY3nK7pL2xM", "job_5tW9mN3kP8yQ"],
    "errors": {}
  }
}

```

Delivery, task and courier events carry the full Job in `data`, with its `jobConfigurations`, `tasks`, quotes, the delivery's status, courier details, documents and `statusHistory`. All timestamps are UTC ISO 8601 with microseconds and no time zone suffix, such as `2022-03-05T01:11:51.078927`. Parse them as UTC.

## Setting up Nash webhooks

1. In the Nash Portal, open **Settings > Webhook Management**.
2. Add an endpoint with your HTTPS URL and choose the event types it should receive. You can add more endpoints to route different events to different services.
3. Open the endpoint and copy its **Signing Secret**. Store it server-side, for example as `NASH_WEBHOOK_SECRET`.
4. Use the **Event Catalog** tab to see each event's schema.
5. Set up a sandbox endpoint first and create test jobs against the sandbox API to see real events.

If your endpoint is behind a firewall, allow traffic from the IP addresses in Svix's source IP documentation.

## Securing Nash webhooks

Nash recommends the Svix SDKs, or manual verification following Svix's documented scheme. Build the signed content as `{svix-id}.{svix-timestamp}.{raw body}`, compute an HMAC-SHA256 with the base64-decoded part of your secret after `whsec_`, base64-encode it, and compare it in constant time with each `v1,` entry in `svix-signature`.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyNashSignature(rawBody, headers, secret) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader) return false;
  if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;

  // The key is the base64-decoded part of the secret after "whsec_".
  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest();

  // The header can hold several space-separated "v1,<base64>" entries.
  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/nash', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.NASH_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyNashSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  // Optional: if your own policy needs it, reject svix-timestamp values that are too old.
  const event = JSON.parse(req.body.toString('utf8'));
  // Route on `${event.type}.${event.event}`, deduplicate on svix-id, then process async.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac

def verify_nash_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("svix-id")
    timestamp = headers.get("svix-timestamp")
    signature_header = headers.get("svix-signature")
    if not (msg_id and timestamp and signature_header):
        return False
    if not isinstance(secret, str) or not secret.startswith("whsec_"):
        return False

    key = base64.b64decode(secret[len("whsec_"):])
    signed_content = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode(), expected):
            return True
    return False

```

The Svix SDKs also check that `svix-timestamp` is recent. If you verify by hand, Nash says you "will also need to implement the timestamp check yourself", so add one if your policy calls for it.

To check your verifier, use the secret `whsec_aG9va2RlY2stZ3VpZGUtdGVzdC1rZXktMzJieXRlcyE=`, the headers `svix-id: msg_2kNashTest0001` and `svix-timestamp: 1761040800`, and this exact body:

```
{"type":"delivery","event":"dropoff_complete","data":{"id":"job_R7FXJrfB99bpvpzf6CriJC"}}

```

The `svix-signature` header should be:

```
v1,T+jidJZejn4D1oZjobN/P3MGx5j/XsmEi22BqYeO4o8=

```

> **Make Nash webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the Svix signature on every Nash delivery, absorbs bursts of courier location updates, and queues delivery events durably before your service sees them.

## Nash webhook limitations and pain points

### The same event can arrive more than once

**The Problem:** Your system sends a customer two "your driver has arrived" messages for one order.

**Why It Happens:** A Nash job can have several tasks, one per delivery attempt or provider. Nash warns that "both providers might send `pickup_arrived` events". Svix retries also resend a message, with the same `svix-id`, if your endpoint didn't return a `2xx`.

**Workarounds:**

* Deduplicate deliveries on `svix-id`.
* Deduplicate business actions on the job ID, task ID and event, so a second `pickup_arrived` for the same job doesn't notify the customer again.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated deliveries based on the fields you choose, such as `data.id`, `type` and `event`.

### Providers can report completion too early

**The Problem:** You mark an order delivered while the driver is still on the way.

**Why It Happens:** Nash says some providers send statuses such as `pickup_complete` or `dropoff_complete` "when their drivers have not completed a delivery yet". Nash passes the status on.

**Workarounds:**

* Check `proofOfDelivery` before you treat a delivery as final.
* Hold customer-facing "delivered" messages until you have proof, or until a short delay has passed without a contradicting event.

**How Hookdeck Can Help:** Every event is stored in Hookdeck with its full payload, so you can see exactly which provider status arrived and when.

### Reassignment creates a new task

**The Problem:** A cancellation event makes your system think the whole delivery was cancelled.

**Why It Happens:** When a job is reassigned, Nash ends the old task with a cancellation event (such as `task.canceled_by_auto_reassign`) and creates a new task on the same job. Nothing on the old task points at its replacement.

**Workarounds:**

* Treat a task cancellation as terminal for that task only.
* Read `jobConfigurations[].advancedTask` to find the attempt that is live.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can send task events and delivery events to different handlers, so task cancellations don't reach code that acts on delivery status.

### Location updates add volume

**The Problem:** Your endpoint slows down when many deliveries are active, and the delivery status events that matter queue behind location updates.

**Why It Happens:** `courier_location.updated` is sent every 1 to 2 minutes per delivery, and with immediate location webhooks at most every 20 seconds per delivery. Each one carries the whole Job.

**Workarounds:**

* Send `courier_location` events to a separate endpoint, using Nash's event type filter.
* Store only the latest position per delivery and drop older ones.

**How Hookdeck Can Help:** Hookdeck queues every event and delivers to your service at the rate you set, so bursts of location updates don't overload your endpoint.

## Best practices

### Treat each payload as a snapshot

Each payload is the whole job as it was when the change was saved. Events raised by the same change share one snapshot. Compare `statusHistory` with what you already stored rather than assuming the payload describes only one change.

### Handle every task on a job

A cancellation or failure on one task shouldn't disrupt your handling of the job's other tasks.

### Return the right status codes

Return a `2xx` when you've accepted an event. Svix retries network errors, `5xx` responses and `408`, `409` and `429`. Use `webhook-delivery: abort-message` to stop retries for a message you will never accept, and avoid `410 Gone` unless you want the endpoint disabled.

### Acknowledge fast and process asynchronously

Return `200` after verification and queue the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Processing the same event twice should have the same result as processing it once. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Nash sends delivery, task, courier and shift events through Svix, each with `type`, `event` and a full Job snapshot in `data`, signed with an HMAC-SHA256 over `svix-id`, `svix-timestamp` and the raw body. Verify with the endpoint's `whsec_` secret, deduplicate on `svix-id` and on the job and event, treat task cancellations as task-level, and check proof of delivery before you tell a customer the order arrived.

[Hookdeck Event Gateway](https://hookdeck.com) checks every Nash signature, smooths out bursts of location updates, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Nash webhooks reliably in minutes.