Webhook Listeners

How to receive, verify, and process Rex webhook events in your application.

Setting up a listener

Express.js (Node.js)

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

const app = express();
const WEBHOOK_SECRET = process.env.REX_WEBHOOK_SECRET!;

// IMPORTANT: use raw body for signature verification
app.post("/webhooks/rex", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-rex-signature"] as string;
  const payload = req.body.toString();

  if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(payload);
  console.log(`Received: ${event.event}`, event.data);

  // Process the event (do heavy work async to respond quickly)
  processEvent(event).catch(console.error);

  // Always respond 200 quickly to acknowledge receipt
  res.status(200).send("ok");
});

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const [tPart, vPart] = signature.split(",");
  const timestamp = tPart.replace("t=", "");
  const expected = vPart.replace("v1=", "");

  const signed = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signed), Buffer.from(expected));
}

app.listen(3000);

FastAPI (Python)

import hmac, hashlib, json
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
WEBHOOK_SECRET = os.environ["REX_WEBHOOK_SECRET"]

@app.post("/webhooks/rex")
async def handle_webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("x-rex-signature", "")

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = json.loads(payload)
    print(f"Received: {event['event']}", event["data"])

    # Process async
    await process_event(event)

    return {"status": "ok"}

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature.split(","))
    timestamp = parts["t"]
    expected = parts["v1"]

    signed = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + payload,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(signed, expected)

Processing events

Event routing

Route events to handlers based on type:

async function processEvent(event: WebhookEvent) {
  switch (event.event) {
    case "contact.created":
      await onContactCreated(event.data);
      break;
    case "deal.updated":
      await onDealUpdated(event.data);
      break;
    case "task.created":
      await onTaskCreated(event.data);
      break;
    default:
      console.log(`Unhandled event: ${event.event}`);
  }
}

Idempotency

Webhooks can be delivered more than once (e.g., on retry after a timeout). Make your handlers idempotent:

  • Use the event id to track which events you've already processed
  • Use upsert operations instead of creates when possible
  • Check current state before making changes
const processed = new Set<string>();

async function processEvent(event: WebhookEvent) {
  if (processed.has(event.data.id + event.event)) return;
  processed.add(event.data.id + event.event);
  // ... handle the event
}

Testing locally

Use a tunnel service to expose your local server to the internet:

# Using ngrok
ngrok http 3000

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

Then create a webhook subscription pointing to your tunnel URL:

curl -X POST "$REX_URL/webhooks" \
  -H "X-Api-Key: $REX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/rex",
    "events": ["*"],
    "secret": "whsec_test_secret"
  }'

Send a test event:

curl -X POST "$REX_URL/webhooks/wh_01HQ.../test" \
  -H "X-Api-Key: $REX_API_KEY"

What's next?

Was this page helpful?

·