Guides
Receiving Webhooks
A complete guide to building a reliable webhook handler — from setup to signature verification to retry handling.
1. Expose a public endpoint
Your webhook URL must be publicly reachable by our servers. During development, use ngrok or smee.io to forward requests to localhost.
ngrok http 30002. Register the webhook
curl -X POST https://api.3qpr.com/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-ngrok-id.ngrok.io/webhooks/3qpr",
"events": ["scan.created", "conversation.completed"]
}'Save the secret from the response as an environment variable: THREEQPR_WEBHOOK_SECRET.
3. Always verify the signature
Every webhook delivery includes an X-3QPR-Signature header. Verify it with HMAC-SHA256 before processing the payload.
Node.js / TypeScript
import { createHmac, timingSafeEqual } from "crypto";
function verify(payload: Buffer, sig: string, secret: string): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(payload).digest("hex")}`;
return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Python
import hmac, hashlib
def verify(payload: bytes, sig: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)4. Respond quickly, process async
Return a 200 within 5 seconds. Move heavy processing to a background job to avoid timeouts.
app/api/webhooks/3qpr/route.tstypescript
export async function POST(req: NextRequest) {
const body = await req.arrayBuffer();
const sig = req.headers.get("x-3qpr-signature") ?? "";
if (!verify(Buffer.from(body), sig, process.env.THREEQPR_WEBHOOK_SECRET!)) {
return NextResponse.json({ error: "bad sig" }, { status: 400 });
}
const event = JSON.parse(Buffer.from(body).toString());
// Queue heavy work — respond fast
void processEventAsync(event);
return NextResponse.json({ ok: true }); // respond immediately
}5. Handle retries and idempotency
If your endpoint returns a non-2xx response, we retry with exponential backoff: 5s, 30s, 2m, 10m, 1h. Use the event id field to deduplicate retries.
const processedEvents = new Set<string>(); // use Redis in production
async function processEventAsync(event: ThreeQPRWebhookEvent) {
if (processedEvents.has(event.id)) return; // deduplicate
processedEvents.add(event.id);
if (event.event === "scan.created") {
await db.scans.create({ data: event.data });
}
}Production checklist
- ✓Always verify signatures — reject requests without a valid X-3QPR-Signature
- ✓Respond with 200 within 5s — offload processing to a queue
- ✓Deduplicate using the event id field
- ✓Store the webhook secret in an environment variable, never in code
- ✓Use HTTPS only for your webhook URL