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

# Webhooks

> How Offload sends async case state changes back to your application.

If you provide `clientWebhookUrl` when creating a case, Offload sends JSON `POST` requests when the case reaches one of its outward-facing async states.

## Events You Can Receive

| Event               | When it is sent                                                 |
| ------------------- | --------------------------------------------------------------- |
| `case.completed`    | The worker marked the case `COMPLETED`.                         |
| `case.failed`       | The worker or AgentMail event handler marked the case `FAILED`. |
| `case.input_needed` | The worker paused the case and needs input from your app.       |

There are no webhook events for:

* case creation
* case accepted
* case resumed after input
* case currently running

## Delivery Behavior

The current implementation sends webhooks like this:

* method: `POST`
* header: `content-type: application/json`
* body: serialized JSON event envelope
* success rule: any HTTP `2xx`
* retry behavior: none in application code
* signing: none

If the request fails or returns a non-`2xx`, Offload logs the failure and drops it.

<Warning>
  This is the most important webhook constraint in the current implementation: delivery is best-effort, single-attempt, and unsigned. Use HTTPS, dedupe with `event_id`, and run a polling reconciliation job for important workflows.
</Warning>

## Event Envelope

```json theme={null}
{
  "event_id": "evt_4fc5904d-0e44-4ed3-aa57-c4a47ad0ae4f_case_completed_1774131900000",
  "event_type": "case.completed",
  "timestamp": 1774131900000,
  "data": {}
}
```

`event_id` is deterministic for a specific case update:

* it includes the case ID
* it includes the event type
* it includes the case `updatedAt` timestamp

That makes it a good dedupe key on your side.

## Recommended Handler Pattern

```ts theme={null}
export async function POST(request: Request) {
  const event = await request.json();

  // 1. Deduplicate by event_id
  // 2. Persist the full payload
  // 3. Queue your own downstream work
  // 4. Return a fast 2xx

  return new Response(null, { status: 204 });
}
```

## Attachment Handling

Webhook payloads include attachment metadata in `data.attachments`, but not a durable file URL.

If you need the file:

1. read `attachmentId` from the webhook payload
2. call `GET /cases/{id}/attachments/{attachmentId}`
3. use the returned `downloadUrl` before `expiresAt`

## Metadata Handling

If you passed `metadata` when creating the case, Offload includes it in webhook payloads. This is useful because `GET /cases/{id}` does not currently return `metadata`.

## Reconciliation Strategy

For production use, a reliable pattern is:

1. use webhooks for low-latency updates
2. store `event_id`
3. periodically poll active or recently updated cases
4. repair any missed terminal or input-needed transitions from polling results
