Webhooks
Receive real-time events from GigaQR: QR created, updated, deleted, scanned, subscription changed.
Webhooks let you react to activity in your GigaQR workspace without polling. Register a URL in Dashboard → Developer → Webhooks, pick which events you care about, and we'll POST a signed JSON payload every time one of them happens.
Events
qr.created— a new QR was created (via dashboard, API, or embed widget).qr.updated— destination, name, tags, or styling changed.qr.deleted— soft-deleted.qr.scanned— someone scanned a QR. One event per scan.bulk.completed— a bulk job finished.subscription.updated— plan changed.page.submitted— a GigaPage form was submitted.
Payload shape
POST https://your-service.example.com/gigaqr
Content-Type: application/json
X-GigaQR-Event: qr.scanned
X-GigaQR-Delivery: 1c2b...
X-GigaQR-Signature: t=1719440000,v1=9f86...
{
"event": "qr.scanned",
"deliveryId": "1c2b...",
"createdAt": "2026-04-21T12:34:56Z",
"data": {
"qrId": "7f8c...",
"hash": "aB3x1q",
"scannedAt": "2026-04-21T12:34:55Z",
"country": "US",
"city": "Brooklyn",
"device": "mobile",
"os": "iOS"
}
}Verifying signatures
Every delivery is signed with your webhook's secret (shown once at create time). The X-GigaQR-Signature header contains a unix timestamp and an HMAC-SHA256 of timestamp.rawBody keyed by your secret:
import crypto from 'node:crypto'
function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=') as [string, string])
)
const t = parts.t
const sig = parts.v1
if (!t || !sig) return false
// Reject replays older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = crypto
.createHmac('sha256', secret)
.update(t + '.' + rawBody)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
}Always verify over the raw request body, before any JSON parsing — a reformat will change the bytes and invalidate the signature.
Retries
If your endpoint doesn't return a 2xx within 10 seconds, we retry with exponential backoff for up to 24 hours: at 30s, 2m, 10m, 30m, 2h, 6h, and 24h. After the last failure, the delivery is marked failed and the webhook is paused if it has accumulated too many consecutive failures.
Replay a specific failed delivery from Dashboard → Developer → Webhooks → Deliveries, or call POST /api/v1/developer/webhooks/{id}/deliveries/{deliveryId}/replay from a Clerk-authenticated session.
Scoping
By default, a webhook receives events from the whole workspace. To scope a webhook to a single QR or a single GigaPage, set scopeQrId or scopePageId when you create the endpoint.
Delivery modes
realtime— one POST per event, fired immediately.batched_5m— events are coalesced into a 5-minute batch to save the receiver processing overhead. The payload becomes an array underevents. Useful for high-scan QRs where you only care about rollups.
Alternatives
If a webhook receiver is overkill, poll /api/v1/qr or /api/v1/qr/{id}/scans directly — the REST API is cursor-paginated and plays well with a simple cron.