Aggregations-API
GET /v1/codes/:id/scans — Zeitreihen + Top-Länder/-Geräte/-OS für ein einzelnes Code-Objekt.
Jeder Scan eines dynamischen QR-Codes erzeugt einen Scan-Record in deinem Workspace. Die Erfassung passiert vollständig am Cloudflare-Edge — ohne externe Tracking-Dienste, ohne Cookies, ohne dass die Original-IP jemals deine Datenbank erreicht.
Drei Wege, Scan-Daten zu konsumieren:
Aggregations-API
GET /v1/codes/:id/scans — Zeitreihen + Top-Länder/-Geräte/-OS für ein einzelnes Code-Objekt.
Webhooks (Echtzeit)
qr.scanned-Event pro Scan, HMAC-SHA256-signiert. Siehe Webhooks.
Dashboard
Visualisierung im qr3.app-Dashboard — kein API-Call nötig.
Pro Scan wird ein Record in der Tabelle scans angelegt:
| Feld | Quelle | Beispiel |
|---|---|---|
id | Server-generierte UUID | scn_d1f8… |
code_id | Code-ID des gescannten QR-Codes | qr_a1b2c3d4 |
workspace_id | Workspace des Codes | ws_xxx |
country | Cloudflare cf.country | AT |
region | Cloudflare cf.region | Vienna |
city | Cloudflare cf.city | Wien |
device_type | User-Agent-Parsing | mobile, tablet, desktop |
os | User-Agent-Parsing | iOS, Android, macOS, Windows |
browser | User-Agent-Parsing | Safari, Chrome, Firefox |
referer | HTTP Referer-Header | https://example.com/landing |
language | Accept-Language-Header (erste Sprache) | de |
redirected_to | Tatsächliches Redirect-Ziel | https://example.com |
ip_hash | SHA-256(IP + täglicher Salt) — nicht reversibel | 8f3a… |
scanned_at | ISO-8601-Timestamp | 2026-05-12T14:32:11.000Z |
Alle Geo-Daten (country, region, city) stammen aus dem cf-Objekt von Cloudflare und basieren auf der Cloudflare-Geo-IP-Datenbank. Es werden keine externen Geo-IP-Services angefragt — die Auflösung passiert im selben Worker, der auch den Redirect ausführt.
Das hat drei Konsequenzen:
city ist nicht immer verfügbar (z.B. bei VPNs, Mobilfunk-Carriern oder kleinen Regionen). Erwarte null-Werte und kalkuliere sie in deinen Dashboards ein.GET /v1/codes/:id/scansLiefert aggregierte Analytics für einen einzelnen QR-Code über einen wählbaren Zeitraum.
curl "https://qr3.app/v1/codes/qr_a1b2c3d4/scans?days=30" \ -H "Authorization: Bearer qr3_sk_..."const analytics = await qr3.scans.get('qr_a1b2c3d4', { days: 30 });console.log(analytics.period_scans, analytics.top_countries);qr3 scans qr_a1b2c3d4 --days 30Query-Parameter:
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
days | integer | 30 | Zeitraum in Tagen (1–365) |
Response (HTTP 200):
{ "data": { "code_id": "qr_a1b2c3d4", "short_code": "r7f3Kx", "total_scans": 1842, "period_days": 30, "period_scans": 367, "scans_by_day": [ { "date": "2026-04-13", "count": 12 }, { "date": "2026-04-14", "count": 18 } ], "top_countries": [ { "value": "AT", "count": 142 }, { "value": "DE", "count": 98 }, { "value": "CH", "count": 41 } ], "top_devices": [ { "value": "mobile", "count": 281 }, { "value": "desktop", "count": 72 }, { "value": "tablet", "count": 14 } ], "top_os": [ { "value": "iOS", "count": 158 }, { "value": "Android", "count": 123 }, { "value": "macOS", "count": 48 } ] }, "meta": { "request_id": "req_xyz123" }}Antwort-Felder:
| Feld | Beschreibung |
|---|---|
total_scans | Lebenslange Scan-Summe des Codes (unabhängig vom days-Fenster) |
period_scans | Summe der Scans im gewählten Zeitfenster |
scans_by_day | Tages-Buckets, aufsteigend nach Datum, leere Tage werden weggelassen |
top_countries | Top 8 Länder im Zeitfenster, absteigend |
top_devices | Top 5 Gerätetypen (mobile, tablet, desktop) |
top_os | Top 5 Betriebssysteme |
Workspace-weite Summen (z.B. Scans pro Monat über alle Codes) stehen über GET /v1/workspaces/:id als scans_this_month-Feld zur Verfügung. Für Cross-Code-Analysen empfehlen wir das Dashboard oder einen periodischen Export.
Wenn du Scan-Events sobald sie passieren verarbeiten willst — etwa für Live-Dashboards, Lead-Tracking oder CRM-Integrationen — abonniere das qr.scanned-Event:
curl -X POST https://qr3.app/v1/webhooks \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned"], "secret": "my-secret-key-min-16-chars" }'Payload:
{ "id": "evt_abc123xyz", "type": "qr.scanned", "created": "2026-05-12T14:32:11.000Z", "data": { "code_id": "qr_a1b2c3d4", "short_code": "r7f3Kx", "scan_id": "scn_d1f8...", "country": "AT", "device_type": "mobile", "os": "iOS" }}Vollständige Doku zu Signaturverifikation, Retry-Logik und Delivery-Logs in API → Webhooks.
Scan-Records enthalten keine personenbezogenen IP-Adressen. Die ursprüngliche IP wird im Edge-Worker durch eine kryptografische Hash-Funktion (SHA-256) mit einem täglich rotierenden Salt ersetzt. Das bedeutet:
Retention ist plan-abhängig:
| Plan | Aufbewahrung |
|---|---|
| Free | 7 Tage |
| Pro | 90 Tage |
| Business / Agency | 1 Jahr |
| Enterprise | Custom (SLA) |
Ein täglich laufender Cron-Job (purgeOldScans) entfernt ältere Records automatisch — total_scans am Code-Objekt bleibt davon unberührt.
days=365) kann der Payload mehrere KB groß werden. Cache clientseitig mit kurzer TTL (z.B. 60 s).top_countries, top_devices, top_os sind serverseitig auf 8 bzw. 5 Einträge limitiert. Für tiefergehende Analysen exportiere die Rohdaten.null-Werte tolerieren: country, region, city, referer, language können null sein. Behandle das in deiner Auswertung als „Unbekannt”.