QRQR Code Agency
API reference

Dynamic QRs

List, create, retrieve, update, delete dynamic QRs and pull scan analytics.

Dynamic QRs

Dashboard endpoints for managing dynamic QRs and reading analytics. All routes require a JWT bearer token.

For a conceptual overview, read Static vs dynamic QRs.

Public scan endpoint

GET /q/{short_id}/

This is the URL encoded into every dynamic QR. It is not part of the versioned API: it lives at the root of q.qrstudio.agency so the encoded URL stays as short as possible.

Response: 302 Found to the live destination_url with Cache-Control: no-store. Each hit creates a DynamicQrScan audit row.

If the QR is paused (is_active: false), we redirect to a "QR is paused" landing page instead of the destination.

List dynamic QRs

GET /api/v1/dynamic/?page=1&page_size=25
Authorization: Bearer <jwt>

Pagination defaults: page_size=25, max 100.

Response: 200 OK

{
 "count": 137,
 "next": "https://api.qrstudio.agency/api/v1/dynamic/?page=2",
 "previous": null,
 "results": [
 {
 "id": 42,
 "short_id": "aBc12dEf",
 "name": "Spring 2026 menu",
 "destination_url": "https://example.com/menus/spring-2026",
 "variants": [],
 "is_active": true,
 "scan_count": 1247,
 "last_scanned_at": "2026-05-08T13:45:12Z",
 "destination_changes_count": 1,
 "public_url": "https://q.qrstudio.agency/q/aBc12dEf/",
 "created_at": "2026-04-01T10:00:00Z",
 "updated_at": "2026-05-01T08:30:00Z"
 }
 ]
}

Create a dynamic QR

This route creates the database row only. To create and render in one call, use POST /api/v1/generate/ with data_type: "dynamic".

POST /api/v1/dynamic/
Authorization: Bearer <jwt>
Content-Type: application/json

{
 "name": "Spring 2026 menu",
 "destination_url": "https://example.com/menus/spring-2026"
}
FieldTypeRequiredDescription
namestringyesInternal label, never visible to scanners. Max 120 chars.
destination_urlURLyesThe URL we 302 redirect to. Editable later. Max 2000 chars.

Response: 201 Created

{
 "id": 42,
 "short_id": "aBc12dEf",
 "name": "Spring 2026 menu",
 "destination_url": "https://example.com/menus/spring-2026",
 "variants": [],
 "is_active": true,
 "scan_count": 0,
 "last_scanned_at": null,
 "destination_changes_count": 0,
 "public_url": "https://q.qrstudio.agency/q/aBc12dEf/",
 "created_at": "2026-05-08T14:00:00Z",
 "updated_at": "2026-05-08T14:00:00Z"
}

403 Forbidden

Plan does not include dynamic QRs (no plan currently does, but kept for completeness) or you reached the active QR cap.

{
 "detail": "Plan 'starter' is capped at 10 active dynamic QRs. Disable one or upgrade."
}

Retrieve

GET /api/v1/dynamic/{short_id}/
Authorization: Bearer <jwt>

Same shape as the list result entries.

404 Not Found

The QR does not exist or does not belong to you.

Update

PATCH /api/v1/dynamic/{short_id}/
Authorization: Bearer <jwt>
Content-Type: application/json

{
 "destination_url": "https://example.com/menus/summer-2026"
}
FieldEditableNotes
nameInternal label
destination_urlThe redirect target
is_activePause / kill switch
variantsA/B variants array (Pro+)
short_idRead only, generated at creation
scan_countRead only, system maintained

A/B variants

Pro plan or higher. Pass an array of {url, weight, label}:

{
 "variants": [
 { "url": "https://example.com/menu/A", "weight": 50, "label": "A" },
 { "url": "https://example.com/menu/B", "weight": 50, "label": "B" }
 ]
}
  • Weight is relative; total must be > 0.
  • Maximum 10 variants per QR.
  • Empty array ([]) means "single destination, use destination_url".

402 Payment Required

Free tier is capped at one lifetime destination edit per dynamic QR. The second edit returns:

{
 "detail": "Plan 'free' allows 1 destination edit per dynamic QR. This QR has been edited 1 time(s) already. Upgrade to Starter ($7/mo) or higher for unlimited edits, or buy a credit pack."
}

Delete

DELETE /api/v1/dynamic/{short_id}/
Authorization: Bearer <jwt>

Returns 204 No Content. The redirect endpoint immediately starts returning a "QR not found" page for that short id. Existing analytics rows are preserved for the standard 13 month retention window.

Analytics

GET /api/v1/dynamic/{short_id}/analytics/?days=30
Authorization: Bearer <jwt>
QueryDefaultRange
days301 to 365

Response: 200 OK

{
 "short_id": "aBc12dEf",
 "name": "Spring 2026 menu",
 "total_scans": 1247,
 "scans_in_period": 1247,
 "period_days": 30,
 "by_day": [
 { "day": "2026-05-07", "count": 12 },
 { "day": "2026-05-08", "count": 31 }
 ],
 "top_referers": [
 { "referer": "(direct)", "count": 980 },
 { "referer": "https://google.com/", "count": 142 }
 ],
 "top_clients": [
 { "client": "iOS", "count": 689 },
 { "client": "Android", "count": 442 }
 ],
 "top_countries": [
 { "country": "CA", "count": 1031 },
 { "country": "US", "count": 178 }
 ],
 "top_cities": [
 { "city": "Montreal, CA", "count": 612 }
 ],
 "top_devices": [
 { "device": "iPhone", "count": 689 },
 { "device": "Android", "count": 442 }
 ],
 "top_os": [
 { "os": "iOS", "count": 689 },
 { "os": "Android", "count": 442 }
 ],
 "top_browsers": [
 { "browser": "Safari", "count": 612 },
 { "browser": "Chrome", "count": 405 }
 ],
 "scan_heatmap_by_hour": [
 [0, 0, 1, 0, 0, 0, 1, 4, 12, 18, 22, 27, 31, 35, 33, 29, 24, 20, 18, 14, 10, 6, 3, 1],
 [0, 0, 0, 1, 0, 1, 2, 5, 14, 20, 24, 30, 33, 37, 35, 31, 26, 22, 19, 15, 11, 7, 3, 2]
 ],
 "geo_pins": [
 { "lat": 45.5, "lon": -73.6, "city": "Montreal", "country": "CA" }
 ],
 "by_variant": []
}
FieldDescription
total_scansAll-time scans on this QR
scans_in_periodScans in the requested window
by_dayOne entry per day in the window (zero-fill not included)
top_referersTop 10 by raw Referer header. (direct) for missing
top_clientsCoarse UA bucket (iOS, Android, macOS, Windows, Linux, Bot, other)
top_countriesTop 10 by ISO 3166 code from edge headers
top_citiesTop 10 "City, Country"
top_devicesTop 10 device families (iPhone, iPad, Pixel, etc.)
top_osTop 10 OS families
top_browsersTop 10 browser families
scan_heatmap_by_hour7 x 24 grid: weekdays vs hours of day
geo_pinsUp to 200 deduped (lat, lon) pins for map rendering
by_variantPer-variant counts (empty array if no A/B variants)

The aggregations sample the last 5 000 scans in the period. For QRs above that scale we maintain an additional rolled-up aggregation table; the response shape stays the same.

Custom domains (Agency+)

GET /api/v1/dynamic/custom-domains/
POST /api/v1/dynamic/custom-domains/
GET /api/v1/dynamic/custom-domains/{id}/
DELETE /api/v1/dynamic/custom-domains/{id}/
POST /api/v1/dynamic/custom-domains/{id}/verify/

White-label your dynamic QR redirects on your own subdomain (go.yourbrand.com/q/<short_id>/). Available on Agency and Enterprise. The verify endpoint runs DNS TXT and CNAME checks. See the dashboard walkthrough for the full setup.

See also

On this page