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:
- Encodes
https://q.qrstudio.agency/q/<short_id>/ - Lives forever as a single piece of printed material
- Repoints to a new destination URL each season
- 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.pngPython
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.pngon 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
- Static vs dynamic: the trade-offs
- API reference: dynamic QRs
- Scan webhooks guide