How to receive, verify, and process Rex webhook events in your application.
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);
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)
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}`);
}
}
Webhooks can be delivered more than once (e.g., on retry after a timeout). Make your handlers idempotent:
id to track which events you've already processedconst 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
}
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"