Abundera QR Pro API
REST API reference for pro.qr.abundera.ai. Create, edit, and analyze dynamic QR codes programmatically. Everything is JSON over HTTPS, authenticated via bearer token.
Introduction
The Abundera QR Pro API lets you create, edit, and analyze dynamic QR codes programmatically — everything a dev wants to automate from the dashboard. Team management, billing, and account flows stay in the dashboard UI; this API is scoped to developer-grade code operations.
Base URL: https://pro.qr.abundera.ai/api
Request format: JSON (Content-Type: application/json) on POST / PATCH / DELETE.
Response format: JSON (application/json; charset=utf-8).
Availability: API access is a Business+ feature. Solo plans can use the dashboard but not the API.
Authentication
Every request carries an API key as a bearer token:
Authorization: Bearer abnd_qrpro_...Create and revoke keys at /account/keys (Business, Team, or Agency tier). The raw abnd_qrpro_... token is shown exactly once at creation — store it immediately. We store only its SHA-256 hash; there is no way to recover a lost key.
Unauthenticated requests return 401 { "error": "not_signed_in" }. Invalid or revoked keys return 401 { "error": "invalid_api_key" }.
API keys are scoped to the user who created them. If that user is a member of a team, the endpoints below operate on the team's codes automatically (current-team context is stored on the user account and managed via the dashboard).
Rate limits
Enforced per API key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds when the window rolls over).
| Plan | Limit | Window |
|---|---|---|
| Business | 1,000 requests / day | UTC day |
| Team | 10,000 requests / day | UTC day |
| Agency | 50,000 requests / day | UTC day |
| Solo | (403 insufficient_plan) | — |
Over-budget requests return 429 { "error": "rate_limited", "window": "day", "retry_at": 1234567890 } with a Retry-After header in seconds.
Scans are not rate-limited — the redirect hot path (qr.abundera.ai/r/{shortcode}) has no auth and no per-scan budget. Each plan has an explicit monthly scan cap (100k / 1M / 10M / 50M). Exceed the cap and the redirect still resolves; we email you so you can decide whether to upgrade or ride out a one-off spike. Planning over ~10M daily scans? Email us to coordinate capacity.
Errors
Every error response is JSON with a machine-readable error code and, where relevant, additional context:
{ "error": "plan_limit", "plan": "business", "limit": 500, "current": 500 }| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | Body field failed validation. Response includes field + message. |
| 401 | not_signed_in / invalid_api_key | Missing, invalid, or revoked bearer token. |
| 402 | plan_limit | At your plan's active+paused code cap. Response includes plan, limit, current. |
| 402 | plan_expired | Account is past the 90-day grace window; upgrade to resume. |
| 403 | insufficient_plan | API access requires Business or higher. Solo users hit this. |
| 403 | insufficient_role | Your team role doesn't allow the requested mutation (admin+ required). |
| 404 | not_found | Resource doesn't exist, or isn't visible to your scope. |
| 409 | code_not_editable | The code is in grace or expired status; reactivate to edit. |
| 429 | rate_limited | Per-plan daily budget exceeded. See Rate limits. |
| 500 | internal | Unhandled server error — email support if reproducible. |
Codes
Dynamic QR codes: a 7-char Base58 shortcode that redirects through qr.abundera.ai/r/{shortcode}. Every code carries a static-backup QR you can download from the dashboard — if you ever stop paying, the static version still resolves without touching our redirect.
GET /api/codes
List all codes in your current scope (personal, or the team you're currently acting under). Returns an array with a 30-day scan rollup per code.
$ curl -H "Authorization: Bearer abnd_qrpro_..." \
https://pro.qr.abundera.ai/api/codes
{
"codes": [
{ "id": "uuid", "shortcode": "aBc123x",
"url": "https://example.com/landing",
"label": "Q2 campaign", "tags": "q2,print",
"status": "active", "scans_30d": 1245,
"created_at": 1713288000, "updated_at": 1713370000 }
],
"plan": "business",
"plan_limit": 500,
"scope": { "type": "user" }
}POST /api/codes
Create a new dynamic code. Your plan's active+paused code cap is checked before insert. Minimal body:
{ "url": "https://example.com/landing" }Full customization (all optional):
{ "url": "https://example.com/landing",
"label": "Spring campaign",
"tags": "q2,print",
"qr_type": "url",
"style_json": "{...}",
"logo_key": "instagram",
"frame_style": "scan-me",
"frame_text": "SCAN ME" }Returns 201 + the created row including the generated shortcode and the short_url you print.
GET /api/codes/{id}
Fetch a single code. 404 if not in your scope.
PATCH /api/codes/{id}
Update any mutable field. The most common use: change the destination URL of an already-printed code.
$ curl -X PATCH \
-H "Authorization: Bearer abnd_qrpro_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/new-landing"}' \
https://pro.qr.abundera.ai/api/codes/uuidChanges propagate to the redirect within seconds. Valid status values for PATCH: "active", "paused". To delete, use DELETE.
DELETE /api/codes/{id}
Soft-delete. Transitions to status=grace with grace_until = now + 90 days. The redirect keeps working for the full grace window — this is the no-hostage-pricing promise made concrete. After 90 days the code becomes expired and the redirect returns 410 Gone.
POST /api/codes/import
Bulk-create from an array payload (also used by the "Save to Pro" flow on qr.abundera.ai). Accepts a single code payload or an array. Plan limit is enforced once before the batch.
Analytics
GET /api/codes/{id}/analytics
Query params:
range=7d|30d|90d|1y|3y— capped to your plan's retention (Solo 1y, Business 2y, Team/Agency 3y).granularity=day|hour— hourly is Team and Agency only; max 7-day window for hourly.
{
"range": "30d", "days": 30, "granularity": "day",
"total": 4321,
"timeseries": [
{ "bucket": "2026-04-01", "scans": 142 },
{ "bucket": "2026-04-02", "scans": 178 },
...
],
"by_country": [
{ "key": "US", "total": 3012 },
{ "key": "CA", "total": 402 },
{ "key": "Other", "total": 108 }
],
"by_device": [
{ "key": "mobile", "total": 3850 },
{ "key": "tablet", "total": 312 },
{ "key": "desktop", "total": 159 }
]
}Countries with fewer than 5 scans in the window are folded into "Other" for privacy (see Privacy model).
API keys
Create and revoke keys from the /account/keys dashboard page. Programmatic self-management is read-only via the API — you can list your keys and revoke them, but creating a new key requires the dashboard (chicken-and-egg: you'd need a key to create a key).
GET /api/keys
List your API keys. Never returns the raw token — only metadata.
{
"keys": [
{ "id": "uuid", "label": "Production server",
"created_at": 1713288000, "last_used_at": 1713370000 }
],
"allowed": true,
"plan": "business"
}DELETE /api/keys/{id}
Revoke. The key stops working immediately; any request bearing it returns 401 invalid_api_key thereafter.
Data export
GET /api/user/export
Download a ZIP containing your full dataset — codes.csv (every code, including grace + expired), scans.csv (aggregated daily rollup), and a README.txt explaining the format. The archive is emitted as application/zip; pipe it to a file:
$ curl -H "Authorization: Bearer abnd_qrpro_..." \
-o abundera-qr-export.zip \
https://pro.qr.abundera.ai/api/user/exportRe-import anywhere. This is the portable-format guarantee — no vendor lock-in is possible if you own your data.
Privacy model
The scan aggregate is the whole privacy story. What we store per scan on the redirect hot path:
code_id— which of your codes was scannedday_bucket— UTC date (YYYY-MM-DD). No sub-day precision on the aggregate returned to you.country— ISO-3166-1 alpha-2 from Cloudflare'sCF-IPCountryheader. No city, no region, no geo-IP lookup.device_type—mobile/tablet/desktop/unknown, classified from a short User-Agent regex. The raw UA string is discarded at classification time.scan_count— aggregate counter, upserted on every hit.
What we do not store: IP addresses (hashed or otherwise), raw User-Agent strings, city-level geo, sub-day timestamps, referer, cookies, retargeting pixels, or any individual-identifying vector. A noise floor of 5 suppresses small-aggregate re-identification.
Team and Agency tiers additionally write a parallel hourly aggregate with hour_bucket (YYYY-MM-DD-HH UTC). Same privacy model — no finer-than-hour timestamps, same noise floor, same absence of individual-scanner data.
Read the full story: /manifesto/ and /no-lock-in/ on the free-tool site.