{"openapi":"3.1.0","info":{"title":"oktogen-qr","version":"0.2.0","description":"In-house QR platform: static generation (free, untracked) + dynamic tracked codes (/r redirect) + hosted landing pages. Privacy-by-construction analytics."},"servers":[{"url":"https://demo.qr.zabazingo.eu"}],"components":{"securitySchemes":{"tenantKey":{"type":"http","scheme":"bearer","description":"Tenant API key from POST /app/api/signup"}}},"paths":{"/healthz":{"get":{"summary":"Liveness","responses":{"200":{"description":"ok"}}}},"/r/{shortId}":{"get":{"summary":"Resolve + track a dynamic code (302 to destination or hosted page)","parameters":[{"name":"shortId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"redirect"},"404":{"description":"unknown"},"410":{"description":"disabled"},"429":{"description":"rate limited"}}}},"/openapi.json":{"get":{"summary":"This contract","responses":{"200":{"description":"OpenAPI document"}}}},"/app/api/types":{"get":{"summary":"Catalog of code types + templates","responses":{"200":{"description":"types"}}}},"/app/api/generate":{"post":{"summary":"Render a static styled QR (free, anonymous)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string"},"fields":{"type":"object"},"style":{"type":"object"}}}}}},"responses":{"200":{"description":"svg + payload"},"429":{"description":"rate limited"}}}},"/app/api/export":{"post":{"summary":"Export a QR as svg/pdf/eps","responses":{"200":{"description":"file"},"429":{"description":"rate limited"}}}},"/app/api/signup":{"post":{"summary":"Create a workspace + tenant API key","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}}}}}},"responses":{"200":{"description":"apiKey"},"429":{"description":"rate limited"}}}},"/app/api/me":{"get":{"summary":"Current tenant + resolved grants","security":[{"tenantKey":[]}],"responses":{"200":{"description":"me"},"401":{"description":"invalid key"}}}},"/app/api/codes":{"get":{"summary":"List my codes + totals","security":[{"tenantKey":[]}],"responses":{"200":{"description":"codes"}}},"post":{"summary":"Create a dynamic (trackable) code","security":[{"tenantKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string"},"fields":{"type":"object"},"target":{"type":"string"},"title":{"type":"string"},"style":{"type":"object"}}}}}},"responses":{"200":{"description":"created"},"400":{"description":"target required"},"402":{"description":"plan limit"},"403":{"description":"feature not enabled"}}}},"/app/api/codes/{id}":{"patch":{"summary":"Edit destination / active","security":[{"tenantKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"updated"}}}},"/app/api/codes/{id}/qr.svg":{"get":{"summary":"Download the TRACKABLE (/r) QR for a saved code","security":[{"tenantKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"svg"}}}},"/app/api/codes/{id}/analytics":{"get":{"summary":"Per-code scan analytics + funnel","security":[{"tenantKey":[]}],"responses":{"200":{"description":"analytics"}}}},"/app/api/codes/{id}/scans":{"get":{"summary":"Per-scan log + durable-device rollup. Returns total, uniqueVisitors, repeatScanners, uniqueDevices, repeatDevices, avgScansPerDevice and a privacy-hashed per-scan events[]. The per-device devices[] list (opaque device id, firstSeen, lastSeen, scanCount) is only present when the operator has granted the tenant the deviceLog visibility. Durable device ids are consent-only (see POST /p/{shortId}/consent).","security":[{"tenantKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"scan log + device rollup"},"401":{"description":"not yours"},"403":{"description":"analytics not enabled"}}}},"/app/api/campaigns":{"get":{"summary":"Campaigns (group tag) rolled up: per-campaign codes/scans/unique/leads + per-code compare (scans, unique, leads, CTA-rate)","security":[{"tenantKey":[]}],"responses":{"200":{"description":"campaigns"},"401":{"description":"invalid key"},"403":{"description":"analytics not enabled"}}}},"/app/api/webhook":{"get":{"summary":"Get this tenant's outbound webhook config (url; secret is never re-revealed)","security":[{"tenantKey":[]}],"responses":{"200":{"description":"{ url, configured, updatedAt }"},"401":{"description":"invalid key"}}},"put":{"summary":"Set/update the tenant webhook url. A fresh signing secret is minted (returned ONCE) on first set or when rotate=true. Empty url removes the webhook.","description":"On every scan (/r) and lead (POST /api/leads) the service POSTs a signed event to this url with header `x-oqr-signature: <hex hmac-sha256(secret, body)>`. Versioned envelope: `{ v:1, product:\"oktogen-qr\", event:\"qr.scan\"|\"qr.lead\", codeId, shortId, at, country, device, repeat (scan), hasEmail, consent (lead) }`.","security":[{"tenantKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"rotate":{"type":"boolean"}}}}}},"responses":{"200":{"description":"{ url, configured, updatedAt, secret? }"},"400":{"description":"invalid url"},"401":{"description":"invalid key"}}}},"/app/api/webhook/test":{"post":{"summary":"Deliver a synthetic signed qr.scan event to the configured webhook (rate-limited 1/10s per tenant)","security":[{"tenantKey":[]}],"responses":{"200":{"description":"delivered"},"400":{"description":"no url set"},"401":{"description":"invalid key"},"429":{"description":"rate limited"}}}},"/app/api/bulk":{"post":{"summary":"CSV → ZIP of QR svgs + manifest","security":[{"tenantKey":[]}],"responses":{"200":{"description":"zip"}}}},"/app/api/redeem":{"post":{"summary":"Redeem an upgrade voucher → plan upgrade","security":[{"tenantKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"}}}}}},"responses":{"200":{"description":"upgraded"},"400":{"description":"invalid/expired voucher"}}}},"/api/leads/{shortId}":{"post":{"summary":"Consented zero-party lead capture (hosted page)","responses":{"200":{"description":"stored"},"429":{"description":"rate limited"}}}},"/api/events/{shortId}":{"post":{"summary":"Anonymous interaction funnel event","responses":{"200":{"description":"ok"},"429":{"description":"rate limited"}}}},"/p/{shortId}/consent":{"post":{"summary":"Record the scanner's device-recognition consent (Art.13 surface on the hosted /p page). accept:true sets a signed, HttpOnly oqr_consent cookie so subsequent /r scans mint a durable, random, consent-only device id; accept:false clears consent and any durable id. No durable id is ever derived from IP/UA.","parameters":[{"name":"shortId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"accept":{"type":"boolean"}}}}}},"responses":{"204":{"description":"recorded"},"404":{"description":"unknown code"}}}}}}