OpenQR

Developers

Build a branded link shortener with the OpenQR dynamic API

By Sam Moreton · updated 28 June 2026

A dynamic QR code is, underneath, a short link on oqr.to that redirects through OpenQR, can be repointed anytime, and logs every hit. Strip away the QR framing and that's the exact definition of a link shortener — with the bonus that every short link is already a scannable QR code. This guide builds a small but complete shortener on the OpenQR dynamic API: create short links with custom back-halves, redirect users, repoint dead links, and read click analytics. Full working Node code, no extra backend.

12 min read · Updated 28 June 2026

Prerequisite: a free API key — see getting started with the OpenQR API. Every call is Authorization: Bearer oqr_… against https://openqr.uk. The conceptual model for what a dynamic code is lives in the dynamic codes API guide; here we treat it purely as a shortener primitive.

Why this works as a shortener

Creating a dynamic code returns a short_url like https://oqr.to/your-slug. Hitting that URL 302-redirects to the destination, OpenQR logs the click (country, device, referrer), and you can PATCH the destination later without changing the link. Custom slugs give you readable branded back-halves. That's a full shortener feature set — you don't host the redirect layer, OpenQR does.

The four operations you need

Shortener actionOpenQR call
Shorten a URLPOST /v1/dynamic { destination } → returns short_url
Pick a custom back-halfPATCH /v1/dynamic/{id} { slug } (or set it before printing)
Repoint a linkPATCH /v1/dynamic/{id} { destination }
Read click analyticsGET /v1/dynamic/{id}/scans?days=
List / delete linksGET /v1/dynamic · DELETE /v1/dynamic/{id}

Shorten a URL

POST /v1/dynamic with { destination }. The destination must be a public http(s) URL — private/internal hosts and oqr.to self-loops are rejected as an abuse guard. You get back the id, the random slug, and the short_url to hand out.

curl -X POST https://openqr.uk/v1/dynamic \
  -H "Authorization: Bearer oqr_live_yourkey" \
  -H "Content-Type: application/json" \
  -d '{ "destination": "https://example.com/a/very/long/url?utm=x", "label": "Q3 newsletter" }'

# 201 Created
# { "id": "b1c2…", "slug": "k7Pm2qR",
#   "short_url": "https://oqr.to/k7Pm2qR",
#   "destination": "https://example.com/a/very/long/url?utm=x" }

Custom branded back-halves

A random slug works, but a readable one (oqr.to/q3-news) is the whole appeal of a branded shortener. Set the slug via PATCH. Rules: 3–48 chars, [a-z0-9-] only, no leading/trailing or doubled hyphen. A 409 means that back-half is already taken; pick another.

curl -X PATCH https://openqr.uk/v1/dynamic/b1c2… \
  -H "Authorization: Bearer oqr_live_yourkey" \
  -H "Content-Type: application/json" \
  -d '{ "slug": "q3-news" }'

# 200 OK  → short link is now https://oqr.to/q3-news

Changing a slug breaks the old link

The slug IS the short link. Repoint the destination as often as you like — the link is stable. But change the slug and the old back-half immediately stops resolving, breaking anything already shared. Lock your custom slug in before you distribute the link.

A complete shortener in Node

Here's a self-contained module that shortens a URL with an optional custom slug, falling back gracefully if the slug is taken. It uses nothing but fetch:

const BASE = "https://openqr.uk";
const KEY = process.env.OPENQR_KEY;
const auth = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

/** Shorten a URL, optionally requesting a custom back-half. */
export async function shorten(destination, slug) {
  const create = await fetch(`${BASE}/v1/dynamic`, {
    method: "POST",
    headers: auth,
    body: JSON.stringify({ destination }),
  });
  if (!create.ok) throw new Error((await create.json()).error);
  const code = await create.json();

  if (!slug) return code; // random slug is fine

  const patch = await fetch(`${BASE}/v1/dynamic/${code.id}`, {
    method: "PATCH",
    headers: auth,
    body: JSON.stringify({ slug }),
  });
  if (patch.status === 409) {
    // Back-half taken — keep the random slug, surface a warning.
    return { ...code, warning: `slug "${slug}" taken; using ${code.slug}` };
  }
  if (!patch.ok) throw new Error((await patch.json()).error);
  return patch.json();
}

const link = await shorten("https://example.com/launch", "launch");
console.log(link.short_url); // https://oqr.to/launch

The killer shortener feature: a link you've already shared can be redirected somewhere new. Newsletter went out with a broken URL? Don't reissue — repoint:

curl -X PATCH https://openqr.uk/v1/dynamic/b1c2… \
  -H "Authorization: Bearer oqr_live_yourkey" \
  -H "Content-Type: application/json" \
  -d '{ "destination": "https://example.com/launch-fixed" }'

Each short link carries analytics for free. GET /v1/dynamic/{id}/scans?days= returns lifetime and windowed click totals, a zero-filled daily series, and top countries, devices and referrers — exactly what a shortener's stats page shows. Full field reference in track QR scans with the analytics API.

const id = "b1c2…";
const res = await fetch(
  `https://openqr.uk/v1/dynamic/${id}/scans?days=30`,
  { headers: { Authorization: `Bearer ${process.env.OPENQR_KEY}` } }
);
const { scans, analytics } = await res.json();
console.log(`${scans.total} total clicks, ${analytics.window_total} this month`);
console.log("Top referrer:", analytics.by_referrer[0]?.value ?? "Direct");

Every short link is already a QR code

Because the short_url is a real URL, you can render it as a QR in one call: GET /v1/qr?data=https://oqr.to/your-slug&format=png. That's the edge a QR-native shortener has over a plain one — print or display the same link, no extra step.

Bulk shortening and limits

Single-create is capped at 20 links per hour per account (a 429 with Retry-After when exceeded). For migrating a batch of links, use POST /v1/dynamic/bulk — up to 200 per request. See bulk QR code generation for chunking and folders. To organise links, file them with folder_id and label them with tags (max 10).

If you'd rather not call the REST endpoints directly, the typed OpenQR TypeScript SDK wraps all of this (createDynamicCode, updateDynamicCode, getScans), and an AI agent can drive the same operations through the MCP server.

Get a free API keySign in via magic link, create a key, and start minting branded short links — each one a QR code, with analytics, free.
No — short links are served on oqr.to, the OpenQR redirect domain. You get a custom back-half (oqr.to/your-slug) via the slug field, but not a custom front-half. If a fully white-label domain is a hard requirement, OpenQR isn't the right shortener; if oqr.to plus a branded slug works, it's a complete, free backend.

Related reading