> ## Documentation Index
> Fetch the complete documentation index at: https://run-deliverys.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Order webhook

> Shipment events sent to your HTTPS endpoint, and how to verify them.

Run sends shipment events to the HTTPS endpoint on your Business. This page is only those events. Address checks are a different webhook: [Address webhook](https://run-deliverys.mintlify.site/webhooks/address.md). `shipment.status_changed` does not deliver an address check.

You register **one HTTPS endpoint per Business**. Run POSTs a signed JSON envelope to that URL.

## Configure

Set your webhook URL, subscribed event types, and signing secret from the Business Dashboard → **Developers → Setup** (dashboard session — not via API key).

![Run Business Manage Webhook settings](https://mintcdn.com/run-deliverys/n4pOxj_PjcJPxjfQ/images/dashboard/developers-setup.png?fit=max\&auto=format\&n=n4pOxj_PjcJPxjfQ\&q=85\&s=2491596f7c7bbb8e92ee25af1f989ff7)

Developers → Setup — webhook URL, signing secret, and event subscriptions

* On first save, the signing secret is returned **once** (looks like `whsec_••••••••••••••••` when masked) — store the full value securely.
* A delivery company starts with `shipment.status_changed`.
* Use **Rotate secret** if the secret may be compromised. The previous secret remains valid for about 24 hours.

Inspect attempts under **Developers → Webhook deliveries**.

![Webhook delivery attempts table](https://mintcdn.com/run-deliverys/n4pOxj_PjcJPxjfQ/images/dashboard/webhook-deliveries.png?fit=max\&auto=format\&n=n4pOxj_PjcJPxjfQ\&q=85\&s=9da0b809ff48da8550dbb6b3513bd664)

Developers → Webhook deliveries

## Event types

`data.object` is `shipment`. `data.order_id` is the order id you sign.

| Event | Description |
| - | - |
| `shipment.created` | Order created |
| `shipment.assigned` | Assigned to a rider (`data.status` is `ASSIGNED`; stored order status remains `PENDING` until accept) |
| `shipment.accepted` | Rider accepted |
| `shipment.picked_up` | Picked up |
| `shipment.in_transit` | In transit |
| `shipment.out_for_delivery` | Out for delivery |
| `shipment.delivered` | Delivered |
| `shipment.cancelled` | Cancelled |
| `shipment.status_changed` | Catch-all for shipment status transitions. It does not deliver address checks. |

One durable event is stored per transition. `type` is the most specific mapped type. Subscribing to `shipment.status_changed` receives shipment status transitions, including create.

See [Shipment statuses](https://run-deliverys.mintlify.site/concepts/shipment-statuses.md) for how lifecycle statuses map to these events.

## Event envelope

Every delivery is a JSON POST with this shape. Headers:

| Header | Description |
| - | - |
| `X-Run-Webhook-Id` | Event id (`evt_…`) |
| `X-Run-Webhook-Timestamp` | Unix seconds (use for verify + replay window) |
| `X-Run-Webhook-Signature` | HMAC-SHA256 hex |
| `X-Run-Delivery-Id` | This delivery attempt id (`dlv_…`) |

## Delivery and retries

Return a **2xx** status to acknowledge receipt.

| Outcome | Behavior |
| - | - |
| Timeouts, connection errors, `408` / `429` / `5xx` | Retried with exponential backoff (+ jitter). `Retry-After` is honored when present |
| Most other `4xx` | Dead-lettered |

Delivery is **at-least-once**. Retries reuse the same event `id` (`evt_…`) — make your handler idempotent. Run does **not** promise global ordering; use `occurred_at`, `previous_status`, and `status`.

In production, webhook URLs must use HTTPS. Run blocks private/reserved destinations and disables redirects when delivering.

## Verify signatures

HMAC-SHA256 of `orderId + status + timestamp`, keyed with your `whsec_…` secret. Compare the hex digest to `X-Run-Webhook-Signature`. The signature is not a hash of the JSON body.

| Value | Source |
| - | - |
| `orderId` | `data.order_id`. If that is missing, use `data.shipment_id`. |
| `status` | `data.status` |
| `timestamp` | `X-Run-Webhook-Timestamp` |

Reject timestamps older than about 5 minutes. Always verify before trusting the body.

```http Headers theme={null}
POST <your webhook_url>
Content-Type: application/json
User-Agent: Run-Webhooks/1.0
X-Run-Webhook-Id: evt_01hxyz...
X-Run-Webhook-Timestamp: 1723467600
X-Run-Webhook-Signature: 9f2c...a1b3
X-Run-Delivery-Id: dlv_01hxyz...
```

```javascript Node.js theme={null}
const crypto = require('crypto');

function verify(secret, orderId, status, timestampHeader, signatureHeader, maxAgeSec = 300) {
  const age = Math.abs(Date.now() / 1000 - Number(timestampHeader));
  if (age > maxAgeSec) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${orderId}${status}${timestampHeader}`)
    .digest('hex');

  return expected === String(signatureHeader).trim();
}
```

```python Python theme={null}
import hashlib
import hmac
import time


def verify(secret, order_id, status, timestamp, signature_header, max_age_sec=300):
    if abs(time.time() - int(timestamp)) > max_age_sec:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{order_id}{status}{timestamp}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header.strip())
```

```php PHP theme={null}
function verify_run_webhook(
  string $secret,
  string $orderId,
  string $status,
  string $timestamp,
  string $signatureHeader,
  int $maxAgeSec = 300
): bool {
  if (abs(time() - (int) $timestamp) > $maxAgeSec) {
    return false;
  }
  $expected = hash_hmac('sha256', $orderId . $status . $timestamp, $secret);
  return hash_equals($expected, trim($signatureHeader));
}
```

```go Go theme={null}
package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "math"
  "strconv"
  "strings"
  "time"
)

func verify(secret, orderId, status, timestamp, signatureHeader string, maxAgeSec int64) bool {
  ts, err := strconv.ParseInt(timestamp, 10, 64)
  if err != nil {
    return false
  }
  if int64(math.Abs(float64(time.Now().Unix()-ts))) > maxAgeSec {
    return false
  }
  mac := hmac.New(sha256.New, []byte(secret))
  mac.Write([]byte(orderId + status + timestamp))
  return hex.EncodeToString(mac.Sum(nil)) == strings.TrimSpace(signatureHeader)
}
```

```json Shipment theme={null}
{
  "id": "evt_01hxyz...",
  "type": "shipment.out_for_delivery",
  "api_version": "2026-08-01",
  "created_at": "2026-08-12T12:00:00.000Z",
  "occurred_at": "2026-08-12T11:59:58.000Z",
  "data": {
    "object": "shipment",
    "shipment_id": "6a7c75f801df3fbb7fad0c58",
    "order_id": "RUN-A1B2C3D4",
    "status": "HEADING_TO_DELIVERY",
    "previous_status": "PICKED_UP",
    "cancel": false,
    "pickup": {
      "address": "12 Admiralty Way, Lekki, Lagos",
      "contact_name": "Acme Stores",
      "contact_phone": "+2348011111111"
    },
    "dropoff": {
      "address": "15 Admiralty Way, Lekki, Lagos",
      "contact_name": "Adewale Johnson",
      "contact_phone": "+2348022222222"
    },
    "picked_at": "2026-08-12T11:40:00.000Z",
    "delivered_at": null,
    "placement_id": null,
    "price": 2500,
    "currency": "NGN"
  }
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.