Guide to PropelAuth Webhooks: Features and Best Practices
PropelAuth webhooks tell your application when something changes in your users and organisations: someone signs up, joins an organisation, has their role changed or is deleted. If you use PropelAuth, the authentication and user management platform for B2B products, webhooks are how you keep a copy of users and organisations in your own database and react to changes, such as updating billing when seats change.
This guide covers how PropelAuth webhooks are configured and signed, the events you can subscribe to, what PropelAuth does and does not document about delivery, and the best practices for running a PropelAuth receiver in production.
What are PropelAuth webhooks?
PropelAuth sends an HTTP request to your endpoint when a subscribed event happens in your project. It does not run its own delivery system: its docs say "We use Svix for our Webhooks", and link to Svix's guide for verifying them. In practice that means the signing scheme, the headers and the secret format are Svix's, and the endpoint management screens in the PropelAuth dashboard are Svix's portal.
You can configure different endpoints for your test and production environments, and each endpoint has its own signing secret.
PropelAuth webhook features
| Feature | Details |
|---|---|
| Configuration | PropelAuth dashboard, Integrations > Webhooks |
| Environments | Separate endpoints for test and production |
| Delivery platform | Svix |
| Signature headers | svix-id, svix-timestamp, svix-signature (Svix can white-label these as webhook-*) |
| Scheme | Base64 HMAC-SHA256, sent as v1,<signature> |
| Signed content | {svix-id}.{svix-timestamp}.{raw body} |
| Secret | One whsec_ secret per endpoint |
| Event catalogue | 41 types on PropelAuth's docs, with example payloads in the dashboard's event catalogue |
| Retries | Failed webhooks are retried. The schedule is not documented by PropelAuth |
| Ordering | Not guaranteed. org.updated and user.updated carry only an ID for this reason |
| Source IPs | Not published by PropelAuth |
Common events
PropelAuth's docs list 41 event types: 12 for organisations, 17 for users and 12 for emails.
| Family | Event types | Fires when |
|---|---|---|
| Users | user.created, user.updated, user.deleted, user.login, user.logged_out, user.locked, user.disabled, user.enabled, user.impersonated, user.deleted_, user.send_mfa_phone_code | A user signs up, changes, is deleted, logs in or out, is locked, disabled or re-enabled, is impersonated by your team, deletes a personal API key or requests an SMS MFA code |
| User and organisation membership | user.added_to_org, user.removed_from_org, user.invited_to_org, user.role_, user.added_to_scim_group, user.removed_ | A user joins, leaves or is invited to an organisation, changes role within it, or is added to or removed from a SCIM group |
| Organisations | org.created, org.updated, org.deleted, org.api_key_deleted, org.saml_setup, org.saml_went_live, org.saml_removed, org.scim_key_created, org.scim_key_revoked, org.scim_group_created, org.scim_group_updated, org.scim_group_deleted | An organisation is created, changed or deleted, deletes an API key, sets up, launches or removes SAML, or creates or revokes a SCIM key or group |
| Emails | email.confirmation_email, email.reset_password, email.passwordless_login, email.otp_login, email.org_invite, email.update_email, email.account_locked, email.mfa_account_locked, email.merge_, email.org_, email.waitlist_, email.waitlist_ | PropelAuth needs a transactional email sent, for an email you have switched to Webhook delivery |
The email.* events are opt-in per email. On the Emails page of the dashboard you switch an email from "PropelAuth" to "Webhook", and from then on PropelAuth stops sending that email itself and fires the matching webhook instead, so your handler is now responsible for delivering it.
Two events are deliberately thin. For org.updated and user.updated, PropelAuth says it "only record[s] the org_id" (or user_id) and asks you to fetch the latest record from its API, so that a webhook received out of order cannot overwrite newer data.
PropelAuth says every payload includes "any relevant information as well as the event type", but it does not publish a payload or name the field that holds the type. The examples are in the event catalogue inside the dashboard. Check one there, or log a real delivery, before you write routing code against a field name.
Setting up PropelAuth webhooks
- In the PropelAuth dashboard, open Integrations, then Webhooks.
- Add an endpoint for the environment you are configuring (test or production) and choose the events it should receive.
- Open the endpoint and copy its signing secret, which starts with
whsec_. Store it server-side, for example asPROPELAUTH_. Test and production endpoints have different secrets, so keep one per environment.WEBHOOK_ SECRET - To receive
email.*events, switch the relevant emails to Webhook on the Emails page.
Trigger a real event to test, such as signing up a test user in your test environment.
Securing PropelAuth webhooks
PropelAuth's webhooks are signed the way Svix signs every webhook. Strip whsec_ from the secret and base64-decode the rest to get the key bytes. Concatenate the svix-id header, a period, the svix-timestamp header, a period and the raw body. Compute an HMAC-SHA256, base64-encode it, and compare it in constant time with each v1, entry in svix-signature.
const crypto = require('crypto');
const express = require('express');
function verifyPropelAuthSignature(rawBody, headers, secret) {
// Svix's default header names; a white-labelled account sends webhook-* instead.
const prefix = headers['svix-id'] ? 'svix' : 'webhook';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signatureHeader = headers[`${prefix}-signature`];
if (!id || !timestamp || !signatureHeader) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = crypto
.createHmac('sha256', key)
.update(Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]))
.digest();
return signatureHeader.split(' ').some((entry) => {
const [version, value] = entry.split(',');
if (version !== 'v1' || !value) return false;
const received = Buffer.from(value, 'base64');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/propelauth', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.PROPELAUTH_WEBHOOK_SECRET;
if (!secret || !secret.startsWith('whsec_')) return res.sendStatus(500); // fail closed
if (!verifyPropelAuthSignature(req.body, req.headers, secret)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on the svix-id header, then route on the event type.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
def verify_propelauth_signature(raw_body: bytes, headers, secret: str) -> bool:
prefix = "svix" if headers.get("svix-id") else "webhook"
msg_id = headers.get(f"{prefix}-id")
timestamp = headers.get(f"{prefix}-timestamp")
signature_header = headers.get(f"{prefix}-signature")
if not (msg_id and timestamp and signature_header):
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed_content = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())
for entry in signature_header.split(" "):
version, _, value = entry.partition(",")
if version == "v1" and hmac.compare_digest(value.encode("utf-8"), expected):
return True
return False
# In FastAPI: raw_body = await request.body()
# verify_propelauth_signature(raw_body, request.headers, os.environ["PROPELAUTH_WEBHOOK_SECRET"])
The official svix library (npm i svix, pip install svix) does the same work and is what PropelAuth points to. It also rejects any request whose timestamp is more than 5 minutes old, which you may not want if requests can reach you late through a queue or proxy. The hand-written version above leaves that check out; add one if your receiver is always reached directly.
Make PropelAuth webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every PropelAuth delivery, whichever header prefix it uses, and durably queues each user and organisation event before your service sees it.
PropelAuth webhook limitations and pain points
The scheme is documented by Svix, not PropelAuth
The Problem: PropelAuth's own page tells you to verify webhooks but does not say how: no header names, algorithm or secret format.
Why It Happens: PropelAuth delegates delivery to Svix and links to Svix's documentation. Svix can also white-label its headers as webhook-id, webhook-timestamp and webhook-signature, and PropelAuth does not say which prefix its deliveries use.
Workarounds:
- Follow Svix's verification guide, or use the
svixlibrary. - Accept either header prefix, as the samples above do. The signed content is the same.
- Log the headers of your first delivery to confirm what you receive.
How Hookdeck Can Help: Hookdeck's PropelAuth source type reads svix-* headers and falls back to webhook-*, so either prefix verifies without code changes.
Undocumented payloads
The Problem: You cannot write a router from the public docs alone.
Why It Happens: PropelAuth publishes the event names but no example bodies. The examples live in the event catalogue behind the dashboard login, and the public page does not name the field that holds the event type.
Workarounds:
- Read the event catalogue in the dashboard before you write handlers.
- Capture a real delivery from your test environment and build against that.
- Log and acknowledge anything your router does not recognise.
How Hookdeck Can Help: Hookdeck stores every request it receives, so the first deliveries from your test environment are there to inspect in full, headers and body, before you write any routing logic.
Update events need a follow-up read
The Problem: A user.updated handler has nothing to update with.
Why It Happens: user.updated and org.updated carry only the ID, by design, so that an older delivery cannot overwrite newer data.
Workarounds:
- Fetch the current user or organisation from the PropelAuth API when these arrive.
- Coalesce bursts: several updates to one user in quick succession need only one fetch.
How Hookdeck Can Help: Hookdeck can deduplicate on the user or organisation ID within a time window, so a burst of updates becomes one fetch.
Delivery details are unstated
The Problem: You cannot plan for outages because you do not know how long PropelAuth keeps retrying.
Why It Happens: PropelAuth says webhooks "are retried if they fail" but publishes no schedule, timeout or success criteria. Svix publishes a default schedule, but PropelAuth does not say whether it uses those defaults.
Workarounds:
- Return a
2xxquickly and treat anything slow as background work. - Deduplicate on
svix-id, which Svix keeps the same across resends of a message. - After an outage, reconcile against the PropelAuth API rather than assuming every event will arrive.
How Hookdeck Can Help: Hookdeck acknowledges each delivery as it arrives and retries towards your service on a schedule you set, so PropelAuth's undocumented retry window stops mattering.
Best practices
Verify against the raw body
Svix signs the exact bytes it sends. In Express, mount express.raw() on the webhook route and parse only after verification. In Next.js, use await request.text(). In FastAPI, read await request.body().
Keep test and production apart
Use separate endpoints, and separate secrets, for the two environments. A test secret in production fails every delivery, and a shared URL mixes test users into production data.
Treat email webhooks as part of sign-in
Once an email is switched to Webhook delivery, PropelAuth no longer sends it. If your handler drops email.confirmation_email or email.reset_password, users cannot confirm their address or reset a password. Alert on failures for these events and process them first.
Deduplicate and process asynchronously
Store the svix-id of each processed message and skip repeats, then do the real work in a background job. See our guide to webhook idempotency and why to process webhooks asynchronously.
Conclusion
PropelAuth hands webhook delivery to Svix, so verification follows Svix's scheme: a base64 HMAC-SHA256 over the message ID, the timestamp and the raw body, keyed with the decoded whsec_ secret for that endpoint. Keep test and production endpoints separate, fetch the latest record when an *.updated event arrives, and handle email webhooks with the same care as the rest of your sign-in flow.
Hookdeck Event Gateway sits in front of your receiver, verifies every PropelAuth delivery, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle PropelAuth webhooks reliably in minutes.