# `PaperTiger.WebhookDelivery`
[🔗](https://github.com/EnaiaInc/paper_tiger/blob/v1.2.2/lib/paper_tiger/webhook_delivery.ex#L1)

Manages webhook event delivery to registered endpoints.

This GenServer delivers webhook events with:
- Stripe-compatible HMAC SHA256 signing of payloads
- Exponential backoff retry logic (max 5 attempts)
- Detailed delivery attempt tracking in Event object
- Concurrent delivery to multiple endpoints
- Optional synchronous mode for testing

## Delivery Modes

By default, webhooks are delivered asynchronously. For testing, you can enable
synchronous mode so API calls block until webhooks are delivered:

    config :paper_tiger, webhook_mode: :sync

In sync mode, `deliver_event_sync/2` is used which blocks until the webhook
is delivered (or fails after all retries).

## Delivery adapter (host-owned delivery)

How and where the signed request is sent is a pluggable
`PaperTiger.WebhookDelivery.Adapter`:

    config :paper_tiger, webhook_delivery_adapter: MyApp.WebhookSink

The default is `PaperTiger.WebhookDelivery.HTTPAdapter`, which performs the
HTTP POST itself (historical behavior). A host embedding PaperTiger
implements the behaviour to take durable ownership of delivery — persist
the webhook so it survives restarts, then deliver/retry on its own
schedule. The adapter contract (`{:ok, %Response{}}` = terminal success /
ownership taken; `{:error, reason}` = PaperTiger retries) is explicit and
enforced by a real function return, so a missing or crashing host cannot
silently drop webhooks. See `PaperTiger.WebhookDelivery.Adapter`.

This is separate from `:webhook_mode` (`:sync`/`:async`/`:collect`), which
controls *when* delivery is dispatched, not *where* it goes.

## Telemetry

`[:paper_tiger, :webhook, :delivering]` is emitted for **every** delivery,
in every adapter, immediately after the payload is signed and the headers
are built and immediately before the adapter is called.

- measurements: `%{system_time: System.system_time()}`
- metadata: `%{event: map, webhook: map, url: String.t(), payload: String.t(),
  headers: [{String.t(), String.t()}], signature_header: String.t(),
  timestamp: integer(), namespace: term()}`

This event is **observability only** — it is not the delivery mechanism.
Delivery handoff is the `Adapter` behaviour. Use this event for metrics and
tracing, not for correctness-critical work.

## Architecture

- **Async delivery**: `deliver_event/2` - Queues a delivery task (default)
- **Sync delivery**: `deliver_event_sync/2` - Blocks until complete
- **Signing**: `sign_payload/2` - Creates Stripe-compatible HMAC SHA256 signature
- **Signed request construction**: `build_signed_request/3` - Produces the
  exact raw body, `Stripe-Signature` header, and HTTP headers without
  delivering the webhook
- **HTTP client**: Uses Req library for reliable, timeout-aware requests
- **Retry strategy**: Exponential backoff (1s, 2s, 4s, 8s, 16s)
- **Tracking**: Stores delivery attempts in Event object via Store.Events

## Stripe Signature Format

The `Stripe-Signature` header follows Stripe's format:
```
Stripe-Signature: t={timestamp},v1={signature}
```

Where:
- `t` = Unix timestamp when webhook was created
- `v1` = HMAC SHA256 signature of "{timestamp}.{payload}" using webhook secret

## Examples

    # Deliver an event asynchronously (default)
    {:ok, _ref} = PaperTiger.WebhookDelivery.deliver_event("evt_123", "we_456")

    # Deliver an event synchronously (for testing)
    {:ok, :delivered} = PaperTiger.WebhookDelivery.deliver_event_sync("evt_123", "we_456")

    # Manually create a signature for testing
    signature = PaperTiger.WebhookDelivery.sign_payload("body", "secret")

    # Build the exact webhook request a controller test should verify
    request = PaperTiger.WebhookDelivery.build_signed_request(event, webhook)

# `build_signed_request`

```elixir
@spec build_signed_request(map(), map(), keyword()) ::
  PaperTiger.WebhookDelivery.Request.t()
```

Builds a Stripe-compatible signed webhook request without delivering it.

The returned `PaperTiger.WebhookDelivery.Request` contains the exact raw
JSON body and `Stripe-Signature` header that PaperTiger will hand to the
delivery adapter. Tests can pass `request.payload` and
`request.signature_header` directly to `Stripe.Webhook.construct_event/5`.

By default, the signature timestamp uses wall-clock time
(`System.system_time(:second)`), matching Stripe's webhook delivery
semantics. PaperTiger's simulated resource clock controls object timestamps,
not webhook signature freshness.

Options:

- `:namespace` - PaperTiger namespace to attach to the request.
- `:timestamp` - Signature timestamp override, useful for stale-signature
  tests.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `deliver_event`

```elixir
@spec deliver_event(String.t(), String.t()) :: {:ok, reference()} | {:error, term()}
```

Delivers a webhook event to a specific endpoint.

This function queues the delivery asynchronously. Multiple calls with different
webhook_endpoint_ids deliver to all endpoints.

## Parameters

- `event_id` - ID of the event to deliver (e.g., "evt_123")
- `webhook_endpoint_id` - ID of the webhook endpoint (e.g., "we_456")

## Returns

- `{:ok, reference}` - Delivery queued successfully
- `{:error, reason}` - Delivery could not be queued

## Examples

    {:ok, _ref} = PaperTiger.WebhookDelivery.deliver_event("evt_123", "we_456")

# `deliver_event_sync`

```elixir
@spec deliver_event_sync(String.t(), String.t()) ::
  {:ok, :delivered | :failed} | {:error, term()}
```

Delivers a webhook event synchronously, waiting for completion.

Unlike `deliver_event/2`, this function blocks until the webhook has been
delivered (or fails after all retries). Use this in test environments where
you need webhooks to be processed before assertions.

## Parameters

- `event_id` - ID of the event to deliver (e.g., "evt_123")
- `webhook_endpoint_id` - ID of the webhook endpoint (e.g., "we_456")

## Returns

- `{:ok, :delivered}` - Webhook delivered successfully
- `{:ok, :failed}` - Delivery failed after all retries
- `{:error, reason}` - Event or webhook not found

## Examples

    {:ok, :delivered} = PaperTiger.WebhookDelivery.deliver_event_sync("evt_123", "we_456")

# `sign_payload`

```elixir
@spec sign_payload(String.t(), String.t()) :: String.t()
```

Signs a payload using HMAC SHA256 (Stripe-compatible).

Creates the signature component for the `Stripe-Signature` header.
The actual signature is computed on "{timestamp}.{payload}".

## Parameters

- `payload` - JSON string (or any string data) to sign
- `secret` - Webhook secret from the webhook endpoint

## Returns

String containing the hex-encoded HMAC SHA256 signature.

## Examples

    signature = PaperTiger.WebhookDelivery.sign_payload(payload, "whsec_...")
    # Returns: "abcd1234..."

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts the WebhookDelivery GenServer.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
