GDPR & Privacy
Privacy by Design
qr3.app is built GDPR-compliant from the ground up:
- IP pseudonymisation — IP addresses are immediately converted to purpose-separated HMAC-SHA-256 UTC-day pseudonyms in the edge worker. The original IP is never stored; the pseudonym remains personal data.
- Data minimisation — Only data necessary for operation is collected.
Privacy API
Data Summary (Art. 15 GDPR)
GET /v1/account/privacy — Returns an overview of all stored data.
curl https://qr3.app/v1/account/privacy \ -H "Authorization: Bearer qr3_sk_..."Response:
{ "data": { "workspace_id": "ws_xxx", "organization_id": "org_xxx", "account": { "email": "you@example.com", "name": "Alex Example", "locale": "en", "created_at": "2026-01-15T09:00:00.000Z" }, "stored_data": { "qr_codes": 42, "scan_records": 15000, "dpp_scan_records": 3200, "dashboard_activity_records": 0, "dashboard_document_activity_records": 36, "api_operation_records": 180, "webhook_delivery_records": 640, "api_keys": 2, "webhooks": 1, "dpp_passports": 12, "dpp_documents": 30, "files": 4, "gs1_identifiers": 2, "gs1_verification_requests": 1, "code_comments": 5, "audit_log_entries": 0, "dpp_import_jobs": 1, "workspace_members": 3, "scan_quota_alerts": 1 }, "gdpr_rights": { "access": "GET /v1/account/privacy", "export": "GET /v1/account/export", "erasure": "DELETE /v1/account", "consents": "GET /v1/account/privacy/consents" }, "data_processing": { "ip_storage": "Purpose-separated HMAC-SHA-256 UTC-day IP pseudonym — pseudonymous personal data", "scan_retention": "90 days by default", "usage_retention": "90 UTC days", "webhook_delivery_retention": "90 UTC days", "pii_in_export": true, "erasure": "DELETE /v1/account erases the person's account: organizations they own with all workspaces and data (unless another org admin takes them over), their memberships in other organizations, their login and their Stripe customer. A running subscription ends immediately, without refund.", "backup_retention": "Erased rows stay restorable from the database backups (Cloudflare D1 Time Travel) for up to 30 days, then they are gone.", "billing_records": "Invoices stay with Stripe for the statutory retention period (7 years, § 132 BAO)." } }, "meta": { "request_id": "req_xxx" }}stored_data counts the stored entries. scan_records are scans of QR codes, dpp_scan_records are requests to product passports via the public DPP resolver. Both contain the IP pseudonym.
dashboard_activity_records and dashboard_document_activity_records count the days on which a member opened the dashboard (two tables), api_operation_records the daily request counters per API key, operation and outcome, and webhook_delivery_records the webhook delivery attempts, including those of deleted webhooks. None of them stores an IP address or a user agent; the delivery log does store the event payload and the first 1 KB of the receiver’s response. All four are deleted after 90 UTC days (usage_retention, webhook_delivery_retention).
account names the person behind the request — the signed-in person in the dashboard, otherwise the person who created the key — with e-mail, name, language and creation date; for a publishable key it is null. The other counters cover every further table with data of the account: product passports, DPP documents, files, GS1 identifiers, comments, audit entries, import runs, workspace members and the organization’s scan-quota notices (scan_quota_alerts). Soft-deleted rows count, because they are still stored. erasure, backup_retention and billing_records describe what DELETE /v1/account deletes and what remains for a while; pii_in_export is true because the export names the person (e-mail, name) — IP pseudonyms never leave the database.
Data Export (Art. 20 GDPR)
GET /v1/account/export — Downloads all your data as a JSON file.
curl https://qr3.app/v1/account/export \ -H "Authorization: Bearer qr3_sk_..." \ -o my-data.jsonThe export contains:
- All QR codes (including deleted ones)
- Aggregated scan statistics per code (no raw IP hashes)
- Aggregated DPP requests per product passport (
dpp_scan_aggregates, no IP hashes either) - Usage statistics as aggregates (
dashboard_activity_aggregates,api_operation_aggregates): active days and number of active users per dashboard channel, request counters per operation and outcome — without user or key IDs - Webhook delivery attempts per webhook and event type (
webhook_delivery_aggregates), without payload and response - The person behind the request (
account), the organization (organization; billing and notification address only for org admins) and the workspace (workspace) - Product passports, metadata of DPP documents and files (not the files themselves), GS1 identifiers, requests to verify them and import runs (
dpp_passports,dpp_documents,files,gs1_identifiers,gs1_verification_requests,dpp_import_jobs) - Comments without author fields (
code_comments), audit entries as aggregates (audit_log_aggregates) and members per role (member_aggregates) - API keys without ID and hash (
api_keys), webhooks without URL and secret (webhooks) and the organization’s scan-quota notices (scan_quota_alerts) - Creation and modification timestamps
Publishable keys (qr3_pk_) receive 403: the file contains the target URLs and notes of all codes.
Consent Management
GET /v1/account/privacy/consents — Get current consent preferences.
POST /v1/account/privacy/consents — Update consent preferences.
{ "marketing_emails": false, "analytics": true, "product_updates": true}Fields and defaults: marketing_emails (false), analytics (true), product_updates (true). Other fields are ignored; a non-boolean value returns 400.
Changing consents requires write rights: the viewer role and publishable keys receive 403.
Account Deletion (Art. 17 GDPR)
DELETE /v1/account — Erases the account of the person behind the request, immediately and irrevocably: the signed-in person in the dashboard, otherwise the person who created the key. An erasure request by e-mail leads to the same erasure: the operator deletes the login at Clerk, and qr3 then erases the account.
curl -X DELETE https://qr3.app/v1/account \ -H "Authorization: Bearer qr3_sk_..."What happens:
- Organizations owned by the person are deleted with all workspaces, codes, product passports, files, statistics and logs, including their files in storage. If such an organization has another active org admin, the longest-standing one takes it over instead, with its data and subscription.
- Subscription: a running subscription ends immediately, without a pro-rata refund and without a final invoice; then the Stripe customer is deleted. Invoices stay with Stripe for the statutory retention period (7 years, § 132 BAO).
- Teams: the person’s memberships in other organizations end; those teams keep their data. The person’s comments no longer name an author, their audit entries lose IP pseudonym and user agent, and the API keys they created there are revoked.
- The person: the login (Clerk) and the user record are deleted.
The answer is 200 with status: "completed" and the counters organizations_deleted, organizations_transferred, memberships_removed and login_deleted. For very large organizations one request is not enough: the answer is then 202 with status: "in_progress", and the erasure completes automatically within hours. If a subscription cannot be ended automatically, because it cannot be told apart from another product’s billing, the answer is 202 with status: "awaiting_operator": everything except the organization’s billing record and the user record is erased without waiting; those follow once the qr3 team has ended the subscription by hand, with nothing further needed from you.
Requires a secret or test key with write rights, or a dashboard session (any role, because it is the person’s own account). Publishable and read-only keys receive 403, a key without a person behind it (created_by = 'system') receives 409. 503 means the deployment is not set up for the erasure: a secret it needs is missing, or Stripe does not accept a configured price or the key; nothing was changed.
Scan Data & Pseudonymisation
| Data point | Treatment |
|---|---|
| IP address | HMAC-SHA-256 UTC-day pseudonym (long-lived master secret + purpose + UTC day), never stored in plain text |
| Country / Region | Derived from Cloudflare geo data, stored directly |
| User-Agent | Parsed to OS/Browser/Device type, raw string discarded |
| Referer | Stored as-is (no PII in standard referers) |
| Accept-Language | Only primary language tag stored |
Technical implementation of IP pseudonymisation
HTTP request → Cloudflare edge worker ↓CF-Connecting-IP header → HMAC-SHA-256(IP_HASH_SALT, purpose, UTC day, IP) ↓ip_hash (UTC-day pseudonym) → D1 database ↓Original IP is never storedIP_HASH_SALT is a long-lived master secret; purpose and UTC day automatically derive separate daily pseudonyms. Pseudonyms from different UTC days are not directly equal, but a holder of the secret can recalculate a known IP candidate for a specific day. No KV day salt or manual daily rotation is used.
Scan data retention (automated)
| Plan | Retention |
|---|---|
| All plans | 90 days |
A daily cron job removes QR code scans (purgeOldScans) and DPP requests (purgeOldDppScans) older than 90 days automatically.
Usage statistics (dashboard activity, API usage counters) and webhook delivery logs are kept for 90 UTC days as well: the current UTC day plus the 89 preceding days. The same daily cron job deletes older rows (purgeOldDashboardActivity, purgeOldApiOperationUsage, purgeOldWebhookDeliveries).
Contact
- Data Protection: office@qr3.app
- Privacy Policy: qr3.app/en/legal/privacy