# Stedi Webhooks

[Stedi](https://www.stedi.com) is a healthcare EDI clearinghouse API (eligibility, claims, ERAs, payer enrollment). Its **event destinations** POST thin JSON events to your HTTPS URL, signed per the Standard Webhooks spec. This skill covers event destinations, not Stedi's Legacy webhooks.

## When to Use This Skill

* How do I receive Stedi webhooks?
* How do I verify a Stedi webhook signature?
* Where do I find my Stedi event destination signing secret?
* How do I handle `transaction.processed`, `file.failed` or `enrollment.activated` events?
* Why is my Stedi `webhook-signature` verification failing, or failing only on retries?
* How do I dedupe Stedi webhook retries?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | `msg_{UUID}`, the same on every retry |
| `webhook-timestamp` | Unix seconds when Stedi **created** the event (not when it delivered it) |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>`, several space-separated entries while a rotated secret is valid |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`, HMAC-SHA256, base64. The key is the base64-**decoded** part of the destination's secret after `whsec_`. Stedi's docs prose says `timestamp.body` (no id). That contradicts the Standard Webhooks spec and the `standardwebhooks` library the same page uses, so sign `id.timestamp.body`.

```javascript
const crypto = require('crypto');

// rawBody: exact request bytes (express.raw({ type: '*/*' })), NOT re-serialised JSON
function verifyStediWebhook(rawBody, headers, secret) {
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', key)
    .update(`${headers['webhook-id']}.${headers['webhook-timestamp']}.`)
    .update(rawBody)
    .digest('base64');
  return headers['webhook-signature'].split(' ').some((entry) => {
    const [version, sig] = entry.split(',');
    const received = Buffer.from(sig || '');
    return version === 'v1' && received.length === expected.length &&
      crypto.timingSafeEqual(received, Buffer.from(expected));
  });
}

```

The `standardwebhooks` package (`new Webhook(secret).verify(rawBody, headers)`; Python: `from standardwebhooks.webhooks import Webhook`) implements the same check, but it hard-codes a 5-minute timestamp window. Stedi retries for up to 48 hours with the original creation timestamp, so the package rejects every retry and every manual recovery. The examples verify manually with an optional window (`STEDI_TIMESTAMP_TOLERANCE_SECONDS`, off by default). Details in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stedi-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/stedi-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/stedi-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/stedi-webhooks/examples/fastapi/).

## Event Envelope

Events are **thin**: they say what changed, not the new state. Fetch the resource through the Stedi API.

```json
{ "id": "evt_a81659f1-...", "object": "v1.event", "type": "enrollment.activated",
  "account": "11111111-2222-3333-4444-555555555555", "environment": "PRODUCTION",
  "created": "2026-03-30T23:19:00.501Z",
  "resource": { "id": "019bb508-dc63-73a1-8ddc-9d4720299072", "type": "enrollment" } }

```

Some events add `relatedResources` (for example the transaction type and execution ID on `transaction.processed`). Dedupe on the `webhook-id` header.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `transaction.processed` | A transaction is received and translated (835 ERA, 277CA, or your JSON claim to X12) |
| `file.processed` | A file is processed (for example a payer's 277CA) |
| `file.failed` | A file can't be processed (parse or SFTP connection problems) |
| `enrollment.activated` | A transaction enrollment request goes `LIVE` |
| `enrollment.rejected` | The payer rejected the enrollment |
| `enrollment.updated` | An enrollment's status, payer, contacts, tasks or documents change |
| `enrollment.task.assigned` / `.completed` / `.deleted` | A provider enrollment task is assigned, completed or removed |
| `app.installed` / `app.uninstalled` | A customer installs or removes your OAuth app |
| `event.ping` | Test event from the **Ping** button |

> **Full payloads:** [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stedi-webhooks/references/overview.md) and [Stedi's event types](https://www.stedi.com/docs/healthcare/event-destinations-event-types).

## Delivery Semantics

* Only `2xx` counts as success. Redirects (3xx) are failures, so point the destination straight at the receiver (HTTPS required).
* Retries after 5 minutes, 30 minutes, 2 hours, 8 hours, 24 hours and 48 hours. If all fail, Stedi disables the destination until you re-enable it.
* Order isn't guaranteed. Duplicates happen (retries, manual recovery). Both reuse the same `webhook-id`.
* Allowlist the `Stedi-Events/` User-Agent prefix if a WAF or CDN blocks the requests. Other delivery headers: `event-id`, `destination-id`, `attempt-number`, `attempt-type`.

## Environment Variables

```bash
STEDI_WEBHOOK_SECRET=whsec_xxxxx            # destination's signing secret; handlers fail closed (500) without it
STEDI_TIMESTAMP_TOLERANCE_SECONDS=0         # optional replay window; 0 disables (retries look old)

```

Get the secret from the destination's **Overview** tab, or `GET /destinations/{id}/secret`. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stedi-webhooks/references/setup.md).

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 stedi --path /webhooks/stedi

```

Create an event destination with the printed URL, then click **Ping** on its **Event deliveries** tab.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stedi-webhooks/references/overview.md) - Event catalog, payload shapes, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stedi-webhooks/references/setup.md) - Create destinations (portal and API), secrets, rotation, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stedi-webhooks/references/verification.md) - Manual and library verification, timestamp caveat, debugging