Guides

Webhooks

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.

Why Use Webhooks?

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.

Creating a Webhook

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.

Subject Filters

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:

FilterMatches 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.

Security Settings

AuthPI supports four authentication modes for webhooks. Choose based on your security requirements.

No Authentication

{
  "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.

Bearer Token

{
  "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');
}

HMAC Signature

{
  "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:

  • Proves the payload wasn’t tampered with
  • Includes timestamp to prevent replay attacks
  • Secret is never transmitted (only used for signing)
{
  "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.

Verifying Signatures

When using signature authentication, verify every webhook request to ensure it’s authentic and hasn’t been tampered with.

Step 1: Extract the Signature Header

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];

Step 2: Prevent Replay Attacks

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');
}

Step 3: Compute Expected Signature

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');

Step 4: Compare Signatures

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);

Complete Example (Node.js/Express)

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);
});

Retry Behavior

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.

What Triggers a Retry?

  • HTTP response status is not 2xx (e.g., 400, 500, 503)
  • Connection timeout (requests must complete within 30 seconds)
  • Network error (DNS failure, connection refused, etc.)

Default Retry Schedule

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:

StageDelay After PreviousCumulative Time
1st attemptImmediate0
Early retries1s, 2s, 4s, 8s, …First minutes
Capped retries1 hour eachUp 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.

Custom Retry Configuration

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
  }
}
SettingDefaultRangeDescription
max_attempts401-100Total delivery attempts, including the immediate first attempt
initial_delay_ms1000100-60,000First retry delay (ms)
backoff_factor21-10Multiplier applied after each failed attempt
max_delay_ms36000001,000-3,600,000Maximum 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.

Circuit Breaker

The circuit breaker protects both your server and AuthPI from wasted resources when a webhook endpoint is consistently failing.

How It Works

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.

Default Configuration

{
  "circuit_breaker": {
    "failure_threshold": 10,
    "reset_after_ms": 300000
  }
}
SettingDefaultRangeDescription
failure_threshold101-100Consecutive failures before opening
reset_after_ms3000001,000-86,400,000Time before testing recovery (5 min default)

When the Circuit Opens

  • Events that occur while the circuit is open are recorded as pending deliveries
  • The webhook status remains “active” while retries wait for the reset period
  • After the reset period, the circuit transitions to half-open for testing
  • A single successful delivery closes the circuit and resumes normal operation

To manually reset: Update the webhook or toggle its status to force a reset.

Event Types

Webhooks can subscribe to over 100 event types. Here are the most common ones:

User Events

EventDescription
user.createdNew user account created
user.updatedUser profile or settings changed
user.deletedUser account deleted
user.blockedUser was blocked
user.unblockedUser was unblocked
user.email.verifiedUser verified their email address
user.password.changedUser changed their password
user.password.resetUser reset their password

Session Events

EventDescription
session.createdNew session created
session.terminatedSession ended (logout or revocation)
session.expiredSession expired

Organization Events

EventDescription
organization.createdNew organization created
organization.updatedOrganization settings changed
organization.deletedOrganization deleted
organization.suspendedOrganization suspended — newly issued tokens stop carrying its claims
organization.reactivatedSuspended organization reactivated
organization.membership.createdUser added to organization
organization.membership.updatedMembership role/scopes changed
organization.membership.deletedUser removed from organization
organization.invitation.createdInvitation sent
organization.invitation.acceptedInvitation accepted
organization.invitation.declinedInvitation declined

Client Events

EventDescription
client.createdNew OAuth client registered
client.updatedClient configuration changed
client.deletedClient deleted
client.secret.rotatedClient secret was rotated

Issuer Events

EventDescription
issuer.createdNew issuer created
issuer.updatedIssuer configuration changed
issuer.deletedIssuer deleted

Webhook Events

EventDescription
webhook.createdNew webhook created
webhook.updatedWebhook configuration changed
webhook.deletedWebhook 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.

Payload Format

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
  }
}

CloudEvents Fields

FieldDescription
specversionAlways "1.0"
idUnique event ID (use for idempotency)
sourceIdentifies the webhook that sent this event
subjectID of the entity the event is about (e.g., the org ID for organization.deleted). Omitted for events without a subject
typeEvent type (e.g., user.created)
datacontenttypeAlways "application/json"
timeISO 8601 timestamp when the event occurred
dataEvent-specific payload, plus the subject context IDs (account_id, issuer_id, and org_id/user_id where applicable)

HTTP Request

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}

Handling Webhooks

Respond Quickly

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);
  });
});

Implement Idempotency

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()
  });
}

Handle Failures Gracefully

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');
  }
});

Managing Webhooks

API Endpoints

MethodEndpointDescription
POST/v2/accounts/{account_id}/webhooksCreate webhook
GET/v2/accounts/{account_id}/webhooksList 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}/deliveriesList delivery logs

Listing Webhooks

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.

Updating a Webhook

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"
  }'

Pausing and Resuming Delivery

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.

Rotating Secrets

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.

Viewing Delivery Logs

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.

Deleting a Webhook

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.

Limits

ResourceLimit
Webhooks per account50
Events per webhook200
Subject filters per webhook50
Request timeout30 seconds
Max retry attempts100 (configurable)
Delivery log retention14 days

Best Practices

Security

  1. Use signature authentication in production—verify every request
  2. Check timestamps to prevent replay attacks (5-minute tolerance)
  3. Use HTTPS for your webhook endpoint
  4. Store secrets securely in environment variables or a secret manager
  5. Rotate secrets regularly and after any potential exposure

Reliability

  1. Respond within 30 seconds to avoid timeouts
  2. Process asynchronously for long-running tasks
  3. Implement idempotency using the event ID
  4. Monitor delivery logs for failures
  5. Configure alerts when circuit breaker opens

Performance

  1. Subscribe only to needed events to reduce traffic
  2. Use subject filters to narrow down events further
  3. Process in batches if handling high volume
  4. Don’t block on external services in your webhook handler

Troubleshooting

Webhooks Not Arriving

  1. Check webhook status is active
  2. Verify the URL is correct and publicly accessible
  3. Check delivery logs for errors
  4. Ensure your server returns 2xx status
  5. Check if circuit breaker has opened

Signature Verification Failing

  1. Ensure you’re using the raw request body (not parsed JSON)
  2. Verify the secret matches (check for extra whitespace)
  3. Check timestamp format (Unix seconds, not milliseconds)
  4. Ensure signature comparison is constant-time

Too Many Retries

  1. Fix the underlying error causing non-2xx responses
  2. Increase timeout if processing is slow
  3. Return 200 immediately and process asynchronously
  4. Check server logs for exceptions

Circuit Breaker Tripping

  1. Check your endpoint health
  2. Review recent delivery logs for error patterns
  3. Fix the underlying issue
  4. Update the webhook to reset the circuit breaker

Next Steps