Prerequisites
Before setting up webhooks, make sure you have:
A publicly accessible HTTPS endpoint
A way to store secrets securely (environment variables, secrets manager)
Step 1: Create Your Endpoint
Create an HTTP POST endpoint to receive webhook events. Here’s a minimal example:
Node.js (Express)
Python (Flask)
Next.js
const express = require ( 'express' );
const app = express ();
// Use raw body for signature verification
app . post ( '/webhooks/jasni' ,
express . raw ({ type: 'application/json' }),
( req , res ) => {
// Acknowledge immediately
res . status ( 200 ). send ( 'OK' );
// Parse and process
const event = JSON . parse ( req . body . toString ());
console . log ( 'Received:' , event . event );
// Handle the event
handleEvent ( event );
}
);
app . listen ( 3000 );
from flask import Flask, request
app = Flask( __name__ )
@app.route ( '/webhooks/jasni' , methods = [ 'POST' ])
def handle_webhook ():
# Acknowledge immediately
event = request.get_json()
print ( f "Received: { event[ 'event' ] } " )
# Handle the event
handle_event(event)
return 'OK' , 200
// app/api/webhooks/jasni/route.ts
import { NextRequest , NextResponse } from 'next/server' ;
export async function POST ( request : NextRequest ) {
const event = await request . json ();
console . log ( 'Received:' , event . event );
// Handle the event
await handleEvent ( event );
return NextResponse . json ({ received: true });
}
Step 2: Register Your Webhook
Use the Jasni API to register your endpoint:
cURL
TypeScript SDK
Python SDK
curl -X POST https://api.jasni.ai/api/v1/webhooks \
-H "Authorization: Bearer jsk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/jasni",
"events": ["email.received", "email.sent"],
"description": "Production webhook"
}'
import Jasni from '@jasni/sdk' ;
const jasni = new Jasni ({ apiKey: 'jsk_your_api_key' });
const { webhook } = await jasni . webhooks . create ({
url: 'https://your-server.com/webhooks/jasni' ,
events: [ 'email.received' , 'email.sent' ],
description: 'Production webhook' ,
});
// IMPORTANT: Save this secret!
console . log ( 'Webhook Secret:' , webhook . secret );
from jasni import Jasni
jasni = Jasni( api_key = "jsk_your_api_key" )
webhook = jasni.webhooks.create(
url = "https://your-server.com/webhooks/jasni" ,
events = [ "email.received" , "email.sent" ],
description = "Production webhook"
)
# IMPORTANT: Save this secret!
print ( f "Webhook Secret: { webhook.secret } " )
Save the secret immediately! The webhook secret is only shown once when creating the webhook. Store it securely in your environment variables.
The response will include:
{
"success" : true ,
"data" : {
"webhook" : {
"id" : "wh_abc123" ,
"url" : "https://your-server.com/webhooks/jasni" ,
"events" : [ "email.received" , "email.sent" ],
"secret" : "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ,
"active" : true ,
"description" : "Production webhook"
}
}
}
Step 3: Add Signature Verification
Always verify that webhooks are genuinely from Jasni:
const crypto = require ( 'crypto' );
function verifySignature ( payload , signature , secret ) {
const expected = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expected )
);
}
app . post ( '/webhooks/jasni' ,
express . raw ({ type: 'application/json' }),
( req , res ) => {
const signature = req . headers [ 'x-webhook-signature' ];
const payload = req . body . toString ();
if ( ! verifySignature ( payload , signature , process . env . WEBHOOK_SECRET )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
res . status ( 200 ). send ( 'OK' );
const event = JSON . parse ( payload );
handleEvent ( event );
}
);
import hmac
import hashlib
import os
WEBHOOK_SECRET = os.environ.get( 'WEBHOOK_SECRET' )
def verify_signature ( payload : bytes , signature : str ) -> bool :
expected = hmac.new(
WEBHOOK_SECRET .encode( 'utf-8' ),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route ( '/webhooks/jasni' , methods = [ 'POST' ])
def handle_webhook ():
signature = request.headers.get( 'X-Webhook-Signature' )
payload = request.get_data()
if not verify_signature(payload, signature):
return 'Invalid signature' , 401
event = request.get_json()
handle_event(event)
return 'OK' , 200
Step 4: Handle Events
Implement handlers for each event type you’ve subscribed to:
async function handleEvent ( event ) {
switch ( event . event ) {
case 'email.received' :
// New email arrived - trigger your AI agent
await processIncomingEmail ( event . data );
break ;
case 'email.sent' :
// Email sent successfully - update your records
await logSentEmail ( event . data );
break ;
case 'email.bounced' :
// Delivery failed - handle the bounce
await handleBounce ( event . data );
break ;
case 'email.spam' :
// Email marked as spam
await logSpamEmail ( event . data );
break ;
case 'email.deleted' :
// Email was deleted
await syncDeletion ( event . data );
break ;
default :
console . log ( 'Unknown event:' , event . event );
}
}
Step 5: Test Your Integration
Local Development with ngrok
Use ngrok to expose your local server:
# Start your local server
npm run dev # Running on http://localhost:3000
# In another terminal, expose it publicly
ngrok http 3000
Use the ngrok URL (e.g., https://abc123.ngrok.io/webhooks/jasni) when creating your test webhook.
Send a Test Event
Create a simple script to simulate webhook events:
const crypto = require ( 'crypto' );
const secret = process . env . WEBHOOK_SECRET ;
const payload = JSON . stringify ({
event: 'email.received' ,
timestamp: new Date (). toISOString (),
data: {
id: 'test-123' ,
from: '[email protected] ' ,
to: [ '[email protected] ' ],
subject: 'Test Email' ,
account: '[email protected] ' ,
folder: 'INBOX' ,
text: 'This is a test email for webhook testing.' ,
},
});
const signature = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
// Send to your endpoint
fetch ( 'http://localhost:3000/webhooks/jasni' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-Webhook-Signature' : signature ,
'X-Webhook-Timestamp' : Math . floor ( Date . now () / 1000 ). toString (),
'X-Webhook-Event' : 'email.received' ,
},
body: payload ,
}). then ( res => console . log ( 'Response:' , res . status ));
Best Practices
Respond Quickly
Webhook requests timeout after 30 seconds . Return a 200 response immediately, then process asynchronously.
app . post ( '/webhooks/jasni' , ( req , res ) => {
// Acknowledge FIRST
res . status ( 200 ). send ( 'OK' );
// Process AFTER responding
processEventAsync ( req . body ). catch ( console . error );
});
Handle Duplicates
The same event might be delivered multiple times. Use idempotency:
const processedEvents = new Set (); // Use Redis in production
async function handleEvent ( event ) {
const eventId = ` ${ event . event } - ${ event . data . id } - ${ event . timestamp } ` ;
if ( processedEvents . has ( eventId )) {
console . log ( 'Duplicate, skipping:' , eventId );
return ;
}
processedEvents . add ( eventId );
await processEvent ( event );
}
Use a Message Queue
For high-volume webhooks, queue events for processing:
import { Queue , Worker } from 'bullmq' ;
const webhookQueue = new Queue ( 'webhooks' );
// Endpoint adds to queue
app . post ( '/webhooks/jasni' , async ( req , res ) => {
await webhookQueue . add ( 'process' , req . body );
res . status ( 200 ). send ( 'OK' );
});
// Worker processes queue
new Worker ( 'webhooks' , async ( job ) => {
await processEvent ( job . data );
});
Deployment Checklist
Use HTTPS - Never use HTTP in production
Verify signatures - Always validate X-Webhook-Signature
Store secrets securely - Use environment variables or a secrets manager
Handle duplicates - Implement idempotency
Respond quickly - Return 200 within 30 seconds
Log events - Track received webhooks for debugging
Monitor errors - Set up alerts for failures
Test thoroughly - Verify all event types work correctly
Troubleshooting
Webhook not receiving events
Verify your endpoint is publicly accessible
Check that the webhook is active (active: true)
Ensure you’re subscribed to the correct events
Check your server logs for incoming requests
Signature verification failing
Use the raw request body , not parsed JSON
Ensure you’re using the correct webhook secret
Check that the secret hasn’t been rotated
Events arriving late or duplicated
Jasni retries failed deliveries (10s, 60s, 300s delays)
Implement idempotency to handle duplicates
Ensure your endpoint responds with 200 quickly
The secret is only shown once at creation
Delete the webhook and create a new one
Save the new secret immediately
Next Steps