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

# Receiving webhooks

> Verify Standard Webhooks signatures, acknowledge delivery, and deduplicate CloudEvents.

Your endpoint must accept an HTTPS `POST` whose raw body is one CloudEvents 1.0
structured JSON document. Return a `2xx` only after the event is durably safe to
acknowledge.

## Verify before parsing

Batch Relay uses the [Standard Webhooks](https://www.standardwebhooks.com/)
headers:

```text theme={null}
webhook-id: msg_...
webhook-timestamp: 1723833751
webhook-signature: v1,...
```

These three required headers are also declared on the AsyncAPI HTTP operation
through the reusable `standardWebhookHeaders` component.

Verify the signature over the exact raw request bytes before JSON parsing or
normalization. Do not reconstruct JSON, stringify a parsed object, or substitute
the CloudEvent `id` for the Standard Webhooks delivery ID.

```ts theme={null}
import { Webhook } from "standardwebhooks";

export async function POST(request: Request) {
  const rawBody = await request.text();
  const verifier = new Webhook(process.env.BATCH_RELAY_WEBHOOK_SECRET!);

  let event: Record<string, unknown>;
  try {
    event = verifier.verify(rawBody, {
      "webhook-id": request.headers.get("webhook-id") ?? "",
      "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
      "webhook-signature": request.headers.get("webhook-signature") ?? ""
    }) as Record<string, unknown>;
  } catch {
    return new Response("invalid signature", { status: 400 });
  }

  // Validate event.specversion and event.type before handling the payload.
  // Persist the CloudEvent id transactionally before returning success.
  return new Response(null, { status: 204 });
}
```

Use the current Standard Webhooks library for your language. Store the webhook
secret outside source control and rotate it in the Developer Dashboard when
needed.

## Dedupe and ordering

Deduplicate by the CloudEvent envelope `id`, not by `webhook-id`. `webhook-id`
identifies a delivery attempt; `id` identifies the immutable business event.

Keep a durable record of successfully processed CloudEvent IDs. If an event has
already been processed, safely return a `2xx` without repeating its side effect.

For stateful resources, compare `resourceversion` with the latest version your
integration has applied. Messages can be delayed, retried, or delivered out of
order. A lower resource version must not overwrite newer local state.

## Retry-safe processing

1. Read the exact raw body.
2. Verify Standard Webhooks headers.
3. Validate the CloudEvents envelope and topic schema.
4. Start a database transaction.
5. Record the CloudEvent ID and apply the side effect exactly once.
6. Commit, then return a `2xx`.

If any step cannot complete durably, return a non-`2xx` response so delivery can
be retried. Do not return success merely because background work was queued
without a durable handoff.

## Capability and destination boundaries

Only configured destinations receive topics available to their Batch Relay
account. Public topic definitions can be read here; account-specific topic
availability and destination configuration stay in the authenticated Developer
Dashboard. An unavailable capability results in no delivery, not a redacted
payload with a different shape.
