HelloJohn / docs
Webhooks

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:

AttemptDelay
1st retry5 seconds
2nd retry30 seconds
3rd retry5 minutes
4th retry30 minutes
5th retry2 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}/replay

Next steps

On this page