# Privy Webhooks

Privy delivers webhooks through Svix, which implements Standard
Webhooks. Every delivery is an HTTP `POST` with a JSON body and three signature headers:
`svix-id`, `svix-timestamp`, and `svix-signature`. The event name is the top-level `type` field,
and the event's own fields (`user`, `wallet_id`, ...) sit beside it. There is no `data` wrapper.

## When to Use This Skill

* How do I receive Privy webhooks?
* How do I verify Privy webhook signatures (`svix-signature`)?
* How do I handle `user.created`, `user.wallet_created` or `transaction.confirmed` events?
* How do I react to `wallet.funds_deposited` or `wallet_action.swap.succeeded`?
* Why is my Privy webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256 using
the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `svix-*` headers, and when `svix-id` is absent fall back to the `webhook-*`
headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.PRIVY_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["PRIVY_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

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

The secret is on the endpoint's page under **Configuration > Webhooks** in the Privy Dashboard.
Privy's docs don't print its format, but Svix secrets start with `whsec_`. Keep the full value in
`PRIVY_WEBHOOK_SECRET` (Privy's own samples name it `PRIVY_WEBHOOK_SIGNING_SECRET`). Handlers
fail closed: with the variable unset they return 500 instead of accepting anything. They also
return 500 for a secret that doesn't start with `whsec_`, because the svix library always
base64-decodes the secret (Hookdeck would use such a secret as raw UTF-8 bytes). Missing
headers or a bad signature return 400.

Privy's `@privy-io/node` SDK also offers `privy.webhooks().verify`, but it needs the app ID and app
secret as well. The generic `svix` library above needs only the signing secret and works in every
language.

### Hookdeck retries and replays hit the 5-minute window

The `svix` and `standardwebhooks` libraries always enforce a fixed 5-minute timestamp window, and
it can't be configured. Hookdeck retries and replays resend the **original** `svix-timestamp`, so
the library rejects any retry or replay that happens more than 5 minutes after the original
delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/privy-webhooks/references/verification.md), where the window can be dropped or
widened. Deduplicate on `svix-id` (and `idempotency_key` where present) instead of relying on the
timestamp.

## Common Event Types

70 events are documented (the list may not be exhaustive). The name is `type` in the JSON body.
The examples handle these:

| Event | Triggered When |
| --- | --- |
| `privy.test` | "Test webhook" is clicked in the dashboard |
| `user.created` | A user is created in your app |
| `user.authenticated` | A user logs in |
| `user.linked_account` | A user links a new login method (also `user.unlinked_account`) |
| `user.wallet_created` | An embedded or smart wallet is created for a user |
| `user.deleted` | A user is deleted |
| `mfa.enabled` | A user enables MFA (also `mfa.disabled`) |
| `wallet.funds_deposited` | Funds are deposited into an embedded wallet (also `wallet.funds_withdrawn`) |
| `wallet.private_key_export` | A user exports their private key |
| `transaction.broadcasted` | A transaction is submitted and awaiting confirmation |
| `transaction.confirmed` | A transaction is confirmed on-chain |
| `transaction.failed` | A transaction could not be submitted |
| `transaction.execution_reverted` | A transaction confirmed but execution reverted |
| `wallet_action.swap.succeeded` | A token swap completes (also `.created`, `.failed`) |
| `intent.created` | An intent is proposed and awaits approval (also `intent.executed`) |
| `user_operation.completed` | An ERC-4337 UserOperation lands on-chain |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/privy-webhooks/references/overview.md) for the full catalog and payload shape.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
PRIVY_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 privy --path /webhooks/privy

```

Register the tunnel URL under Configuration > Webhooks in the Privy Dashboard (HTTPS only), then
click **Test webhook** to send `privy.test`. Production webhooks need Privy's Enterprise plan;
development environments don't.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/privy-webhooks/references/overview.md) - Privy webhook concepts, events, payload shape, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/privy-webhooks/references/setup.md) - Register an endpoint, get the secret, test webhook
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/privy-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window