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

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

EventDescription
scan.createdA QPR Code was scanned. Fires on every scan.
conversation.completedAn AI conversation session ended (user closed or timed out).
qpr_code.createdA new QPR Code was created via the API.
subscription.upgradedUser upgraded to a paid plan.
subscription.cancelledUser cancelled their subscription.
POST/v1/webhooks

Register 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": {}
  }
}