Learn how to set up webhooks to receive real-time notifications about events in your AuthPI account.
Last updated 2026-06-20
Webhooks allow you to receive real-time HTTP notifications when events occur in your AuthPI account. Instead of polling the API to check for changes, webhooks push data to your server as things happen.
When a user signs up, logs in, changes their password, or joins an organization, AuthPI can immediately notify your application by sending an HTTP POST request to a URL you specify.
Real-time updates: Know immediately when something happens instead of discovering it later through polling.
Sync external systems: Keep your database, CRM, analytics, or other systems in sync with user data in AuthPI.
Trigger workflows: Automatically send welcome emails, provision resources, notify team members, or update billing systems.
Audit and compliance: Log security-relevant events to your own systems for audit trails.
Webhooks are created through the Core API. API keys authenticate with HTTP Basic auth — the key ID is the username and the secret is the password (curl’s -u flag builds the header for you). Here’s a basic example:
curl -X POST https://api.authpi.com/v1/accounts/{account_id}/webhooks \
-u "{key_id}:{key_secret}" \
-H "Content-Type: application/json" \
-d '{
"name": "User Events",
"url": "https://your-server.com/webhooks/authpi",
"events": ["user.created", "user.updated", "user.deleted"],
"auth": {
"type": "signature",
"signature_algorithm": "hmac-sha256"
}
}'
The response includes a one-time plaintext secret:
{
"id": "wh_abc123xyz",
"name": "User Events",
"url": "https://your-server.com/webhooks/authpi",
"status": "active",
"events": ["user.created", "user.updated", "user.deleted"],
"auth": {
"type": "signature",
"signature_algorithm": "hmac-sha256",
"signature_secret_hint": "...xyz789"
},
"signature_secret_plain": "whs_a1b2c3d4e5f6g7h8i9j0..."
}
Important: The signature_secret_plain field is only returned once at creation. Store it securely—you won’t be able to retrieve it again.
By default a webhook receives every subscribed event in your account. To narrow delivery to specific entities, set subjects — an array of up to 50 filters matched against each event’s subject identifiers:
curl -X POST https://api.authpi.com/v1/accounts/{account_id}/webhooks \
-u "{key_id}:{key_secret}" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Org Events",
"url": "https://your-server.com/webhooks/acme",
"events": ["organization.membership.created", "organization.membership.deleted"],
"subjects": [{ "type": "org", "id": "org_abc123" }]
}'
An event is delivered if it matches any filter (OR). Within a filter:
| Filter | Matches events where… |
|---|---|
{ "type": "org", "id": "org_abc" } | the subject’s org_id equals org_abc |
{ "type": "org" } | the subject has an org_id (any organization event) |
{ "id": "org_abc" } | any subject identifier equals org_abc |
type accepts either the entity name (org, user, issuer) or the full subject key (org_id, user_id, issuer_id). Webhooks without subjects receive all subscribed events.
AuthPI supports four authentication modes for webhooks. Choose based on your security requirements.
{
"auth": {
"type": "none"
}
}
No authentication headers are added to webhook requests. Use this only for testing or for endpoints on private networks that have other authentication mechanisms.
When to use: Development, testing, or endpoints behind a VPN/private network.
Risk: Anyone who discovers your webhook URL can send fake events.
{
"auth": {
"type": "bearer"
}
}
AuthPI generates a random token and includes it in every request:
Authorization: Bearer wht_a1b2c3d4e5f6g7h8...
Your server validates that the token matches the one you stored during webhook creation.
When to use: Simple authentication where you just need to verify requests come from AuthPI.
How to validate:
const expectedToken = process.env.AUTHPI_WEBHOOK_TOKEN;
const receivedToken = req.headers.authorization?.replace('Bearer ', '');
if (receivedToken !== expectedToken) {
return res.status(401).send('Unauthorized');
}
{
"auth": {
"type": "signature",
"signature_algorithm": "hmac-sha256"
}
}
AuthPI signs every request with HMAC-SHA256 and includes the signature in a header:
authpi-signature: t=1705330496,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The signature is computed as: HMAC-SHA256(timestamp.payload, secret)
When to use: When you need to verify both authenticity and payload integrity.
Advantages over bearer token:
{
"auth": {
"type": "bearer+signature",
"signature_algorithm": "hmac-sha256"
}
}
Combines both authentication methods for maximum security. Requests include both headers:
Authorization: Bearer wht_a1b2c3d4e5f6g7h8...
authpi-signature: t=1705330496,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
When to use: Production environments where security is important.
When using signature authentication, verify every webhook request to ensure it’s authentic and hasn’t been tampered with.
const signatureHeader = req.headers['authpi-signature'];
// Format: t=1705330496,v1=5257a869...
const [timestampPart, signaturePart] = signatureHeader.split(',');
const timestamp = parseInt(timestampPart.split('=')[1]);
const signature = signaturePart.split('=')[1];
Reject requests with timestamps too far in the past:
const currentTime = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutes
if (currentTime - timestamp > tolerance) {
return res.status(400).send('Request too old');
}
const crypto = require('crypto');
const payload = await req.text(); // Raw request body
const signedPayload = `${timestamp}.${payload}`;
const secret = process.env.AUTHPI_WEBHOOK_SECRET;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
Use constant-time comparison to prevent timing attacks:
const signatureBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
if (!crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
return res.status(401).send('Invalid signature');
}
// Signature valid - process the webhook
const event = JSON.parse(payload);
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.use('/webhooks/authpi', express.raw({ type: 'application/json' }));
app.post('/webhooks/authpi', (req, res) => {
const secret = process.env.AUTHPI_WEBHOOK_SECRET;
const signatureHeader = req.headers['authpi-signature'];
if (!signatureHeader) {
return res.status(400).send('Missing signature');
}
// Parse header
const parts = signatureHeader.split(',');
const timestamp = parseInt(parts[0].split('=')[1]);
const signature = parts[1].split('=')[1];
// Check timestamp (5 minute tolerance)
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > 300) {
return res.status(400).send('Request too old');
}
// Verify signature
const payload = req.body.toString();
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const sigBuffer = Buffer.from(signature, 'hex');
const expBuffer = Buffer.from(expected, 'hex');
if (sigBuffer.length !== expBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expBuffer)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const event = JSON.parse(payload);
console.log('Received event:', event.type, event.id);
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process asynchronously (recommended)
processWebhookAsync(event);
});
AuthPI uses exponential backoff to retry failed webhook deliveries.
Webhook delivery is at least once. Your endpoint may receive the same event more than once, especially around retry or timeout boundaries, so use the event id for idempotency. Ordering is not guaranteed across events or across different webhook endpoints.
The first delivery is attempted immediately. If it fails, a retry workflow waits initial_delay_ms, then continues with exponential backoff using backoff_factor, capped by max_delay_ms.
With the default configuration, AuthPI makes up to 40 total delivery attempts over roughly 28 hours:
| Stage | Delay After Previous | Cumulative Time |
|---|---|---|
| 1st attempt | Immediate | 0 |
| Early retries | 1s, 2s, 4s, 8s, … | First minutes |
| Capped retries | 1 hour each | Up to ~28 hours |
After the final attempt fails, the delivery is marked permanently failed and is not retried automatically again.
If you update a webhook’s URL or authentication settings while retries are pending, future retry attempts use the latest configuration. An attempt that is already in progress may still have used the previous URL or credentials.
You can customize retry behavior when creating or updating a webhook:
{
"retry": {
"max_attempts": 5,
"initial_delay_ms": 2000,
"backoff_factor": 3,
"max_delay_ms": 120000
}
}
| Setting | Default | Range | Description |
|---|---|---|---|
max_attempts | 40 | 1-100 | Total delivery attempts, including the immediate first attempt |
initial_delay_ms | 1000 | 100-60,000 | First retry delay (ms) |
backoff_factor | 2 | 1-10 | Multiplier applied after each failed attempt |
max_delay_ms | 3600000 | 1,000-3,600,000 | Maximum delay between attempts (1 hour default) |
Example: With max_attempts: 5, initial_delay_ms: 2000, backoff_factor: 3, and max_delay_ms: 120000: AuthPI makes the immediate first attempt, then up to 4 retries with delays of 2s, 6s, 18s, and 54s before marking the delivery permanently failed.
The circuit breaker protects both your server and AuthPI from wasted resources when a webhook endpoint is consistently failing.
Closed (normal): Deliveries proceed normally. Failures are counted.
Open (tripped): After too many consecutive failures, the circuit opens. New events are accepted into the delivery queue but are not attempted until the reset period elapses.
Half-Open (testing): After a recovery period, one request is attempted. If successful, the circuit closes. If it fails, it reopens.
{
"circuit_breaker": {
"failure_threshold": 10,
"reset_after_ms": 300000
}
}
| Setting | Default | Range | Description |
|---|---|---|---|
failure_threshold | 10 | 1-100 | Consecutive failures before opening |
reset_after_ms | 300000 | 1,000-86,400,000 | Time before testing recovery (5 min default) |
To manually reset: Update the webhook or toggle its status to force a reset.
Webhooks can subscribe to over 100 event types. Here are the most common ones:
| Event | Description |
|---|---|
user.created | New user account created |
user.updated | User profile or settings changed |
user.deleted | User account deleted |
user.blocked | User was blocked |
user.unblocked | User was unblocked |
user.email.verified | User verified their email address |
user.password.changed | User changed their password |
user.password.reset | User reset their password |
| Event | Description |
|---|---|
session.created | New session created |
session.terminated | Session ended (logout or revocation) |
session.expired | Session expired |
| Event | Description |
|---|---|
organization.created | New organization created |
organization.updated | Organization settings changed |
organization.deleted | Organization deleted |
organization.suspended | Organization suspended — newly issued tokens stop carrying its claims |
organization.reactivated | Suspended organization reactivated |
organization.membership.created | User added to organization |
organization.membership.updated | Membership role/scopes changed |
organization.membership.deleted | User removed from organization |
organization.invitation.created | Invitation sent |
organization.invitation.accepted | Invitation accepted |
organization.invitation.declined | Invitation declined |
| Event | Description |
|---|---|
client.created | New OAuth client registered |
client.updated | Client configuration changed |
client.deleted | Client deleted |
client.secret.rotated | Client secret was rotated |
| Event | Description |
|---|---|
issuer.created | New issuer created |
issuer.updated | Issuer configuration changed |
issuer.deleted | Issuer deleted |
| Event | Description |
|---|---|
webhook.created | New webhook created |
webhook.updated | Webhook configuration changed |
webhook.deleted | Webhook deleted |
Subscriptions use exact event names — wildcard patterns are not supported. List each event you need. The API rejects unknown event names and internal-only event types when you create or update a webhook.
All webhooks are delivered as CloudEvents 1.0 JSON payloads:
{
"specversion": "1.0",
"id": "evt_12345678-1234-1234-1234-123456789012",
"source": "com.authpi.webhooks/v2/wh_abc123xyz",
"subject": "usr_abcd1234",
"type": "user.created",
"datacontenttype": "application/json",
"time": "2024-01-15T14:22:33.123Z",
"data": {
"account_id": "acc_xyz789",
"issuer_id": "iss_xyz789",
"user_id": "usr_abcd1234",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"verified": false,
"created_at": 1705330953123
}
}
| Field | Description |
|---|---|
specversion | Always "1.0" |
id | Unique event ID (use for idempotency) |
source | Identifies the webhook that sent this event |
subject | ID of the entity the event is about (e.g., the org ID for organization.deleted). Omitted for events without a subject |
type | Event type (e.g., user.created) |
datacontenttype | Always "application/json" |
time | ISO 8601 timestamp when the event occurred |
data | Event-specific payload, plus the subject context IDs (account_id, issuer_id, and org_id/user_id where applicable) |
POST https://your-webhook-endpoint.com
Content-Type: application/json
User-Agent: AuthPI-Webhooks/2.0
Authorization: Bearer {token} # If bearer auth enabled
authpi-signature: t={timestamp},v1={sig} # If signature auth enabled
{CloudEvents JSON payload}
Return a 2xx response within 30 seconds. If your processing takes longer, acknowledge receipt immediately and process asynchronously:
app.post('/webhooks/authpi', (req, res) => {
// Acknowledge immediately
res.status(200).send('OK');
// Process in background
setImmediate(() => {
processEvent(req.body);
});
});
Webhooks provide at-least-once delivery. The same event may be delivered multiple times (due to retries or network issues). Use the event id to deduplicate:
async function processEvent(event) {
// Check if already processed
const existing = await db.events.findOne({ eventId: event.id });
if (existing) {
console.log('Event already processed:', event.id);
return;
}
// Process the event
await handleEvent(event);
// Mark as processed
await db.events.insertOne({
eventId: event.id,
processedAt: new Date()
});
}
If you can’t process an event, return a non-2xx status to trigger a retry:
app.post('/webhooks/authpi', async (req, res) => {
try {
await processEvent(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
// Return 500 to trigger retry
res.status(500).send('Processing failed');
}
});
| Method | Endpoint | Description |
|---|---|---|
POST | /v2/accounts/{account_id}/webhooks | Create webhook |
GET | /v2/accounts/{account_id}/webhooks | List webhooks |
GET | /v2/accounts/{account_id}/webhooks/{id} | Get webhook |
PATCH | /v2/accounts/{account_id}/webhooks/{id} | Update webhook |
DELETE | /v2/accounts/{account_id}/webhooks/{id} | Delete webhook |
GET | /v2/accounts/{account_id}/webhooks/{id}/deliveries | List delivery logs |
curl https://api.authpi.com/v1/accounts/{account_id}/webhooks \
-u "{key_id}:{key_secret}"
Supports pagination with limit and cursor parameters, and filtering by status.
curl -X PATCH https://api.authpi.com/v1/accounts/{account_id}/webhooks/{webhook_id} \
-u "{key_id}:{key_secret}" \
-H "Content-Type: application/json" \
-d '{
"events": ["user.created", "user.updated", "user.deleted", "user.verification.succeeded"],
"status": "active"
}'
Set a webhook’s status to disabled to pause delivery. Pending or failing deliveries stop retrying while the webhook is disabled, and they do not consume retry attempts or become permanently failed just because delivery is paused.
Set status back to active to resume retrying retained pending and failing deliveries. Delivery logs are retained for 14 days, so paused deliveries older than the retention window may be purged before you re-enable the webhook.
To rotate your webhook secret, update the auth configuration:
curl -X PATCH https://api.authpi.com/v1/accounts/{account_id}/webhooks/{webhook_id} \
-u "{key_id}:{key_secret}" \
-H "Content-Type: application/json" \
-d '{
"auth": {
"type": "signature",
"signature_algorithm": "hmac-sha256"
}
}'
The response includes the new plaintext secret. Update your server immediately—the old secret is invalidated.
Pending retries use the new secret for future attempts. An attempt already in progress may still have been signed with the previous secret.
Check the delivery history for a webhook:
curl "https://api.authpi.com/v1/accounts/{account_id}/webhooks/{webhook_id}/deliveries?limit=20" \
-u "{key_id}:{key_secret}"
Filter by status (success/failed), event_type, or time range (after/before timestamps).
Delivery logs are retained for 14 days, then automatically purged. Export anything you need for longer-term audit before then.
curl -X DELETE https://api.authpi.com/v1/accounts/{account_id}/webhooks/{webhook_id} \
-u "{key_id}:{key_secret}"
Deleting a webhook stops retry work for that webhook. Pending and failing deliveries are marked failed, and the webhook is soft-deleted before being permanently purged after 31 days.
| Resource | Limit |
|---|---|
| Webhooks per account | 50 |
| Events per webhook | 200 |
| Subject filters per webhook | 50 |
| Request timeout | 30 seconds |
| Max retry attempts | 100 (configurable) |
| Delivery log retention | 14 days |
active