Gareth Wilson Gareth Wilson

Guide to Nash Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationNash Portal: Settings > Webhook Management
EndpointsMultiple endpoints, each with its own event type filter
EnvironmentsSandbox and production
Event CatalogA Portal tab with every event schema and example payloads for some events
Signature headerssvix-id, svix-timestamp, svix-signature
SchemeHMAC-SHA256, base64, over {svix-id}.{svix-timestamp}.{raw body}
SecretOne per endpoint, whsec_ prefixed
Event nametype and event in the body
Event IDNone in the body. svix-id stays the same when a message is resent
PayloadA snapshot of the whole Job, not a diff
RetriesImmediately, then 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours
RecoveryRetry single messages, or recover all failed messages from a date, in the Portal
Delivery controlwebhook-delivery: abort-message stops retries. webhook-delivery: disable or 410 Gone disables the endpoint
Source IPsSvix'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:

{
  "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.

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:

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 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 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 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.

Make handlers idempotent

Processing the same event twice should have the same result as processing it once. See our guide to 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 checks every Nash signature, smooths out bursts of location updates, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Nash webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.