Agent skill
Privy Webhooks Skill
Receive and verify Privy webhooks (auth and embedded-wallet infrastructure, delivered via Svix). Use when setting up Privy webhook handlers, debugging svix-signature verification, or handling events like user.created, user.wallet_created, transaction.confirmed, and wallet.funds_deposited.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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_createdortransaction.confirmedevents? - How do I react to
wallet.funds_depositedorwallet_?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.
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
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/, examples/nextjs/, 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_). 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, 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_ | 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_ | A transaction confirmed but execution reverted |
wallet_ | 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 for the full catalog and payload shape.
Environment Variables
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
PRIVY_WEBHOOK_SECRET=whsec_xxxxx
Local Development
# 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 - Privy webhook concepts, events, payload shape, retries
- references/setup.md - Register an endpoint, get the secret, test webhook
- references/verification.md - Svix verification details, header fallback, manual verifier, timestamp window