← Back to Documentation

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

Implementation 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
end

Best 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/engageplus

Using webhook.site

For quick testing without code, use webhook.site:

  1. 1. Go to webhook.site and copy your unique URL
  2. 2. Add it as a webhook endpoint in EngagePlus
  3. 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/webhooks

Returns 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=0

Returns delivery logs for a specific webhook with pagination.