Webhooks
Receive real-time event notifications from HelloJohn — user created, login, MFA enrolled, tenant changes, and more. Setup, security, and retry behavior.
HelloJohn sends HTTP POST requests to your endpoint when events occur — a user logs in, MFA is enrolled, a tenant is created, etc. Use webhooks to sync data to your database, trigger onboarding flows, send notifications, or audit auth events.
Setup
Register a webhook endpoint
POST /v2/admin/webhooks
Authorization: Bearer $ADMIN_TOKEN
Content-Type: application/json
{
"url": "https://yourapp.com/webhooks/hellojohn",
"events": ["user.created", "user.login", "mfa.enrolled"],
"secret": "whsec_your-signing-secret",
"tenant_id": null // null = all tenants, or specify a tenant ID
}Response:
{
"id": "wh_01HX...",
"url": "https://yourapp.com/webhooks/hellojohn",
"events": ["user.created", "user.login", "mfa.enrolled"],
"created_at": "2026-01-15T10:00:00Z"
}Handle incoming events
HelloJohn sends a POST request to your URL with a JSON body:
{
"id": "evt_01HX...",
"type": "user.created",
"created_at": "2026-03-07T14:00:00Z",
"tenant_id": "ten_01HX...",
"data": {
"user": {
"id": "usr_01HX...",
"email": "alice@acme.com",
"roles": ["member"]
}
}
}Verify the signature
Always verify that the request came from HelloJohn before processing it. See Webhook Verification →.
Webhook handler examples
import express from 'express'
import { verifyWebhookSignature } from '@hellojohn/js'
const app = express()
// Use raw body for signature verification
app.post('/webhooks/hellojohn',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-hellojohn-signature']
try {
const event = verifyWebhookSignature(
req.body,
signature,
process.env.HELLOJOHN_WEBHOOK_SECRET
)
switch (event.type) {
case 'user.created':
await syncUserToDatabase(event.data.user)
break
case 'user.login':
await logLoginEvent(event)
break
}
res.json({ received: true })
} catch (err) {
res.status(400).json({ error: 'Invalid signature' })
}
}
)// app/api/webhooks/hellojohn/route.ts
import { verifyWebhookSignature } from '@hellojohn/nextjs/server'
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('x-hellojohn-signature') ?? ''
let event
try {
event = verifyWebhookSignature(
body,
signature,
process.env.HELLOJOHN_WEBHOOK_SECRET!
)
} catch {
return Response.json({ error: 'Invalid signature' }, { status: 400 })
}
if (event.type === 'user.created') {
await db.users.upsert({
where: { hellojohnId: event.data.user.id },
create: {
hellojohnId: event.data.user.id,
email: event.data.user.email,
tenantId: event.tenant_id,
},
update: {}
})
}
return Response.json({ received: true })
}func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", 400)
return
}
sig := r.Header.Get("X-HelloJohn-Signature")
event, err := hellojohn.VerifyWebhook(body, sig, os.Getenv("HELLOJOHN_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "invalid signature", 400)
return
}
switch event.Type {
case "user.created":
var payload struct {
User hellojohn.User `json:"user"`
}
json.Unmarshal(event.Data, &payload)
syncUser(payload.User)
}
w.WriteHeader(http.StatusOK)
}from flask import Flask, request, jsonify
from hellojohn import verify_webhook_signature
app = Flask(__name__)
@app.route('/webhooks/hellojohn', methods=['POST'])
def webhook():
body = request.get_data()
sig = request.headers.get('X-HelloJohn-Signature', '')
try:
event = verify_webhook_signature(
body, sig, os.environ['HELLOJOHN_WEBHOOK_SECRET']
)
except ValueError:
return jsonify(error='Invalid signature'), 400
if event['type'] == 'user.created':
sync_user(event['data']['user'])
return jsonify(received=True)Retries
If your endpoint returns a non-2xx response or times out (30s), HelloJohn retries the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 5 minutes |
| 4th retry | 30 minutes |
| 5th retry | 2 hours |
After 5 failed attempts, the event is marked as failed. You can replay it manually from the admin panel.
Return 200 OK as quickly as possible — do heavy processing asynchronously (job queue). HelloJohn considers a delivery successful when it receives any 2xx response within 30 seconds.
Managing webhooks
# List all webhooks
GET /v2/admin/webhooks
# Update a webhook
PATCH /v2/admin/webhooks/{webhookId}
{ "events": ["user.created", "user.deleted"], "active": true }
# Pause a webhook
PATCH /v2/admin/webhooks/{webhookId}
{ "active": false }
# Delete a webhook
DELETE /v2/admin/webhooks/{webhookId}
# View delivery history
GET /v2/admin/webhooks/{webhookId}/deliveries?status=failed
# Replay a failed delivery
POST /v2/admin/webhooks/{webhookId}/deliveries/{deliveryId}/replayNext steps
Per-tenant Configuration
Configure authentication settings per tenant in HelloJohn — allowed auth methods, MFA policy, allowed email domains, OAuth apps, and JWT customization.
Event Reference
Complete list of HelloJohn webhook events — user, session, MFA, tenant, and organization events with full payload examples.