# Guide to DoorDash Drive Webhooks: Features and Best Practices

DoorDash Drive webhooks tell your application where a delivery is. When a Dasher accepts an order, arrives at the store, picks up, drops off, or the delivery is cancelled or returned, DoorDash sends the update to your endpoint. That is what powers order status pages, customer notifications and live maps for businesses that use DoorDash's Dasher network to fulfil their own orders.

This guide covers how Drive webhooks are configured and authenticated, the delivery, return and tracking events, what the payload contains at each stage, how DoorDash retries, and the practices that make a Drive receiver dependable.

## What are DoorDash Drive webhooks?

Drive webhooks are configured once per environment in the DoorDash Developer Portal. Sandbox and Production each support exactly one endpoint, and that endpoint receives every event for every delivery in the environment. Each request is a JSON object describing the delivery as it stands when the webhook is sent, with an `event_name` field saying what just happened.

DoorDash does not sign webhooks. The Portal offers two ways to protect your endpoint. **Basic Auth** sends a value you choose in the `Authorization` header. Despite the name, DoorDash sends exactly the text you enter, which is often a bare token rather than a base64 `username:password`. **OAuth** has DoorDash fetch a token from a URL you provide, using a client ID and secret.

This guide covers the Drive API. Drive (classic) has its own webhook reference.

## DoorDash Drive webhook features

| Feature | Details |
| --- | --- |
| Configuration | Developer Portal > **Webhooks** > **+** for Sandbox or Production |
| Endpoints | One per environment. Production requires production access first |
| Transport | HTTPS, JSON body |
| Authentication | **Basic Auth** (your text, sent verbatim as `Authorization`) or **OAuth** (client ID, secret, token URL, optional scope) |
| Signature and timestamp | None |
| Event field | `event_name`, at the top level |
| Payload | The delivery's current fields. Fields that are empty or not yet known are omitted |
| Time and money | Times are ISO-8601 in UTC. Amounts are in cents |
| Retries | Each event is sent up to 3 times. Anything other than `200 OK`, or no response, is retried |
| Tracking events | Opt-in by support request. Dasher location every 30 seconds |
| Refund webhooks | By approval from the DoorDash Integrations team |
| Testing | The Sandbox delivery simulator sends a webhook for every change |
| Source IPs | Not published |

## Common events

| Group | Events | Sent when |
| --- | --- | --- |
| All deliveries | `DASHER_CONFIRMED`, `DASHER_CONFIRMED_PICKUP_ARRIVAL`, `DASHER_PICKED_UP`, `DASHER_CONFIRMED_DROPOFF_ARRIVAL`, `DASHER_DROPPED_OFF`, `DELIVERY_CANCELLED` | A Dasher accepts, arrives at pickup, picks up, arrives at dropoff, completes the delivery, or the delivery is cancelled |
| Return to pickup | `DELIVERY_RETURN_INITIALIZED`, `DASHER_CONFIRMED_RETURN_ARRIVAL`, `DELIVERY_RETURNED` | A Dasher could not deliver and is returning, arrives back at pickup, or completes the return. A failed return is `DELIVERY_CANCELLED` with `cancellation_reason` `failed_to_return` |
| Pre-staged batches | `DELIVERY_BATCHED` | A delivery is assigned to a batch, identified by `force_batch_id`, before a Dasher is assigned |
| Tracking (opt-in) | `dasher_enroute_to_pickup`, `dasher_enroute_to_dropoff`, `dasher_enroute_to_return` | Every 30 seconds, starting 15 minutes before pickup or dropoff, with the Dasher's location |

Note the casing: delivery events are upper case and the tracking events are lower case. Cancellations carry a `cancellation_reason` such as `cancel_by_merchant`, `no_available_dashers`, `store_closed` or `wrong_delivery_address`.

A payload is a flat delivery object. Here is DoorDash's `DASHER_DROPPED_OFF` example, trimmed:

```json
{
  "created_at": "2022-02-01T23:18:22.791883Z",
  "event_name": "DASHER_DROPPED_OFF",
  "external_delivery_id": "c19a5d37-e457-4247-9a67-921ec0134125",
  "dasher_id": 123212,
  "dasher_name": "John D.",
  "dasher_location": { "lat": 43.333333333, "lng": -79.333333333 },
  "pickup_address": "1000 4th Avenue, Seattle, WA 98104",
  "dropoff_address": "1201 3rd Avenue, Seattle, WA 98101",
  "order_value": 5555,
  "currency": "USD",
  "updated_at": "2022-02-01T23:18:22.791883Z",
  "pickup_time_actual": "2022-02-01T23:17:20.521249Z",
  "dropoff_time_actual": "2022-02-01T23:18:22.541773Z",
  "fee": 975,
  "tip": 230,
  "support_reference": "1343593362",
  "tracking_url": "https://doordash.com/drive/portal/track/53904a0b-18cd-4308-b6dc-1d83932d7990",
  "contactless": false
}

```

`external_delivery_id` is the ID you supplied when you created the delivery, so it is the key that ties a webhook back to your order. Fields appear as they become known: `pickup_time_actual` only from `DASHER_PICKED_UP` onward, `dropoff_time_actual` and the verification image URLs from `DASHER_DROPPED_OFF`, and `return_address` from `DELIVERY_RETURN_INITIALIZED`. DoorDash tells you to check that a field is present before reading it.

Refund webhooks look different. They are sent only once DoorDash has approved you for them, and the body is an `event_data` object with `reason`, `amount`, `currency_code` and `amount_details`, with no `event_name`.

> **Inspect DoorDash Drive webhooks as they arrive.** Point your Sandbox webhook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and run the delivery simulator to see every event, with no account or setup required.

## Setting up DoorDash Drive webhooks

Generate a long random token first, for example with `openssl rand -hex 32`.

1. In the Developer Portal, click **Webhooks** in the left navigation.
2. Click **+** for the environment you want, Sandbox or Production. Production needs production access, and each environment has only one endpoint.
3. Enter your HTTPS endpoint URL.
4. Choose **Basic Auth** and enter the exact text you want in the `Authorization` header, for example `Bearer <your token>`.
5. Click **Configure Endpoint**.
6. In Sandbox, use the delivery simulator to create a delivery and advance it through its stages. Each change sends a webhook.

To put Hookdeck in front, create a Hookdeck source with the **DoorDash Drive** type and use its URL in step 3. The source has one field, **Authorization Header**: paste the value exactly as you entered it in the Developer Portal, including any `Bearer ` prefix. The source compares the whole header, so a missing prefix on either side means no match. It accepts `POST`. The Portal's **OAuth** option is not something this source type can verify, so use **Basic Auth** with Hookdeck.

Because each environment has one endpoint, use separate Hookdeck sources, or at least separate tokens, for Sandbox and Production.

## Securing DoorDash Drive webhooks

Compare the whole `Authorization` header with the value you entered in the Portal, in constant time. Do not decode it as HTTP Basic or strip a prefix: DoorDash sends your text unchanged.

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

// Exactly what you typed in the Developer Portal, e.g. "Bearer 7c1e...".
const EXPECTED = process.env.DOORDASH_DRIVE_AUTHORIZATION;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/doordash', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  let delivery;
  try {
    delivery = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  if (delivery.event_data) {
    // Refund webhook: no event_name.
  } else {
    // Route on delivery.event_name; key on delivery.external_delivery_id.
  }

  // DoorDash retries anything other than 200 OK, so send exactly 200.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

EXPECTED = os.environ.get("DOORDASH_DRIVE_AUTHORIZATION", "")

def verify_doordash_drive_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get("authorization")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

```

A static `Authorization` value proves the sender knows the secret. It does not cover the body, and there is no timestamp, so a captured request can be replayed and still pass. Serve the endpoint over HTTPS only, use a long random value, and rotate it if it might have leaked.

> **Make DoorDash Drive webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `Authorization` value you configure, queues every delivery update durably, and retries delivery to your service long after DoorDash's three attempts are spent.

## DoorDash Drive webhook limitations and pain points

### "Basic Auth" is not HTTP Basic

**The Problem:** Code that decodes the `Authorization` header as base64 `username:password` rejects every request, or accepts nothing it can parse.

**Why It Happens:** The Portal's **Basic Auth** option sends whatever text you enter, verbatim. Many integrations enter a bare token or a `Bearer` value, which is not HTTP Basic at all.

**Workarounds:**

* Store the exact text you entered and compare the whole header against it.
* If you want real HTTP Basic, enter `Basic ` followed by a base64 `username:password` yourself, and still compare the whole value.

**How Hookdeck Can Help:** Hookdeck's DoorDash Drive source compares the whole `Authorization` value you paste, whatever its format. Hookdeck answers DoorDash with a `200` either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service.

### Three attempts, then nothing

**The Problem:** A short outage or a bad deploy loses delivery updates, and your status pages stop at "picked up" for orders that were delivered.

**Why It Happens:** DoorDash sends each event at most three times. It does not document the interval between attempts, and there is no replay from the Portal.

**Workarounds:**

* Return `200 OK` as soon as the header checks out and process afterwards.
* Reconcile open deliveries against the Drive API, so a lost `DASHER_DROPPED_OFF` or `DELIVERY_CANCELLED` is caught.

**How Hookdeck Can Help:** Hookdeck accepts each request and stores it, then delivers to your service with [retries](/docs/retries) you configure and lets you replay anything that failed, so DoorDash's three attempts are spent against Hookdeck, not your service.

### Only `200 OK` counts

**The Problem:** A handler that returns `202 Accepted` or `204 No Content` receives every event three times.

**Why It Happens:** DoorDash retries any response other than `200 OK`, not any non-2xx.

**Workarounds:**

* Return exactly `200` from your webhook route.
* Make handlers idempotent anyway, because duplicates can still happen.

**How Hookdeck Can Help:** Hookdeck answers DoorDash with a `200` as soon as it has the request, so your own response codes no longer decide whether DoorDash retries.

### One endpoint per environment

**The Problem:** You cannot send delivery updates to two services, or split tracking events from status events, at the DoorDash end.

**Why It Happens:** Sandbox and Production each support a single webhook endpoint, and it receives every event.

**Workarounds:**

* Fan out from your own receiver.
* Keep the receiver thin, so one slow consumer does not delay the rest.

**How Hookdeck Can Help:** Point the one endpoint at a Hookdeck source, then use [filters](/docs/filters) on `event_name` to send status events, tracking events and refunds to different destinations.

### High-volume tracking events

**The Problem:** Turning on the `dasher_enroute_*` events multiplies traffic: one request every 30 seconds per active delivery.

**Why It Happens:** The tracking events stream the Dasher's location for the last 15 minutes before pickup and dropoff.

**Workarounds:**

* Handle tracking events on a separate, lightweight path that only updates a location cache.
* Size your endpoint for your peak number of concurrent deliveries.

**How Hookdeck Can Help:** Hookdeck can deliver tracking events to their own destination with a [max delivery rate](/docs/destinations#set-a-max-delivery-rate), so a spike in location updates never crowds out status changes.

## Best practices

### Key everything on `external_delivery_id`

Use the ID you supplied at creation to find the order. `support_reference` is the number to quote to DoorDash support, and `tracking_url` opens the delivery in the Merchant Portal.

### Treat every field as optional

Fields are omitted until they are known, so check for presence before you read anything other than `event_name` and the identifiers. A missing `dropoff_time_actual` on `DASHER_PICKED_UP` is expected.

### Deduplicate and order by `updated_at`

Retries mean the same event can arrive more than once, and nothing guarantees order. Build an idempotency key from `external_delivery_id`, `event_name` and `created_at`, and compare `updated_at` with what you stored before overwriting a status. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Verify the header, store the payload, return `200`, and send customer notifications from a background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Test the whole lifecycle in Sandbox

Run deliveries through the simulator, including cancellations and returns, before you request production access, so your handler has seen every event it will meet.

## Conclusion

DoorDash Drive sends a flat delivery object with an `event_name` to one endpoint per environment, authenticated only by the text you entered as its `Authorization` header. Compare that whole value in constant time over HTTPS, return exactly `200 OK`, key on `external_delivery_id`, treat every field as optional, and reconcile against the API so DoorDash's three attempts are not your only safety net.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, keeps every delivery update, and fans events out to as many services as you need, with retries and replay on your terms.

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