Webhook Integration Guide
Receive real-time authentication events with secure, standards-based webhooks.
Quick Start
1. Create a Webhook Endpoint
Navigate to Dashboard → Integrations and click Create Webhook.
Configuration:
- • Name: A friendly identifier (e.g., "Production Webhook")
- • Endpoint URL: Your HTTPS endpoint (e.g., https://api.example.com/webhooks/engageplus)
- • Events: Select
AUTH_SUCCESS - • Data Scope: Choose UUID only or include user data
2. Save Your Signing Secret
Important!
The signing secret is shown only once. Save it securely (e.g., in your environment variables).
3. Implement Your Endpoint
See the implementation examples below for your preferred language/framework.
Webhook Payload Format
Webhooks are delivered as Security Event Tokens (SET) following RFC 8417.
HTTP Request
POST /your-webhook-endpoint HTTP/1.1
Host: api.example.com
Content-Type: application/secevent+jwt
User-Agent: EngagePlus-Webhooks/1.0
X-EngagePlus-Event: AUTH_SUCCESS
X-EngagePlus-Signature: eyJhbGciOiJSUzI1NiIsInR5cCI6InNlY2V2ZW50K2p3dCIsImtpZCI6IjEyMyJ9...
eyJhbGciOiJSUzI1NiIsInR5cCI6InNlY2V2ZW50K2p3dCIsImtpZCI6IjEyMyJ9...JWT Structure
Header
{
"alg": "RS256",
"typ": "secevent+jwt",
"kid": "abc123" // Key ID for JWKS lookup
}Payload (UUID Only)
{
"sub": "103425823847234892347", // User UUID from IdP
"events": {
"https://engageplus.id/events/auth_success": {
"organizationId": "org_abc123",
"provider": {
"id": "provider_xyz789",
"type": "GOOGLE"
},
"timestamp": 1698765432
}
},
"iss": "https://engageplus.id",
"iat": 1698765432,
"jti": "unique-event-id-abc123"
}Payload (Include User Data)
{
"sub": "103425823847234892347",
"events": {
"https://engageplus.id/events/auth_success": {
"organizationId": "org_abc123",
"provider": {
"id": "provider_xyz789",
"type": "GOOGLE"
},
"timestamp": 1698765432,
"user": {
"email": "john@example.com",
"name": "John Doe",
"picture": "https://lh3.googleusercontent.com/...",
"email_verified": true
}
}
},
"iss": "https://engageplus.id",
"iat": 1698765432,
"jti": "unique-event-id-abc123"
}Signature Verification
Always verify signatures!
Never trust webhook data without verifying the JWT signature. This prevents spoofing attacks.
JWKS Endpoint
Verify signatures using our public JSON Web Key Set (JWKS):
https://engageplus.id/.well-known/jwks.jsonImplementation Examples
Node.js (Express + jose)jose docs
import express from 'express';
import * as jose from 'jose';
const app = express();
// IMPORTANT: Use raw body for JWT
app.post('/webhooks/engageplus',
express.text({ type: 'application/secevent+jwt' }),
async (req, res) => {
try {
const jwt = req.body;
// Create JWKS client
const JWKS = jose.createRemoteJWKSet(
new URL('https://engageplus.id/.well-known/jwks.json')
);
// Verify JWT signature
const { payload } = await jose.jwtVerify(jwt, JWKS, {
issuer: 'https://engageplus.id',
});
// Get event data
const eventType = req.headers['x-engageplus-event'];
const event = payload.events['https://engageplus.id/events/auth_success'];
console.log('User authenticated:', {
userId: payload.sub,
organizationId: event.organizationId,
provider: event.provider.type,
});
// Your business logic here
await handleUserAuthentication(payload.sub, event);
// Respond with 200
res.status(200).send('OK');
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(401).send('Unauthorized');
}
}
);
app.listen(3000);Python (Flask + PyJWT)PyJWT docs
from flask import Flask, request
import jwt
import requests
app = Flask(__name__)
@app.route('/webhooks/engageplus', methods=['POST'])
def webhook():
try:
token = request.get_data(as_text=True)
# Fetch JWKS
jwks_url = 'https://engageplus.id/.well-known/jwks.json'
jwks_client = jwt.PyJWKClient(jwks_url)
# Get signing key
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Verify and decode JWT
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer='https://engageplus.id'
)
# Extract event data
event = payload['events']['https://engageplus.id/events/auth_success']
user_id = payload['sub']
print(f"User {user_id} authenticated via {event['provider']['type']}")
# Your business logic
handle_user_authentication(user_id, event)
return 'OK', 200
except Exception as e:
print(f'Webhook verification failed: {e}')
return 'Unauthorized', 401
if __name__ == '__main__':
app.run(port=3000)PHP (firebase/php-jwt)php-jwt docs
<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
// Get raw POST body
$jwt = file_get_contents('php://input');
try {
// Fetch JWKS
$jwks_url = 'https://engageplus.id/.well-known/jwks.json';
$jwks_json = file_get_contents($jwks_url);
$jwks = json_decode($jwks_json, true);
// Decode and verify JWT
$decoded = JWT::decode($jwt, JWK::parseKeySet($jwks));
// Extract event data
$event = $decoded->events->{'https://engageplus.id/events/auth_success'};
$user_id = $decoded->sub;
error_log("User $user_id authenticated via " . $event->provider->type);
// Your business logic
handleUserAuthentication($user_id, $event);
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
error_log('Webhook verification failed: ' . $e->getMessage());
http_response_code(401);
echo 'Unauthorized';
}
?>Ruby (Sinatra + jwt)ruby-jwt docs
require 'sinatra'
require 'jwt'
require 'net/http'
require 'json'
post '/webhooks/engageplus' do
begin
token = request.body.read
# Fetch JWKS
jwks_url = 'https://engageplus.id/.well-known/jwks.json'
jwks_response = Net::HTTP.get(URI(jwks_url))
jwks = JSON.parse(jwks_response)
# Verify and decode JWT
decoded = JWT.decode(
token,
nil,
true,
{
algorithms: ['RS256'],
iss: 'https://engageplus.id',
jwks: jwks
}
)
payload = decoded[0]
event = payload['events']['https://engageplus.id/events/auth_success']
user_id = payload['sub']
puts "User #{user_id} authenticated via #{event['provider']['type']}"
# Your business logic
handle_user_authentication(user_id, event)
status 200
body 'OK'
rescue => e
puts "Webhook verification failed: #{e.message}"
status 401
body 'Unauthorized'
end
endBest Practices
Return 200 Quickly
Respond with HTTP 200 within 10 seconds. If you need to perform long-running operations, queue the webhook for background processing.
// ✅ Good - Queue for background processing
app.post('/webhook', async (req, res) => {
await queue.add('process-webhook', { payload: req.body });
res.status(200).send('OK'); // Respond immediately
});Handle Duplicates
Use the jti (JWT ID) claim to detect and ignore duplicate webhook deliveries.
// Track processed JTIs in Redis/database
const processedJTIs = new Set();
if (processedJTIs.has(payload.jti)) {
console.log('Duplicate webhook, ignoring');
return res.status(200).send('OK');
}
processedJTIs.add(payload.jti);
// Process webhook...Secure Your Endpoint
- •Use HTTPS only (TLS 1.2+)
- •Implement rate limiting (e.g., max 100 requests/minute)
- •Use a dedicated endpoint path (not easy to guess)
- •Monitor for unusual patterns or attacks
Log Everything
Keep detailed logs for debugging. Include:
- •Timestamp and event type
- •User ID and organization ID
- •Success/failure status
- •Error messages (if any)
Troubleshooting
Webhook Not Received
- 1.Check endpoint URL: Must be publicly accessible via HTTPS
- 2.Check firewall: Allow incoming connections
- 3.Check webhook status: Ensure it's ACTIVE in the dashboard
- 4.View delivery logs: Check the Integrations page for error messages
Signature Verification Failed
- 1.Check JWKS URL: Use
https://engageplus.id/.well-known/jwks.json - 2.Check issuer: Must be
https://engageplus.id - 3.Clock skew: Allow ±5 minutes for time differences
- 4.Raw body: Don't parse JSON before verifying - use raw JWT string
Timeout Errors
- 1.Respond quickly: Return 200 within 10 seconds
- 2.Use queue: Process webhooks asynchronously in background
- 3.Optimize database: Avoid slow queries in webhook handler
Testing Your Webhook
Local Development with ngrok
Use ngrok to expose your local server:
# Start your local server
npm start # or python app.py, etc.
# In another terminal, start ngrok
ngrok http 3000
# Use the ngrok URL in EngagePlus dashboard
https://abc123.ngrok.io/webhooks/engageplusUsing webhook.site
For quick testing without code, use webhook.site:
- 1. Go to webhook.site and copy your unique URL
- 2. Add it as a webhook endpoint in EngagePlus
- 3. Trigger an auth event and view the payload on webhook.site
Viewing Delivery Logs
In the EngagePlus dashboard, go to Integrations and click the logs icon next to your webhook to see all delivery attempts, including status codes and error messages.
API Reference
List Webhooks
GET /api/webhooksReturns all webhooks for your organization.
Create Webhook
POST /api/webhooks
Content-Type: application/json
{
"name": "Production Webhook",
"endpointUrl": "https://api.example.com/webhooks",
"events": ["AUTH_SUCCESS"],
"dataScope": "UUID_ONLY" | "INCLUDE_USER_DATA"
}Creates a new webhook. Returns the webhook object including the signing secret (shown only once).
Update Webhook
PUT /api/webhooks/{id}
Content-Type: application/json
{
"status": "ACTIVE" | "PAUSED" | "DISABLED",
"name": "Updated Name",
"endpointUrl": "https://new-url.com/webhook",
"dataScope": "UUID_ONLY" | "INCLUDE_USER_DATA"
}Delete Webhook
DELETE /api/webhooks/{id}Deletes the webhook and all associated logs.
Get Webhook Logs
GET /api/webhooks/{id}/logs?limit=50&offset=0Returns delivery logs for a specific webhook with pagination.