API Documentation

REST API · v1

Create QR short links and change where they lead from your own systems. The API is free and available to every team. It works on the same data as the dashboard, so a code created through the API shows up there right away.

Authentication

Base URL: https://qr-redirect-vercel.vercel.app. All endpoints live under /api/v1, accept and return JSON, and need an API key in the Authorization header:

Authorization: Bearer qr_your_key

The team owner creates and revokes keys in the dashboard: Settings → Developer API. A key is shown once, when it is created. Only its hash is stored, so a lost key cannot be recovered: revoke it and create a new one. A key gives access to every QR code of the team. When a person leaves the team, the keys they created stop working.

The link object

FieldTypeMeaning
short_idstringThe id printed in the QR code. Never changes.
short_urlstringFull address encoded in the QR code: https://qr-redirect-vercel.vercel.app/<short_id>.
target_urlstringWhere a scan is redirected right now.
labelstring | nullName shown in the dashboard. Up to 120 characters, longer values are cut.
statusstringactive, paused or archived. Only active codes redirect.
tagsstring | nullJSON-encoded array of tags, for example "[\"print\",\"autumn\"]".
expires_atstring | nullISO 8601 date-time in UTC.
expiry_behaviorstringinactive (default): after expires_at the code shows the inactive page. keep: it keeps redirecting.
expiredbooleantrue when expires_at is in the past.
default_utmstring | nullJSON-encoded object of query parameters added to the target. Single-link responses only.
append_utm0 | 1Whether default_utm is added on redirect. Parameters already present in the target win. Single-link responses only.
pass_through_query0 | 1Whether the query string of the scanned URL is passed on to the target. Single-link responses only.
folder_id, campaign_id, client_idstring | nullGrouping set in the dashboard. Single-link responses only.
created_at, updated_atstringISO 8601 date-time in UTC.

QR designs (colors, logo) are edited in the dashboard only. The API ignores qr_style.

List links

GET /api/v1/links returns the team's links, newest first. Deleted links are never returned.

QueryDefaultRules
limit200Whole number. Values outside 1–200 are clamped into that range.
offset0Non-negative whole number. Read page after page until a page comes back shorter than limit.
curl "https://qr-redirect-vercel.vercel.app/api/v1/links?limit=50&offset=0" \
  -H "Authorization: Bearer qr_your_key"

Response 200:

{ "links": [ { "short_id": "menu", "short_url": "https://qr-redirect-vercel.vercel.app/menu", "target_url": "https://example.com/menu",
    "label": "Menu", "status": "active", "tags": null, "expires_at": null, "expiry_behavior": "inactive",
    "expired": false, "created_at": "2026-09-18T09:14:27.203Z", "updated_at": "2026-09-18T09:14:27.203Z" } ] }

Anything that is not a whole number answers 400 invalid_pagination.

Create a link

POST /api/v1/links

curl -X POST https://qr-redirect-vercel.vercel.app/api/v1/links \
  -H "Authorization: Bearer qr_your_key" \
  -H "Content-Type: application/json" \
  -d '{"target_url":"https://example.com/menu","label":"Menu","custom_slug":"menu"}'
FieldRequiredRules
target_urlyeshttp or https, up to 4,096 characters. Private, loopback and link-local addresses are rejected. long_url is accepted as an alias.
custom_slugno2–40 characters a-z 0-9 _ -. Stored in lower case. Reserved words such as admin or docs are rejected. Without it a random 6-character id is generated.
labelnoUp to 120 characters.
tagsnoArray of strings or one comma-separated string. Up to 20 tags of 40 characters.
expires_atnoYYYY-MM-DD (midnight UTC) or an ISO 8601 date-time. Must be in the future.
expiry_behaviornoinactive (default) or keep.
default_utmnoObject of up to 10 entries. Keys a-z 0-9 _ up to 40 characters, values up to 200 characters, 1 KB in total.
append_utmnoBoolean. Add default_utm to the target on redirect.
pass_through_querynoBoolean. Pass the scanned URL's query string on to the target.
folder_id, campaign_id, client_idnoIds of a folder, campaign or client of the same team.

Response 201:

{ "short_id": "menu", "short_url": "https://qr-redirect-vercel.vercel.app/menu", "target_url": "https://example.com/menu",
  "status": "active", "label": "Menu", "expires_at": null }

Print the QR code only after you have scanned short_url with a phone and seen the right page.

Get one link

GET /api/v1/links/{id} returns { "link": { … } } with every field of the link object. A deleted link, or a link of another team, answers 404 not_found.

curl https://qr-redirect-vercel.vercel.app/api/v1/links/menu -H "Authorization: Bearer qr_your_key"

Update a link

PATCH /api/v1/links/{id} changes where a printed code leads. The code itself stays the same, and the change applies from the next scan. Send only the fields you want to change:

curl -X PATCH https://qr-redirect-vercel.vercel.app/api/v1/links/menu \
  -H "Authorization: Bearer qr_your_key" \
  -H "Content-Type: application/json" \
  -d '{"target_url":"https://example.com/winter-menu"}'

Fields: target_url (or long_url), label, status (active, paused, archived), expires_at, expiry_behavior, default_utm, append_utm, pass_through_query, tags. The rules are the same as for creating. Two differences: expires_at may be in the past (the response then carries "expired": true), and null or an empty string clears expires_at, default_utm or tags. The short id cannot be changed.

Response 200: { "link": { … } } with the updated object. A body without any of these fields answers 400 empty_update. Bringing an archived link back counts against the active-code limit and can answer 403 limit_reached. Every change of target, status, label or expiry is written to the link's history in the dashboard, marked as made by the API.

Delete a link

DELETE /api/v1/links/{id} answers 200 { "ok": true }. The link disappears from lists, and the printed code shows the inactive page. Its scan history is kept. The short id is retired for good: nobody, including you, can register it again. To switch a code off for a while, use "status": "paused". To keep it out of the way and free a slot, use "archived". Both can be undone.

Statistics

GET /api/v1/links/{short_id}/stats returns the scan report of one code, GET /api/v1/analytics the report of the whole team. Both take the same query parameters as the dashboard: range=7|30|90|all or from=YYYY-MM-DD&to=YYYY-MM-DD, tz=<IANA timezone> (default: the team timezone, else UTC) and bots=1 to include bots and link previews. Numbers count people only unless bots=1 is given.

curl "https://qr-redirect-vercel.vercel.app/api/v1/links/aB3k9x/stats?range=30&tz=Asia/Dubai" \
  -H "Authorization: Bearer qr_your_key"

Response fields: totals (total, uniques, last24h, bots, inactive), daily and hourly series (zero-filled), and breakdowns byCountry, byCity, byChannel, byDevice, byOs, byBrowser, byReferrer, byOutcome. The team report adds top (codes by scans).

Rate limit: 120 requests per minute per key. Above that the API answers 429 with Retry-After.

Errors

Every error is JSON with a readable message and a stable machine code:

{ "error": "Slug is taken", "code": "slug_taken" }
StatusCodeWhen
400invalid_targettarget_url is missing, not http/https, too long, or points to a private address.
400invalid_slugcustom_slug breaks the format or is a reserved word.
400invalid_dateexpires_at is not a real date, or is in the past when creating.
400invalid_expiry_behavior, invalid_status, invalid_utmThe value is outside the rules above.
400folder_not_found, campaign_not_found, client_not_foundThe id does not belong to your team.
400invalid_paginationlimit or offset is not a whole number.
400empty_updatePATCH without any known field.
401unauthorizedThe key is missing, wrong or revoked.
403limit_reachedThe team already holds the maximum number of active QR codes.
404not_foundNo such link in your team, or it was deleted.
409slug_takenThe id belongs to a live link.
409slug_retiredThe id was used before and can never be reused.
413too_largeA field is over its size limit. Used for QR designs in the dashboard, listed here so clients handle it.
503id_unavailableA free random id could not be allocated. Retry the request.

Fair-use limits

QR Service is free. To protect the shared infrastructure, a team can hold up to 1,000 active QR codes and 25 members. Archived and deleted codes do not count. Reaching the limit answers 403 limit_reached, and existing codes keep working. A team can have up to 50 API keys. One list request returns at most 200 links. Keep automated traffic reasonable: we may rate-limit clients that put the service at risk. If you need more room, write to hello@beizstudio.com.

What a scan returns

A QR code encodes https://qr-redirect-vercel.vercel.app/<short_id>. What a scanner gets back:

StatusCase
302Active code. Location is the current target. The response is never cached, so a new target applies from the next scan.
404Unknown or deleted code.
410Archived code, or expired code with expiry_behavior: "inactive".
423Paused code.
503The lookup failed on our side. Comes with Retry-After: 30.

Every case except 302 shows the branded page "This QR code is not active". Preview it at /inactive?reason=paused (also unknown, expired, archived, deleted); the preview always answers 200. HEAD requests return the same status and are not counted as scans. Custom ids are not case-sensitive: /Menu finds /menu.

Help

Questions and bug reports: hello@beizstudio.com.

← Create a key in the dashboard