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.

Last updated: 2026-04-16 · support@abundera.ai

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).

PlanLimitWindow
Business1,000 requests / dayUTC day
Team10,000 requests / dayUTC day
Agency50,000 requests / dayUTC 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 }
StatusCodeMeaning
400validation_errorBody field failed validation. Response includes field + message.
401not_signed_in / invalid_api_keyMissing, invalid, or revoked bearer token.
402plan_limitAt your plan's active+paused code cap. Response includes plan, limit, current.
402plan_expiredAccount is past the 90-day grace window; upgrade to resume.
403insufficient_planAPI access requires Business or higher. Solo users hit this.
403insufficient_roleYour team role doesn't allow the requested mutation (admin+ required).
404not_foundResource doesn't exist, or isn't visible to your scope.
409code_not_editableThe code is in grace or expired status; reactivate to edit.
429rate_limitedPer-plan daily budget exceeded. See Rate limits.
500internalUnhandled 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/uuid

Changes 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/export

Re-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 scanned
  • day_bucket — UTC date (YYYY-MM-DD). No sub-day precision on the aggregate returned to you.
  • country — ISO-3166-1 alpha-2 from Cloudflare's CF-IPCountry header. No city, no region, no geo-IP lookup.
  • device_typemobile / 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.