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

# Webhooks

> Configure one HTTPS endpoint, verify HMAC signatures, and handle retries.

Webhooks let Run notify your server when shipment events happen. You register **one HTTPS endpoint per Business**. Run POSTs a signed JSON envelope for each event.

## Configure

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

<Frame caption="Developers → Setup — webhook URL, signing secret, and event subscriptions">
  <img src="https://mintcdn.com/run-deliverys/n4pOxj_PjcJPxjfQ/images/dashboard/developers-setup.png?fit=max&auto=format&n=n4pOxj_PjcJPxjfQ&q=85&s=2491596f7c7bbb8e92ee25af1f989ff7" alt="Run Business Manage Webhook settings" width="2880" height="1558" data-path="images/dashboard/developers-setup.png" />
</Frame>

* On first save, the signing secret is returned **once** (looks like `whsec_••••••••••••••••` when masked) — store the full value securely.
* Default subscription when omitted: `["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**.

<Frame caption="Developers → Webhook deliveries">
  <img src="https://mintcdn.com/run-deliverys/n4pOxj_PjcJPxjfQ/images/dashboard/webhook-deliveries.png?fit=max&auto=format&n=n4pOxj_PjcJPxjfQ&q=85&s=9da0b809ff48da8550dbb6b3513bd664" alt="Webhook delivery attempts table" width="2880" height="1564" data-path="images/dashboard/webhook-deliveries.png" />
</Frame>

## Event types

| 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 status transitions |

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

See [Shipment statuses](/concepts/shipment-statuses) 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`.

| Value | Source |
| - | - |
| `orderId` | `data.order_id` (fallback `data.shipment_id`) |
| `status` | `data.status` |
| `timestamp` | `X-Run-Webhook-Timestamp` |

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

<RequestExample>
  ```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)
  }
  ```
</RequestExample>

<ResponseExample>
  ```json Envelope 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"
    }
  }
  ```
</ResponseExample>


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