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

# Delivery & Retry Policy

> How webhooks are delivered, what status codes to return, and how failed deliveries are retried

Understand how Baanx delivers webhook events, what HTTP responses are expected from your endpoint, and how automatic retries work.

## Delivery Guarantees

| Guarantee               | Detail                                                                          |
| ----------------------- | ------------------------------------------------------------------------------- |
| **At-least-once**       | Events may be delivered more than once due to retries or network conditions     |
| **No guaranteed order** | Events are not guaranteed to arrive in the same order they occurred             |
| **30-second timeout**   | Requests that do not receive a response within 30 seconds are treated as failed |
| **Idempotency**         | The same `event_id` is used across all retry attempts                           |

<Note>
  Because delivery is at-least-once, always use `event_id` as an idempotency key to prevent processing the same event twice. See [Event Types & Payload Structure](/guides/webhooks/events) for an example.
</Note>

## Responding to Webhooks

Your endpoint must return an HTTP response within **30 seconds**. To meet this reliably:

1. **Acknowledge immediately** — return a `2xx` response as soon as you've received and validated the request
2. **Process asynchronously** — push the event to an internal queue and process it in the background
3. **Verify the signature first** — return `401` if the signature is invalid (this will not be retried)

### Success Responses

Any `2xx` status code is treated as a successful delivery:

| Status           | Description                              |
| ---------------- | ---------------------------------------- |
| `200 OK`         | Event received and processed             |
| `201 Created`    | Event received and resource created      |
| `204 No Content` | Event received (no response body needed) |

### Client Error Responses — Not Retried

<Warning>
  `4xx` responses are treated as permanent failures and **will not be retried**. Use these to signal that the request itself is invalid.
</Warning>

| Status             | Description                  |
| ------------------ | ---------------------------- |
| `400 Bad Request`  | Invalid payload format       |
| `401 Unauthorized` | Invalid or missing signature |
| `403 Forbidden`    | Endpoint not authorised      |
| `404 Not Found`    | Endpoint not found           |

### Server Error Responses — Will Retry

`5xx` responses and network failures trigger automatic retries:

| Status                      | Description         |
| --------------------------- | ------------------- |
| `500 Internal Server Error` | Server error        |
| `502 Bad Gateway`           | Gateway error       |
| `503 Service Unavailable`   | Service unavailable |
| `504 Gateway Timeout`       | Request timed out   |

## Retry Policy

When delivery fails with a `5xx` response or a network error, Baanx automatically retries with exponential backoff:

| Attempt   | Delay After Failure |
| --------- | ------------------- |
| 1st retry | 1 second            |
| 2nd retry | 2 seconds           |
| 3rd retry | 4 seconds           |
| 4th retry | 8 seconds           |
| 5th retry | 16 seconds          |

**Retry rules:**

* Maximum **5 retry attempts** after the initial delivery (6 total attempts)
* Only `5xx` status codes and network failures trigger retries
* `4xx` errors are **not** retried — they indicate a client-side problem
* The same `event_id` is used across all attempts
* After all retries are exhausted, the event is marked `failed` and can be [manually retried](/api-reference/webhooks/retry-event)

## Handling Duplicates

Since retries use the same `event_id`, your endpoint should store processed event IDs and skip re-processing:

```javascript theme={null} theme={null}
async function handleWebhook(event) {
  // Check if already processed
  const existingEvent = await db.findEvent(event.event_id);
  if (existingEvent) {
    // Already handled — return success without reprocessing
    return { status: 204 };
  }

  // Process the event
  await processEvent(event);
  await db.markEventProcessed(event.event_id);

  return { status: 204 };
}
```

```python theme={null} theme={null}
async def handle_webhook(event: dict) -> dict:
    # Check if already processed
    if await db.find_event(event["event_id"]):
        return {"status": 204}

    # Process the event
    await process_event(event)
    await db.mark_event_processed(event["event_id"])

    return {"status": 204}
```

## Monitoring Deliveries

Use the API to inspect delivery history and troubleshoot failures:

<CardGroup cols={2}>
  <Card title="Webhook Delivery Logs" icon="clipboard-list" href="/api-reference/webhooks/webhook-logs">
    View recent delivery attempts for a specific webhook endpoint
  </Card>

  <Card title="Event Delivery Logs" icon="magnifying-glass" href="/api-reference/webhooks/event-logs">
    See all delivery attempts for a specific event across all webhooks
  </Card>

  <Card title="Retry an Event" icon="rotate" href="/api-reference/webhooks/retry-event">
    Manually trigger re-delivery of a failed event
  </Card>

  <Card title="List Events" icon="list" href="/api-reference/webhooks/list-events">
    Browse events and filter by status to find failures
  </Card>
</CardGroup>
