QRQR Code Agency
Guides

Tutorial - Dynamic campaign with analytics

End-to-end walk-through. Create a dynamic QR, print it, swap the destination, pull scan analytics.

Tutorial: Dynamic campaign with analytics

Print one QR for your spring menu. In June, swap the destination to the summer menu. In September, point the same printed QR at the autumn campaign. Pull scan analytics whenever you want.

This is the canonical "print once, edit forever" workflow.

What you will build

A printable PNG that:

  1. Encodes https://q.qrstudio.agency/q/<short_id>/
  2. Lives forever as a single piece of printed material
  3. Repoints to a new destination URL each season
  4. Logs every scan with date, country, device, and referer

Step 1: Create the dynamic QR

Use data_type: "dynamic" on the regular generate endpoint. The same call creates the row in our database and returns the rendered PNG.

cURL

curl -X POST https://api.qrstudio.agency/api/v1/generate/ \
-H "X-Api-Key: smk_..." \
-H "Content-Type: application/json" \
-d '{
"data_type": "dynamic",
"payload": {
"name": "Spring 2026 menu",
"destination_url": "https://example.com/menus/spring-2026"
},
"size_inches": 6,
"color": "black",
"background": "white",
"pattern": "rounded"
}' \
--output spring-menu.png

Python

import requests

r = requests.post(
"https://api.qrstudio.agency/api/v1/generate/",
headers={"X-Api-Key": "smk_..."},
json={
"data_type": "dynamic",
"payload": {
"name": "Spring 2026 menu",
"destination_url": "https://example.com/menus/spring-2026",
},
"size_inches": 6,
"color": "black",
"background": "white",
"pattern": "rounded",
},
)
r.raise_for_status()
open("spring-menu.png", "wb").write(r.content)

You now have:

  • spring-menu.png on disk, ready to print
  • A row in our database identified by short_id

Step 2: Find your short_id

The short id is embedded in the encoded URL. To list every dynamic QR you own, use the JWT-authed endpoint:

curl https://api.qrstudio.agency/api/v1/dynamic/ \
 -H "Authorization: Bearer <your-jwt>"
{
 "count": 1,
 "next": null,
 "previous": null,
 "results": [
 {
 "id": 42,
 "short_id": "aBc12dEf",
 "name": "Spring 2026 menu",
 "destination_url": "https://example.com/menus/spring-2026",
 "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-07T14:00:00Z"
 }
 ]
}

Persist short_id somewhere your application can find it later (database, spreadsheet, environment variable for tests).

Step 3: Print

Send spring-menu.png to your print shop. Stick it on the table tent, the front door, the menu booklet, wherever. The printed QR is now in the wild.

Step 4: Watch scans roll in

After your campaign goes live, pull analytics:

curl https://api.qrstudio.agency/api/v1/dynamic/aBc12dEf/analytics/?days=30 \
 -H "Authorization: Bearer <your-jwt>"
{
 "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 },
 { "day": "2026-05-09", "count": 47 }
 ],
 "top_referers": [
 { "referer": "(direct)", "count": 980 },
 { "referer": "https://google.com/", "count": 142 }
 ],
 "top_countries": [
 { "country": "CA", "count": 1031 },
 { "country": "US", "count": 178 }
 ],
 "top_devices": [
 { "device": "iPhone", "count": 689 },
 { "device": "Android", "count": 442 }
 ],
 "scan_heatmap_by_hour": [[0, 0, ...], ...],
 "geo_pins": [
 { "lat": 45.5, "lon": -73.6, "city": "Montreal", "country": "CA" }
 ]
}

The full schema is in the API reference.

Step 5: Swap the destination

Summer arrives. Repoint the same printed QR at the new menu:

curl -X PATCH https://api.qrstudio.agency/api/v1/dynamic/aBc12dEf/ \
 -H "Authorization: Bearer <your-jwt>" \
 -H "Content-Type: application/json" \
 -d '{
 "destination_url": "https://example.com/menus/summer-2026"
 }'

Response:

{
 "id": 42,
 "short_id": "aBc12dEf",
 "name": "Spring 2026 menu",
 "destination_url": "https://example.com/menus/summer-2026",
 "destination_changes_count": 1,
 "is_active": true,
 ...
}

The next scanner gets the new URL. The printed QR is unchanged.

warning: Free tier is capped at 1 lifetime edit per QR On Free, this PATCH succeeds the first time. The second time it returns 402 Payment Required. Upgrade to Starter or any paid plan for unlimited edits.

Step 6: Pause or kill switch

If the destination is broken (server down, link rotted), pause the QR to send scanners to a "we will be right back" page instead of a 404:

curl -X PATCH https://api.qrstudio.agency/api/v1/dynamic/aBc12dEf/ \
 -H "Authorization: Bearer <your-jwt>" \
 -H "Content-Type: application/json" \
 -d '{ "is_active": false }'

Re-enable when you fix the destination.

Variant: A/B test two destinations

Pro plan or higher. Store an array of variants with weights:

curl -X PATCH https://api.qrstudio.agency/api/v1/dynamic/aBc12dEf/ \
 -H "Authorization: Bearer <your-jwt>" \
 -H "Content-Type: application/json" \
 -d '{
 "variants": [
 { "url": "https://example.com/menu/A", "weight": 50, "label": "A" },
 { "url": "https://example.com/menu/B", "weight": 50, "label": "B" }
 ]
 }'

Each scanner is randomly routed by weight. The analytics endpoint returns a by_variant block with per-variant counts so you can declare a winner.

Variant: Listen for scans in real time

Subscribe a webhook to scan.created and we will POST you the moment each scan happens. See Listening for scan events.

What is next

On this page