---
title: "Get a webhook delivery"
method: GET
path: "/v1/projects/{project_id}/webhook-deliveries/{delivery_id}"
tags: ["Webhooks"]
---

# Get a webhook delivery

`GET /v1/projects/{project_id}/webhook-deliveries/{delivery_id}`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/webhook-deliveries/{delivery_id}`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

One delivery, including the exact payload that was signed and sent.

## Response `200`

The delivery.

- WebhookDelivery: One event addressed to one endpoint, and everything that happened to it.
  - `id` string, required: Public delivery ID (whd_ prefix). Sent with the request as `X-Naturali-Delivery`.
  - `project_id` string, required
  - `webhook_id` string, required
  - `event_id` string, required: The event's id; shared by every delivery and redelivery of it.
  - `type` 'conversation.started' | 'message.received' | 'generation.completed' | 'generation.failed' | 'usage.threshold_crossed' | 'project.paused' | 'project.resumed' | 'decision.completed' | 'decision.failed' | 'task.created' | 'task.transitioned' | 'task.closed' | 'task.migrated' | 'task.stalled' | 'task.paused' | 'task.resumed' | 'task.approval_failed' | 'task.automation_retrying' | 'task.automation_rejected' | 'task.automation_unrouted' | 'approval.created' | 'approval.approved' | 'approval.rejected' | 'approval.expired', required: A naturali event type. The set below is what this API emits — deliberately only the events it can itself cause, so no declared name is one that never fires. It grows as modules land.
  - `payload` Event, required: The envelope POSTed to your endpoint. It is also what a delivery's `payload` holds, byte for byte, so the signed body and the recorded one are the same document.
    - `id` string, required: Unique per event (`evt_` prefix), and stable across redeliveries — dedupe on this if your receiver must process an event exactly once.
    - `type` 'conversation.started' | 'message.received' | 'generation.completed' | 'generation.failed' | 'usage.threshold_crossed' | 'project.paused' | 'project.resumed' | 'decision.completed' | 'decision.failed' | 'task.created' | 'task.transitioned' | 'task.closed' | 'task.migrated' | 'task.stalled' | 'task.paused' | 'task.resumed' | 'task.approval_failed' | 'task.automation_retrying' | 'task.automation_rejected' | 'task.automation_unrouted' | 'approval.created' | 'approval.approved' | 'approval.rejected' | 'approval.expired', required: A naturali event type. The set below is what this API emits — deliberately only the events it can itself cause, so no declared name is one that never fires. It grows as modules land.
    - `project_id` string, required
    - `resource_type` 'conversation' | 'message' | 'generation' | 'usage_threshold' | 'project' | 'decision' | 'task' | 'approval', required: What the event is about.
    - `resource_id` string, required: The id of that resource, in the form its own API uses.
    - `data` object, required: The resource, shaped exactly as its own API returns it — a `generation.completed` payload carries the same object `getGeneration` would.
    - `created_at` string, date-time, required
  - `status` 'pending' | 'success' | 'failed', required: `pending` until a 2xx is received (`success`) or the attempts are exhausted (`failed`). A failed delivery can be replayed with `…:redeliver`.
  - `status_code` integer, nullable, required: The receiver's HTTP status on the last attempt; null when the attempt never got a response (DNS, TLS, refused, timed out).
  - `attempts` integer, required: Attempts made so far.
  - `next_attempt_at` string, date-time, nullable, required: When the next retry is due; null once the delivery is terminal.
  - `last_attempt_at` string, date-time, nullable, required
  - `response_body` string, nullable, required: A truncated snippet of the receiver's response, or the transport error when there was no response.
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `401`: Missing or invalid credentials.
- `404`: The resource does not exist (existence is not leaked).

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
