---
title: List webhook deliveries
description: List a webhook’s delivery log, newest first, with the last attempt of each delivery.
doc_version: 0.1.0-preview
last_updated: 2026-09-14
---

# List webhook deliveries

`GET /webhooks/{webhook}/deliveries`

List a webhook’s delivery log, newest first, with the last attempt of each delivery.

**Permission:** Cluster key, or a scoped key with read on a namespace or collection that the webhook covers.

[API key types and authentication](/engine/reference/authentication.md).

Lists return items and next_cursor. Keep the same key and filters for the next page. List cursors expire after one hour.

A delivery is one event queued at this webhook. An attempt is one POST of the batch that held it.

file_id finds the deliveries of one file. In the log of a wider webhook, a namespace key sees only its own namespace’s events, and a collection key only its own collections’ events.

The log keeps 30 days of events.

## Example request

```http
GET /webhooks/acme-status/deliveries HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>
```

## Path parameters

### `webhook`

**string · required**

Webhook ID with the whk_ prefix, or the webhook’s name.

## Query parameters

### `status`

**string · optional · nullable**

Return only deliveries with this status.

- `pending`: Queued, or waiting for a retry.
- `succeeded`: The receiver answered 2xx to a batch that held the event.
- `failed`: Engine will not send the event again unless you replay it.

### `event_type`

**string · optional · nullable**

Return only deliveries of this event type, such as file.status_changed.

### `file_id`

**string · optional · nullable**

Return only deliveries of events that name this fil_ ID.

### `cursor`

**string · optional · nullable**

Opaque continuation token from next_cursor. Keep the same key, filters, and other request values. Omit it for the first page.

### `limit`

**integer · optional**

Maximum resources in this page. Default 50, minimum 1, maximum 100.

Default: `50`.

- Minimum: 1
- Maximum: 100

## Responses

### 200 · Success

A page of the delivery log, newest first.

Content type: `application/json`.

#### Response headers

##### `x-engine-request-id`

**string · required**

The Engine request ID. Retain it when investigating a failed or unexpected request.

##### `x-request-id`

**string · required**

The X-Request-Id you sent, echoed when it is a valid ID. Otherwise, the Engine request ID.

```json
{
  "items": [
    {
      "event_id": "evt_01K6Q6N7D8E9F0G1H2J3K4M5N6",
      "event_type": "file.status_changed",
      "occurred_at": "2026-10-06T10:05:10Z",
      "namespace": "acme",
      "collection": "incident-logs",
      "status": "succeeded",
      "attempt_count": 2,
      "next_attempt_at": null,
      "last_attempt": {
        "number": 2,
        "batch_id": "bat_01K6Q6N7D8E9F0G1H2J3K4M5N6",
        "event_count": 2,
        "status": "succeeded",
        "response_status": 204,
        "error": null,
        "duration_ms": 87,
        "sent_at": "2026-10-06T10:05:13Z"
      },
      "queued_at": "2026-10-06T10:05:11Z"
    }
  ],
  "next_cursor": null
}
```

#### Response fields

##### `items`

**array<object> · required**

Page of deliveries visible to this key. An empty array means no resources matched.

###### `items[].attempt_count`

**integer · required**

POSTs of the event’s batch since the event was last queued. A replay starts the count again.

###### `items[].collection`

**string · required · nullable**

Collection name that the event names, or null.

###### `items[].event_id`

**string · required**

The event’s evt_ ID. It is the same at every webhook and in every retry or replay.

###### `items[].event_type`

**string · required**

The event’s type, such as file.status_changed.

###### `items[].last_attempt`

**object · required · nullable**

The last POST of the newest batch that held the event. Null before the first POST.

###### `items[].last_attempt.batch_id`

**string · required**

The batch’s bat_ ID: the webhook-id header of this POST. Every retry of a batch keeps it. A replay uses a new batch.

###### `items[].last_attempt.duration_ms`

**integer · required**

Time from the start of the POST to the answer or the error, in milliseconds.

###### `items[].last_attempt.error`

**string · required · nullable**

Why the POST got no usable answer. Null when the receiver answered with a status other than 3xx.

###### `items[].last_attempt.event_count`

**integer · required**

Number of events in the batch.

###### `items[].last_attempt.number`

**integer · required**

Attempt number within the batch, from 1.

###### `items[].last_attempt.response_status`

**integer · required · nullable**

HTTP status of the receiver’s answer. Null when no answer came back.

###### `items[].last_attempt.sent_at`

**string (date-time) · required**

Time the POST was sent, as an ISO 8601 UTC timestamp.

###### `items[].last_attempt.status`

**string · required**

succeeded for a 2xx answer, retry when the batch will be sent again, failed when it will not.

- `succeeded`: The receiver answered 2xx.
- `retry`: The batch will be sent again.
- `failed`: The batch will not be sent again.

###### `items[].namespace`

**string · required · nullable**

Namespace name that the event names, or null.

###### `items[].next_attempt_at`

**string (date-time) · required · nullable**

When the next POST of a retrying batch goes. Otherwise null.

###### `items[].occurred_at`

**string (date-time) · required**

Time of the status change, as an ISO 8601 UTC timestamp.

###### `items[].queued_at`

**string (date-time) · required**

When the event was queued for this webhook, or queued again by a replay.

###### `items[].status`

**string · required**

pending (queued, or waiting for a retry), succeeded, or failed (a 3xx or 4xx answer, or the webhook turned off while the event waited).

- `pending`: Queued, or waiting for a retry.
- `succeeded`: The receiver answered 2xx to a batch that held the event.
- `failed`: Engine will not send the event again unless you replay it.

##### `next_cursor`

**string · required · nullable**

Token for the next page, or null when this result has no next page. Do not parse or modify the token.

## Errors

[Error format and retry guidance](/engine/reference/errors.md).

### 400 · `invalid_request`

A field, path, parameter, or conditional request rule is invalid.

Correct the request. Unknown fields are rejected.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "A field, path, parameter, or conditional request rule is invalid.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 400 · `invalid_cursor`

The cursor expired or does not belong to this request and key.

Restart from the first page. Preserve the same key, filters, and request values when continuing.

```json
{
  "error": {
    "code": "invalid_cursor",
    "message": "The cursor expired or does not belong to this request and key.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 401 · `unauthenticated`

The API key is missing, invalid, or revoked.

Send an active key in Authorization: Bearer or X-API-Key.

```json
{
  "error": {
    "code": "unauthenticated",
    "message": "The API key is missing, invalid, or revoked.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 403 · `forbidden`

The key can see the resource but lacks permission for this operation.

Use a key with the required action or scope kind.

```json
{
  "error": {
    "code": "forbidden",
    "message": "The key can see the resource but lacks permission for this operation.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 404 · `not_found`

The resource is absent, deleted, or outside this key’s scope.

Check the identifier and the key’s namespace and collection scope.

```json
{
  "error": {
    "code": "not_found",
    "message": "The resource is absent, deleted, or outside this key’s scope.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 503 · `temporarily_unavailable`

A dependency, cluster readiness condition, or capacity limit prevents this operation.

Inspect retryable and details. Retry only when permitted, preserving the idempotency key for the same write.

```json
{
  "error": {
    "code": "temporarily_unavailable",
    "message": "A dependency, cluster readiness condition, or capacity limit prevents this operation.",
    "request_id": "req_example",
    "retryable": true,
    "details": {}
  }
}
```

### 500 · `internal_error`

An unexpected server failure prevented this request from completing.

Retain request_id for investigation. Follow retryable before retrying a write.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An unexpected server failure prevented this request from completing.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

## Other request languages

### cURL

```bash
curl -sS "$GRAPHON_ENGINE_URL/webhooks/acme-status/deliveries" \
  -H "Authorization: Bearer $GRAPHON_ENGINE_API_KEY"
```

### Python

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    os.environ["GRAPHON_ENGINE_URL"] + "/webhooks/acme-status/deliveries",
    headers={
        "Authorization": "Bearer " + os.environ["GRAPHON_ENGINE_API_KEY"],
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.read().decode())
```

### JavaScript

```javascript
const response = await fetch(process.env.GRAPHON_ENGINE_URL + "/webhooks/acme-status/deliveries", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GRAPHON_ENGINE_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
