QRQR Code Agency
API reference

Dynamic QRs

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

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-09T14:00:00Z",
  "updated_at": "2026-05-09T14:00:00Z"
}

403 Forbidden

You reached the active QR cap on your plan.

{
  "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
nameyesInternal label
destination_urlyesThe redirect target
is_activeyesPause / kill switch
variantsyesA/B variants array (Pro+)
short_idnoRead only, generated at creation
scan_countnoRead 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