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

# Webhook delivery guarantees, retries, and idempotency

> How Yoshi delivers webhooks with at-least-once semantics. Covers the retry schedule, event deduplication, ordering, and failure handling.

Yoshi delivers webhooks with at-least-once semantics. Your endpoint may receive the same event more than once, so design your handler to be idempotent.

## Delivery guarantee

| Aspect               | Behavior                                                                         |
| -------------------- | -------------------------------------------------------------------------------- |
| **Guarantee**        | At-least-once. Your endpoint may receive duplicate events.                       |
| **Success criteria** | Your server must return a `2xx` status code within **15 seconds**.               |
| **Ordering**         | Events are **not** guaranteed to arrive in order. Use `created_at` for ordering. |
| **Batching**         | One HTTP request per event per endpoint. Events are not batched.                 |

<Info>
  Return a `200` response immediately, then process the event asynchronously. If your handler takes longer than 15 seconds, the delivery will be marked as failed and retried.
</Info>

## Retry schedule

When your endpoint returns a non-`2xx` response or times out, Yoshi retries with exponential backoff:

| Attempt   | Delay      | Total elapsed |
| --------- | ---------- | ------------- |
| 1st retry | 5 seconds  | 5 seconds     |
| 2nd retry | 5 minutes  | \~5 minutes   |
| 3rd retry | 30 minutes | \~35 minutes  |
| 4th retry | 2 hours    | \~2.5 hours   |
| 5th retry | 5 hours    | \~7.5 hours   |
| 6th retry | 10 hours   | \~17.5 hours  |
| 7th retry | 10 hours   | \~27.5 hours  |

After all retries are exhausted (approximately 28 hours), the event is moved to a dead letter queue.

## Endpoint failure handling

If your endpoint consistently fails to respond:

<Steps>
  <Step title="Retries exhaust">
    After all retry attempts fail, the event is marked as failed.
  </Step>

  <Step title="Endpoint is disabled">
    If an endpoint fails repeatedly over multiple events, it is automatically disabled to prevent unnecessary traffic.
  </Step>

  <Step title="You investigate and fix">
    Check the delivery logs via `GET /v1/webhooks/deliveries` or the [consumer portal](/webhooks/overview#consumer-portal) to see response codes and error details.
  </Step>

  <Step title="Re-enable and replay">
    After fixing the issue, re-enable the endpoint via the API or portal. Use the portal's replay button to resend failed events.
  </Step>
</Steps>

## Idempotency

Every event has a unique `id` field that is stable across retries. Use this ID to deduplicate events in your handler:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const processedEvents = new Set<string>(); // In production, use Redis or a database

  app.post("/webhooks/yoshi", (req, res) => {
    const event = JSON.parse(req.body);

    if (processedEvents.has(event.id)) {
      // Already processed — return 200 to stop retries
      return res.status(200).send("OK");
    }

    processedEvents.add(event.id);

    // Process the event...
    handleEvent(event);
    res.status(200).send("OK");
  });
  ```

  ```python Python theme={null}
  processed_events: set[str] = set()  # In production, use Redis or a database

  @app.post("/webhooks/yoshi")
  def handle_webhook(request):
      event = request.json()

      if event["id"] in processed_events:
          # Already processed — return 200 to stop retries
          return Response(status_code=200)

      processed_events.add(event["id"])

      # Process the event...
      handle_event(event)
      return Response(status_code=200)
  ```
</CodeGroup>

<Info>
  For production use, store processed event IDs in a database or Redis with a TTL of at least 7 days. An in-memory set won't survive server restarts.
</Info>

## Ordering

Events are not guaranteed to arrive in the order they occurred. For example, you might receive `transaction.updated` before `transaction.created` for the same transaction.

To handle this:

* Use the `created_at` timestamp to determine the chronological order of events.
* Design your handlers to be order-independent when possible.
* If you need strict ordering, buffer events and sort by `created_at` before processing.

## Best practices

<AccordionGroup>
  <Accordion title="Return 200 immediately, process asynchronously">
    Your webhook handler should acknowledge receipt as fast as possible. Enqueue the event for background processing rather than doing work inline. This prevents timeouts and retries.
  </Accordion>

  <Accordion title="Use event IDs for deduplication">
    The `id` field is stable across retries. Store processed event IDs to prevent duplicate processing, especially for events that trigger side effects like sending emails or creating records.
  </Accordion>

  <Accordion title="Verify signatures before processing">
    Always [verify the webhook signature](/webhooks/verification) before trusting the payload. An unverified webhook could be forged by a third party.
  </Accordion>

  <Accordion title="Fetch full details from the API">
    Webhook payloads contain resource IDs and metadata, not full objects. Use the IDs to fetch current data from the API. This ensures you always have the latest state, even if events arrive out of order.
  </Accordion>

  <Accordion title="Monitor your delivery logs">
    Check `GET /v1/webhooks/deliveries` or the consumer portal regularly to catch failed deliveries early. Set up alerts if your failure rate spikes.
  </Accordion>

  <Accordion title="Handle unknown event types gracefully">
    Yoshi may add new event types in the future. If your handler encounters an event type it doesn't recognize, return `200` and ignore it. Never return an error for unknown types — that would trigger unnecessary retries.
  </Accordion>
</AccordionGroup>
