Skip to content
You're offline. Some features may be unavailable.
QPortal is live. Deploy QR-powered AI instantly.

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 3000

2. 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