# Webhook Strategies

## Retry behavior

When your endpoint fails to respond with a `2xx` status code, Apiera retries delivery using an exponential backoff
schedule:

| Attempt | Delay | Cumulative |
|  --- | --- | --- |
| 1 | Immediate | - |
| 2 | 30 seconds | 30s |
| 3 | 1 minute | 1m 30s |
| 4 | 5 minutes | 6m 30s |
| 5 | 15 minutes | 21m 30s |
| 6 | 30 minutes | 51m 30s |
| 7 | 1 hour | 1h 51m |
| 8 | 2 hours | 3h 51m |
| 9 | 4 hours | 7h 51m |
| 10 | 8 hours | 15h 51m |
| 11 | 24 hours | ~40 hours |


After 11 attempts (spanning approximately 40 hours), the delivery is marked as **failed**.

### What counts as success or failure

| Your response | Result |
|  --- | --- |
| `2xx` | **Delivered.** No further attempts. |
| `429` | **Retried.** Your endpoint is rate-limiting us. |
| `5xx` | **Retried.** Your server had an error. |
| `4xx` (except 429) | **Failed.** Permanent client error, not retried. |
| Connection refused / timeout | **Retried.** Network-level failure. |


Return `2xx` as quickly as possible. Process the webhook asynchronously after acknowledging receipt. If your endpoint
takes too long to respond (beyond the subscription's `requestTimeoutMs`, default 10 seconds), the attempt is treated
as a timeout and retried.

### Request timeout

Each subscription has a configurable `requestTimeoutMs` (default: 10,000ms). If your endpoint does not respond within
this window, the request is aborted and treated as a failed attempt.

Set this when creating or updating your subscription:


```bash
curl -X PATCH https://api.apiera.io/v1/webhook-subscriptions/{uuid} \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{ "requestTimeoutMs": 15000 }'
```

## Ordering guarantees

Apiera does **not** guarantee strict ordering of webhook deliveries. Events are delivered with best-effort ordering,
but network conditions, retries, and concurrent processing can cause events to arrive out of sequence.

**This is by design.** The thin payload architecture means ordering doesn't matter. You always fetch current state
from the API, so you always get the latest data regardless of which event triggered the fetch.

If you receive `product.attribute.linked` before `product.created` for the same product, both events point to the same
product UUID. Fetching the product returns its current state either way.

## Idempotency

Your webhook handler should be **idempotent**. Processing the same event more than once should produce the same
result. Duplicate deliveries can occur in edge cases such as:

- Network issues where your `2xx` response doesn't reach Apiera.
- Infrastructure recovery after outages.


**Use `webhook-id` for deduplication.** Each delivery has a unique `webhook-id` header. Track which IDs you've
processed and skip duplicates:


```csharp
app.MapPost("/webhooks/apiera", async (HttpContext context, IDeduplicationStore store) =>
{
    var webhookId = context.Request.Headers["webhook-id"].ToString();

    if (await store.HasBeenProcessedAsync(webhookId))
        return Results.Ok(new { status = "duplicate" });

    // Process event...

    await store.MarkProcessedAsync(webhookId);
    return Results.Ok(new { status = "accepted" });
});
```

Since payloads are thin (just UUIDs) and you always fetch current state, duplicate processing is generally harmless
even without deduplication. You just fetch the same current state twice. But deduplication avoids unnecessary API calls
and downstream side effects.

## Responding to webhooks

### Do

- **Return `2xx` immediately.** Acknowledge receipt before doing any processing.
- **Process asynchronously.** Queue the event for background processing.
- **Verify the signature.** Always validate HMAC-SHA256 before trusting the payload.
- **Handle unknown event types gracefully.** Return `200` for event types you don't handle. New types may be added at
any time.


### Don't

- **Don't return `4xx` for events you don't handle.** This causes the delivery to be marked as permanently failed.
Return `200` instead.
- **Don't do heavy processing synchronously.** Your endpoint has a limited timeout window.
- **Don't rely on event ordering.** Always fetch current state from the API.


### Recommended handler structure


```
1. Read headers and body
2. Verify HMAC signature -> reject if invalid (return 401)
3. Return 200 OK immediately
4. (Async) Check webhook-id for deduplication
5. (Async) Route by X-Webhook-Event-Type header
6. (Async) Fetch current state from REST API using payload UUIDs
7. (Async) Apply business logic
```

## Monitoring and debugging

Use the delivery and attempt endpoints to debug failures:


```bash
# List failed deliveries
curl "https://api.apiera.io/v1/webhook-subscriptions/{uuid}/deliveries?statuses[eq]=failed" \
  -H "Authorization: Bearer {token}"

# Inspect attempts for a specific delivery
curl https://api.apiera.io/v1/webhook-subscriptions/{uuid}/deliveries/{deliveryUuid}/attempts \
  -H "Authorization: Bearer {token}"
```

Each attempt records:

| Field | Description |
|  --- | --- |
| `attemptNumber` | Which attempt this was (1-11). |
| `statusCode` | HTTP status code your endpoint returned (0 for network failures). |
| `responseBody` | First 2048 characters of your response body. |
| `responseTimeMs` | How long the request took in milliseconds. |


## Subscription suspension

If deliveries consistently fail, the subscription may be automatically **suspended**. When suspended:

- No new events are created for the subscription.
- Pending deliveries continue their retry schedule.


Reactivate a suspended subscription after fixing the issue:


```bash
curl -X PATCH https://api.apiera.io/v1/webhook-subscriptions/{uuid}/actions/lifecycle \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{ "transition": "activate" }'
```