API Reference
Webhooks
Webhooks let you receive real-time HTTP notifications when events happen in your 3QPR account — scan events, conversation completions, and billing changes.
Event types
| Event | Description |
|---|---|
| scan.created | A QPR Code was scanned. Fires on every scan. |
| conversation.completed | An AI conversation session ended (user closed or timed out). |
| qpr_code.created | A new QPR Code was created via the API. |
| subscription.upgraded | User upgraded to a paid plan. |
| subscription.cancelled | User cancelled their subscription. |
POST
/v1/webhooksRegister a webhook
curl -X POST https://api.3qpr.com/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/3qpr",
"events": ["scan.created", "conversation.completed"]
}'Response 200json
{
"id": "wh_01HXYZ",
"url": "https://yourapp.com/webhooks/3qpr",
"events": ["scan.created", "conversation.completed"],
"secret": "whsec_AbCdEfGhIjKlMnOpQrStUv...",
"active": true,
"created_at": "2026-03-20T12:00:00Z"
}The
secret is returned only once. Save it — you'll use it to verify incoming webhook signatures.Verifying webhook signatures
Every webhook request includes an X-3QPR-Signature header. Verify it using your webhook secret to ensure the request is genuine.
verify_webhook.pypython
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""Return True if the webhook signature is valid."""
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
# In your FastAPI / Flask handler:
# signature = request.headers.get("X-3QPR-Signature", "")
# if not verify_webhook(await request.body(), signature, WEBHOOK_SECRET):
# raise HTTPException(status_code=400, detail="Invalid signature")verify-webhook.tstypescript
import { createHmac, timingSafeEqual } from "crypto";
export function verifyWebhook(
payload: Buffer,
signature: string,
secret: string
): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(payload).digest("hex")}`;
const sigBuffer = Buffer.from(signature);
const expBuffer = Buffer.from(expected);
if (sigBuffer.length !== expBuffer.length) return false;
return timingSafeEqual(sigBuffer, expBuffer);
}Payload example — scan.created
{
"event": "scan.created",
"id": "evt_01HXYZ",
"created_at": "2026-03-20T15:30:00Z",
"data": {
"scan_id": 9142,
"qpr_code_id": "qpr_01HXYZ789ABC",
"short_id": "hc9m3r",
"scanned_at": "2026-03-20T15:30:00Z",
"metadata": {}
}
}