Gareth Wilson Gareth Wilson

Guide to PropelAuth Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationPropelAuth dashboard, Integrations > Webhooks
EnvironmentsSeparate endpoints for test and production
Delivery platformSvix
Signature headerssvix-id, svix-timestamp, svix-signature (Svix can white-label these as webhook-*)
SchemeBase64 HMAC-SHA256, sent as v1,<signature>
Signed content{svix-id}.{svix-timestamp}.{raw body}
SecretOne whsec_ secret per endpoint
Event catalogue41 types on PropelAuth's docs, with example payloads in the dashboard's event catalogue
RetriesFailed webhooks are retried. The schedule is not documented by PropelAuth
OrderingNot guaranteed. org.updated and user.updated carry only an ID for this reason
Source IPsNot published by PropelAuth

Common events

PropelAuth's docs list 41 event types: 12 for organisations, 17 for users and 12 for emails.

FamilyEvent typesFires when
Usersuser.created, user.updated, user.deleted, user.login, user.logged_out, user.locked, user.disabled, user.enabled, user.impersonated, user.deleted_personal_api_key, user.send_mfa_phone_codeA 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 membershipuser.added_to_org, user.removed_from_org, user.invited_to_org, user.role_changed_within_org, user.added_to_scim_group, user.removed_from_scim_groupA user joins, leaves or is invited to an organisation, changes role within it, or is added to or removed from a SCIM group
Organisationsorg.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_deletedAn 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
Emailsemail.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_account_with_sso, email.org_now_requires_2fa, email.waitlist_user_confirmation, email.waitlist_user_invitationPropelAuth 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

  1. In the PropelAuth dashboard, open Integrations, then Webhooks.
  2. Add an endpoint for the environment you are configuring (test or production) and choose the events it should receive.
  3. Open the endpoint and copy its signing secret, which starts with whsec_. Store it server-side, for example as PROPELAUTH_WEBHOOK_SECRET. Test and production endpoints have different secrets, so keep one per environment.
  4. 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 svix library.
  • 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 2xx quickly 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.