> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Delivery Logs

> Inspect, retry, and audit individual webhook delivery attempts

# Webhook Delivery Logs

Every webhook dispatch — merchant-registered webhooks **and** app-scoped
webhooks — is persisted as a delivery log. The platform
exposes three views over these logs:

1. **Merchant-scoped** — store owner/staff inspects deliveries across
   all apps installed on their store.
2. **Developer-scoped (per-app)** — app owner inspects every delivery
   to their app, across every install.
3. **Manual retry** — kick off a fresh delivery attempt that respects
   the original `secret` and exponential-backoff retry queue.

All endpoints require merchant (or staff-admin) JWT auth.
There is no public app-scoped endpoint — apps
can subscribe to delivery-failure notifications, but cannot read merchant
logs over the OAuth API.

## Delivery log fields

| Field           | Type                    | Notes                                                                         |
| --------------- | ----------------------- | ----------------------------------------------------------------------------- |
| `deliveryId`    | UUID                    | Unique id. Surfaced as `:deliveryId` in URLs.                                 |
| `webhookId`     | string                  | The app or merchant webhook this delivery belongs to.                         |
| `webhookType`   | `'app'` \| `'merchant'` | Whether the webhook is app-registered or merchant-registered.                 |
| `storeId`       | UUID                    | The merchant who owns this delivery.                                          |
| `topic`         | string                  | e.g. `orders/create`, `customers/redact`.                                     |
| `callbackUrl`   | string                  | URL we POST to.                                                               |
| `payload`       | object                  | Exact body sent (without HMAC; HMAC is in headers).                           |
| `status`        | enum                    | `PENDING`, `RETRYING`, `SUCCESS`, `FAILED`.                                   |
| `attempts`      | integer                 | Number of delivery attempts so far (0-3).                                     |
| `lastAttemptAt` | timestamp               | When the most recent send happened.                                           |
| `nextRetryAt`   | timestamp \| null       | When the next retry is scheduled. `null` once `SUCCESS` or terminal `FAILED`. |
| `responseCode`  | integer \| null         | HTTP status from the last attempt.                                            |
| `responseBody`  | text \| null            | First few KB of the response body (truncated).                                |
| `errorMessage`  | text \| null            | Network error message (timeout, DNS, TLS) if no HTTP response.                |

## Retry Schedule

Failed dispatches retry **3 times** with exponential backoff:

| Attempt     | Delay after previous | Cumulative time |
| ----------- | -------------------- | --------------- |
| 1 (initial) | –                    | 0               |
| 2           | 1 minute             | 1m              |
| 3           | 5 minutes            | 6m              |
| 4 (final)   | 15 minutes           | 21m             |

After the fourth failed attempt, `status` is set to `FAILED` and
`nextRetryAt` is cleared. The delivery can still be retried manually
via the `/retry` endpoint below — this re-arms the row but starts the
attempt counter fresh.

## Merchant Endpoints

### `GET /apps/store/webhook-logs`

List webhook deliveries for the caller's store.

```bash theme={null}
curl -X GET "https://api.launchmystore.io/apps/store/webhook-logs?page=1&limit=20&status=FAILED" \
  -H "Authorization: Bearer MERCHANT_JWT"
```

<ParamField query="page" type="integer" default="1">Page number.</ParamField>
<ParamField query="limit" type="integer" default="20">Items per page.</ParamField>
<ParamField query="status" type="string">Filter: `PENDING`, `RETRYING`, `SUCCESS`, `FAILED`.</ParamField>
<ParamField query="topic" type="string">Filter by webhook topic (e.g. `orders/create`).</ParamField>

Response:

```json theme={null}
{
  "status": 200,
  "state": "success",
  "data": {
    "items": [
      {
        "deliveryId": "d3a1...-...-...",
        "webhookId": "w8b2...",
        "webhookType": "app",
        "topic": "orders/create",
        "callbackUrl": "https://your-app.com/webhooks/orders",
        "status": "FAILED",
        "attempts": 4,
        "lastAttemptAt": "2026-05-16T12:55:00.000Z",
        "nextRetryAt": null,
        "responseCode": 503,
        "errorMessage": null
      }
    ],
    "pagination": { "page": 1, "limit": 20, "total": 37, "hasMore": true }
  }
}
```

### `GET /apps/store/webhook-logs/:deliveryId`

Return the full record including `payload` and `responseBody`.

```json theme={null}
{
  "status": 200,
  "state": "success",
  "data": {
    "deliveryId": "d3a1...",
    "topic": "orders/create",
    "callbackUrl": "https://your-app.com/webhooks/orders",
    "payload": { "id": "ord_1a2b", "total_price": "29.99", "...": "..." },
    "status": "FAILED",
    "attempts": 4,
    "responseCode": 503,
    "responseBody": "Service Unavailable\n",
    "errorMessage": null,
    "secret": "ws_sec_..."
  }
}
```

`404 Delivery log not found` when no row matches `(deliveryId, storeId)`.

### `POST /apps/store/webhook-logs/:deliveryId/retry`

Manually trigger a fresh send. The retry uses the **original** `secret`,
`callbackUrl`, and `payload` from the row — only `attempts` is reset.

```json theme={null}
{
  "status": 200,
  "state": "success",
  "message": "Webhook delivery retry initiated",
  "data": { "deliveryId": "d3a1...", "status": "PENDING", "attempts": 0 }
}
```

## Developer Endpoints (per-app, all stores)

These views require ownership of the app — the platform verifies it
first and returns `403 Forbidden` if the JWT's storeId is not the app's
`developerId`.

### `GET /apps/developer/:appId/webhook-deliveries`

Same query parameters as the merchant endpoint, but scoped to one app
across all of its installations.

### `GET /apps/developer/:appId/webhook-deliveries/stats`

Aggregate counters (last 24h / 7d / 30d) plus per-topic failure
rates — useful for the developer dashboard's health page.

```json theme={null}
{
  "status": 200,
  "state": "success",
  "data": {
    "last24h": { "total": 4821, "success": 4789, "failed": 32 },
    "last7d":  { "total": 31204, "success": 30988, "failed": 216 },
    "byTopic": [
      { "topic": "orders/create", "total": 1942, "failureRate": 0.4 },
      { "topic": "customers/update", "total": 612, "failureRate": 1.1 }
    ]
  }
}
```

### `GET /apps/developer/:appId/webhook-deliveries/:deliveryId`

Same as the merchant single-delivery view but scoped to one app.

### `POST /apps/developer/:appId/webhook-deliveries/:deliveryId/retry`

Manually retry. Same semantics as the merchant retry, scoped to one app.

## Error Codes

| HTTP  | Error                    | When                                                         |
| ----- | ------------------------ | ------------------------------------------------------------ |
| `401` | (auth guard)             | Missing/invalid merchant JWT.                                |
| `403` | `Forbidden`              | Developer endpoints — caller doesn't own `:appId`.           |
| `404` | `Delivery log not found` | `:deliveryId` doesn't exist or belongs to another store/app. |
| `500` | (generic)                | DB read error.                                               |

## Operational Notes

* The signing secret is captured with each delivery so that retries
  (both automatic and manual) can re-sign the body. Rotating
  `clientSecret` does **not** invalidate pending retries — they continue
  with the secret as of first dispatch.
* `responseBody` is **truncated** to the first \~64KB. Use it for
  debugging; do not rely on it for full audit retention.
