HelloJohn / docs
Webhooks

Webhook Setup

Create and configure webhook endpoints to receive HelloJohn events.

Webhook Setup

Webhooks let HelloJohn push real-time notifications to your server when events occur — user created, session revoked, MFA enrolled, and more.


Step 1: Create an Endpoint in Your App

Your server needs a public HTTPS endpoint that accepts POST requests:

import express from "express";
import crypto from "crypto";

const app = express();

// Use raw body for signature verification
app.use("/webhooks/hellojohn", express.raw({ type: "application/json" }));

app.post("/webhooks/hellojohn", (req, res) => {
  const sig = req.headers["hellojohn-signature"] as string;
  const payload = req.body.toString();

  // Verify signature (see below)
  if (!verifySignature(payload, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(payload);
  console.log("Received event:", event.type);

  // Always respond quickly — process async if needed
  res.sendStatus(200);

  // Process the event after responding
  handleEvent(event).catch(console.error);
});

function verifySignature(payload: string, signature: string, secret: string) {
  const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

Requirements for your endpoint:

  • Must be HTTPS (TLS)
  • Must respond within 30 seconds
  • Must return 2xx status to acknowledge delivery
  • Any other status code is treated as failure and triggers retries

Step 2: Register the Webhook

Via Dashboard

Go to Tenant Settings → Webhooks → Add Endpoint.

Via API

curl -X POST "https://api.hellojohn.dev/v1/webhooks" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.your-app.com/webhooks/hellojohn",
    "events": ["user.created", "user.deleted", "session.created"],
    "description": "User lifecycle events"
  }'

Response: 201 Created

{
  "id": "wh_01HABCDEF777001",
  "url": "https://api.your-app.com/webhooks/hellojohn",
  "events": ["user.created", "user.deleted", "session.created"],
  "signing_secret": "whsec_abc123defghijklmnop...",
  "created_at": "2024-01-15T10:00:00Z"
}

Save the signing_secret — it's shown once and used to verify webhook payloads.


Step 3: Configure Your Environment

# .env
WEBHOOK_SECRET=whsec_abc123defghijklmnop...

Step 4: Send a Test Event

Verify your endpoint receives events:

curl -X POST "https://api.hellojohn.dev/v1/webhooks/wh_01HABCDEF777001/test" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{"event": "user.created"}'

Check your server logs to confirm receipt.


Subscribing to Events

Use ["*"] to subscribe to all events:

{ "events": ["*"] }

Or specify individual events:

{
  "events": [
    "user.created",
    "user.deleted",
    "session.created",
    "mfa.enrolled",
    "org.member.added"
  ]
}

See the full Events Reference.


Local Development

For local development, use a tunnel to expose your local server:

# Using ngrok
ngrok http 3000

# Using Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000

Register the tunnel URL as a webhook endpoint while developing.


Multiple Endpoints

You can register multiple endpoints — useful for different services subscribing to different events:

# Analytics service
POST /v1/webhooks  url: analytics.example.com, events: ["user.created", "session.created"]

# Billing service
POST /v1/webhooks  url: billing.example.com, events: ["user.deleted"]

# Audit service
POST /v1/webhooks  url: audit.example.com, events: ["*"]

Disabling a Webhook

Temporarily disable without deleting:

curl -X PATCH "https://api.hellojohn.dev/v1/webhooks/wh_01HABCDEF777001" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

On this page