Receive scans in your CRM
Get every scan pushed to your endpoint the moment it happens, verify it, and store it against the contact or campaign it belongs to.
Two ways to get scan data out: webhooks push each scan to you within a second (Business), and GET /scans lets you pull them in pages on your own schedule (PRO and up). Most CRM integrations use the webhook for freshness and the pull for reconciliation.
Option A: webhooks
1. Stand up an endpoint
An https URL on a public hostname that answers 2xx quickly. Do the work after responding; deliveries have a ten-second timeout and are not retried.
2. Register it and connect codes
curl -X POST https://api.kuikcode.com/v1/webhooks \
-H "Authorization: Bearer kc_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://crm.example.com/hooks/kuikcode", "codeIds": ["K02bEPC2SmtopwPr6S-jt", "Zq9vX1mN4kLp8rTb2cWd0"] }'The response includes the endpoint's id and its secret. One endpoint exists per URL; posting the same URL again returns the existing one. PATCH /webhooks/{id} with codeIds replaces the connected set, so adding a code is a read-modify-write.
3. Verify every delivery
POST /hooks/kuikcode
X-KuikCode-Event: scan.created
X-KuikCode-Signature: sha256=3f1c...
{ "event": "scan.created", "timestamp": "...", "code": { "id": "Ab3dE9xYz", "name": "Front window" },
"scan": { "scanId": "...", "country": "US", "region": "NJ", "city": "Montclair", "deviceType": "mobile",
"os": "iOS", "browser": "Safari", "language": "en", "timezone": "America/New_York",
"destination": "https://example.com/summer-menu", "matchedRule": null } }Compute HMAC-SHA256 over the raw body with the secret and compare to the header in constant time:
import { createHmac, timingSafeEqual } from "node:crypto"
export function verify(rawBody, header, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex")
return header?.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(header))
}Reject anything that fails. code.id in the payload is the printed short code; map it to your record through the mapping you stored when creating the codes.
4. Send a test
POST /webhooks/{id}/test fires a sample event with "test": true and reports the status your server returned. Use it after every deploy of the endpoint.
Option B: pull with GET /scans
For a nightly sync or a backfill, page through scans with a window and a cursor:
curl "https://api.kuikcode.com/v1/scans?from=2026-09-01T00:00:00Z&to=2026-09-07T00:00:00Z&limit=100" \
-H "Authorization: Bearer kc_live_..."Follow next_cursor until it is null. Filter by codeId, folderId or tag to scope the pull. Windows are clamped to the plan's history and the response says so with clamped: true. Each row carries the same fields as the webhook payload plus the scan time.
What a scan tells you about a person
Nothing that identifies them. Scans carry country, region and city, device, operating system, browser and language, the destination served and the Smart Rule that chose it. There is no IP address and no cookie. To tie a scan to a contact, put the contact or campaign in the code itself: one code per rep, per mailing or per store, and UTM parameters on the destination for your own analytics.
Leads from KuikPage forms
Form submissions are a separate stream: each one posts form.submission to the form's webhook URL with the fields keyed by their labels, and GET /submissions lists them. See Form webhooks.