Developers
Rotate and schedule dynamic QR destinations programmatically
By Sam Moreton · updated 28 June 2026
PATCH /v1/dynamic/{id}, with the scan data to prove each window worked.11 min read · Updated 28 June 2026
You'll need a free API key (getting started guide) and at least one dynamic code (dynamic codes API). Everything below is one endpoint — PATCH /v1/dynamic/{id} with a new destination — invoked on a timer. There is no built-in scheduler in OpenQR; you own the clock (a cron job, a GitHub Action, a Cloudflare scheduled worker), which keeps the timing logic explicit and yours to change.
How a scheduled repoint actually behaves
PATCH changes the stored destination immediately. The next scan after the call lands on the new URL; scans before it landed on the old one. There's no propagation delay and no edit to the printed image — the QR encodes the oqr.to short link, which never changes. So "scheduling" is just: call PATCH at the right moment.
Pattern 1: time-box a launch
Print the poster before the page exists. Point the code at a teaser/holding page now; flip it to the live page the instant the launch opens; flip it again to a "thanks, it's over" page when it closes. Two scheduled PATCH calls, no reprint.
const BASE = "https://openqr.uk";
const KEY = process.env.OPENQR_KEY;
const id = "b1c2…";
async function repoint(destination) {
const res = await fetch(`${BASE}/v1/dynamic/${id}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ destination }),
});
if (!res.ok) throw new Error((await res.json()).error);
return res.json();
}
// Schedule these three lines as separate cron entries:
// at launch open: repoint("https://example.com/live")
// at launch close: repoint("https://example.com/ended")
// before launch: repoint("https://example.com/teaser")
await repoint("https://example.com/live");Use crontab as the scheduler
The cleanest setup is one cron entry per transition, each running a tiny script with a fixed destination. 0 9 1 7 * → repoint to /live (9am, 1 July). 0 0 8 7 * → repoint to /ended. The schedule lives in cron where it's auditable; the script just makes one PATCH call.
Pattern 2: rotate through a daily list
"Code of the day" mechanics: one code on a flyer that points at a different page each day — a daily deal, a rotating menu, an advent-calendar reveal. Drive it from a date-indexed list and a once-a-day cron.
const rotation = [
"https://example.com/day/mon",
"https://example.com/day/tue",
"https://example.com/day/wed",
"https://example.com/day/thu",
"https://example.com/day/fri",
"https://example.com/day/sat",
"https://example.com/day/sun",
];
const today = new Date().getDay(); // 0=Sun … 6=Sat
const destination = rotation[(today + 6) % 7]; // Mon-first index
await fetch(`https://openqr.uk/v1/dynamic/${id}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ destination }),
});
console.log("Repointed to", destination);Repointing is not rate-limited, but be sane about cadence
The 20-per-hour rate limit only applies to creating codes, not to PATCH. You can repoint as often as you need. Still, rotating faster than people scan just muddies your analytics — keep windows long enough that each destination gets a meaningful sample.
Pattern 3: per-session venue handoff
A fixed code on a table tent, lectern or kiosk that means "now" — the current session's slides, today's specials, this event's feedback form. A scheduler PATCHes it as the programme advances. The audience always scans the same code and always gets the live thing.
// A schedule of { at: ISO time, url } transitions for one event day.
const schedule = [
{ at: "2026-07-01T09:00:00Z", url: "https://example.com/session/keynote" },
{ at: "2026-07-01T11:00:00Z", url: "https://example.com/session/workshops" },
{ at: "2026-07-01T16:00:00Z", url: "https://example.com/feedback" },
];
// Run this every few minutes from cron; it applies the latest due transition.
const now = Date.now();
const due = schedule.filter((s) => Date.parse(s.at) <= now).at(-1);
if (due) {
await fetch(`https://openqr.uk/v1/dynamic/${id}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ destination: due.url }),
});
}Measuring each window
The point of rotating is knowing which window worked. The analytics.daily series from GET /v1/dynamic/{id}/scans buckets scans by day, so for daily rotations each bucket already maps to one destination. For finer-grained windows, snapshot window_total right before each PATCH and diff the snapshots — the delta is the scans the just-ended destination earned. Full field reference: track QR scans with the analytics API.
async function totalNow() {
const res = await fetch(
`https://openqr.uk/v1/dynamic/${id}/scans?days=1`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
return (await res.json()).scans.total; // lifetime, ignores window
}
const before = await totalNow();
await repoint("https://example.com/next"); // window boundary
// …time passes…
const after = await totalNow();
console.log(`Previous destination earned ${after - before} scans.`);Rotate by slug, not just destination — carefully
You can also PATCH the slug to swap which short link resolves, but that breaks any code already printed with the old slug. For scheduling, only ever rotate the destination — the printed QR and its slug stay fixed, which is the whole reason this works.
Robustness notes for unattended schedulers
- Validate destinations up front — a bad URL in your rotation list fails the PATCH with a 400 (private/internal hosts and oqr.to self-loops are rejected). Test the list once before trusting the cron.
- Make it idempotent — re-applying the same destination is harmless, so a cron that runs every few minutes and PATCHes the "current" target self-heals if a previous run was missed.
- Log and alert — wrap the PATCH so a non-2xx pings you; a silently-failed repoint means the code is pointing at the wrong window.
- Mind the clock/timezone — schedule in UTC or be explicit about the venue's timezone; an off-by-an-hour repoint is the classic event-day bug.
Prefer no code for the cron? The same scheduled-PATCH pattern fits a no-code tool's scheduler + HTTP module — see QR codes in n8n, QR codes in Make or QR codes in Zapier. Or let an AI agent do it on request via the MCP server.
Get a free API keySign in via magic link, create a key, and start scheduling repoints. Dynamic codes, analytics and the API are all free.