QR Codes API
The QR Codes API allows you to create, retrieve, list, and update dynamic QR codes programmatically.
All dynamic QR codes generated through the API behave exactly like those created in the QRForge Dashboard.
Base Path
All QR Code API endpoints use:
/v1/qr-codes
Example:
GET https://api.qrforge.link/v1/qr-codes
Object: QR Code
A QR Code resource uses the following structure:
{
"qr_id": "wUF83EsmkvjkgdsRSCAE",
"label": "API happy path",
"slug": "qr-1ap99pp",
"status": "active",
"type": "url",
"project_id": "NDcK35kvVopZCn26gTm3",
"redirect_url": "https://example.com/api-happy",
"redirect_rules": [
{ "condition": "device", "value": "ios", "url": "https://apps.apple.com/app/id123" },
{ "condition": "country", "value": "DE", "url": "https://example.de" }
],
"expires_at": null,
"scan_limit": null,
"scan_count": 0,
"destination_url": "https://example.com/api-happy",
"qr_data": {},
"qr_render_metadata": {},
"analytics_enabled": true,
"render_status": "done",
"render_requested_at": "2025-11-19T12:36:32.654Z",
"render_attempt": 1,
"render_error": null,
"file_url_svg": "https://storage.googleapis.com/.../wUF83EsmkvjkgdsRSCAE.svg",
"file_url_png": "https://storage.googleapis.com/.../wUF83EsmkvjkgdsRSCAE.png",
"created_at": "2025-11-19T12:36:32.654Z",
"updated_at": "2025-11-19T12:36:36.092Z",
"last_rendered_at": "2025-11-19T12:36:47.697Z"
}
Field definitions:
| Field | Type | Description |
|---|---|---|
qr_id | string | QR document ID |
label | string | Display name. Sent as name in the Create request body, but always returned as label in responses. |
slug | string | Public scan URL slug (qr-xxxxxxx) |
status | string | active, archived, or blocked. blocked is set automatically when an automated Web Risk re-scan flags a previously-safe URL as unsafe after creation. |
type | string | QR type. The public API supports url, dynamic_url, and text — other types (vCard, WiFi, etc.) can only be created in the QRForge app. |
project_id | string | ID of the project this QR belongs to |
redirect_url | string | Target redirect URL |
redirect_rules | array | Conditional redirection rules, evaluated in order — first match wins. Each entry is {condition, value, url} where condition is "device" (value one of ios/android/desktop) or "country" (value an ISO-3166-1 alpha-2 code). An entry may optionally add condition2/value2 to require a second condition (the other type — device+country only, never device+device) to also match before the rule fires, e.g. {"condition": "device", "value": "ios", "condition2": "country", "value2": "DE", "url": "..."} matches only iOS scanners from Germany. A third condition type, "schedule", is a standalone rule shape instead — {condition: "schedule", schedule_days: [...], schedule_start, schedule_end, url} (no value/condition2; see below). Scans matching no rule fall back to redirect_url. Requires an Enterprise/Agency plan. Limited to 20 entries (MAX_REDIRECT_RULES) — exceeding it returns a 400 error. |
expires_at | timestamp | null | Scans after this time get a 410 "expired" response instead of redirecting. |
scan_limit | integer | null | Once scan_count reaches this value, further scans get a 410 "scan limit reached" response instead of redirecting. null means unlimited. Present on the single-QR GET-by-id and GET-by-slug responses; not included in the List response. |
scan_count | integer | Number of successful scans so far (read-only). Present on the single-QR GET-by-id and GET-by-slug responses; not included in the List response. |
destination_url | string | Effective destination URL for the QR (read-only). |
qr_data | object | Raw structured data encoded in the QR. Shape varies by type. |
qr_render_metadata | object | Visual rendering configuration: colors, size, error correction level, etc. |
analytics_enabled | boolean | Whether scan analytics are collected for this QR. |
render_status | string | Async render pipeline status: staging, rendering, done, error, or idle. |
render_requested_at | timestamp | null | When the current render was requested. |
render_attempt | integer | Number of render attempts made. |
render_error | string | null | Error message from the last failed render attempt, if any. |
file_url_svg | string | URL of the rendered SVG file. |
file_url_png | string | URL of the rendered PNG file. |
created_at | timestamp | Creation timestamp |
updated_at | timestamp | Last update |
last_rendered_at | timestamp | Last QR rendering completion time |
Schedule-condition rules. schedule_days is a non-empty array of lowercase 3-letter weekday codes (mon, tue, wed, thu, fri, sat, sun); schedule_start/schedule_end are "HH:mm" 24-hour time strings, with schedule_start required to be earlier than schedule_end (no overnight wraparound in v1 — a rule spanning midnight needs two entries, e.g. 22:00–23:59 and 00:00–06:00). The window is evaluated in the QR owner's account timezone (set in the QRForge app's profile settings), not the scanning visitor's — this matches a "business hours" model, e.g. a restaurant's own local closing time. A schedule rule cannot be combined with condition2/value2.
List QR Codes
Retrieve all QR codes in the authenticated workspace.
GET /v1/qr-codes
Query Parameters
All parameters are optional and can be combined.
| Parameter | Type | Description |
|---|---|---|
project_id | string | Filter to a single project. |
status | string, repeatable | Filter by status. Repeat the parameter to match multiple values, e.g. ?status=active&status=archived. By default (no status filter), archived QRs are hidden from the list — pass status=archived explicitly to see them. |
type | string | Filter by QR type (exact match). |
label_contains | string | Case-insensitive substring match against label. |
page_size, page_token | — | Pagination. See Pagination. |
Example — cURL
curl -X GET "https://api.qrforge.link/v1/qr-codes" \
-H "x-api-key: api_live_xxxxxxxxxxxxxxxxxxxxx"
Example — Response
{
"ok": true,
"items": [
{
"qr_id": "kBfYQ8oD7wTsVOJiLMHk",
"project_id": "NDcK35kvVopZCn26gTm3",
"slug": null,
"label": "Minute Window Test 1",
"status": "active",
"type": "url",
"redirect_url": "https://example.com/rate-min-1",
"created_at": "2025-11-19T12:43:23.633Z",
"render_status": "pending"
}
]
}
scan_limit and scan_count are known gaps in the List response — they're present on the single-QR GET-by-id and GET-by-slug responses but not returned here.
Create a QR Code
Create a dynamic URL QR.
POST https://api.qrforge.link/v1/qr-codes
Content-Type: application/json
Body
{
"name": "API happy path",
"redirect_url": "https://example.com/api-happy",
"type": "url",
"redirect_rules": [
{ "condition": "device", "value": "ios", "url": "https://apps.apple.com/app/id123" },
{
"condition": "device", "value": "ios",
"condition2": "country", "value2": "DE",
"url": "https://apple.de"
},
{
"condition": "schedule",
"schedule_days": ["mon", "tue", "wed", "thu", "fri"],
"schedule_start": "09:00",
"schedule_end": "17:00",
"url": "https://example.com/business-hours"
}
],
"expires_at": "2026-12-31T23:59:00.000Z",
"scan_limit": 500
}
redirect_rules, expires_at, and scan_limit are all optional — omit them for a plain always-on redirect. See the field table above for their shapes. condition2/value2 are likewise optional per rule (and mutually exclusive with the schedule_* fields — see the schedule-condition note above).
Example — cURL
curl -X POST "https://api.qrforge.link/v1/qr-codes" \
-H "x-api-key: api_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "API happy path",
"redirect_url": "https://example.com/api-happy",
"type": "url"
}'
Example — Response
{
"ok": true,
"qr_id": "wUF83EsmkvjkgdsRSCAE",
"project_id": "NDcK35kvVopZCn26gTm3",
"status": "active",
"slug": null
}
Errors
| Status | Error | Cause |
|---|---|---|
| 402 | quota_exceeded_qr | The plan's QR-count cap has been reached. |
| 422 | unsafe_url | The redirect_url failed a Web Risk safety check. |
| 403 | feature_disabled | Either QR creation isn't included in the caller's plan, or redirect_rules/expires_at/scan_limit were set but the plan doesn't include Enterprise/Agency-tier advanced redirect features. |
Retrieve a Single QR Code
GET https://api.qrforge.link/v1/qr-codes/{id}
Example — cURL
curl -X GET "https://api.qrforge.link/v1/qr-codes/wUF83EsmkvjkgdsRSCAE" \
-H "x-api-key: api_live_xxxxxxxxxxxxxxxxxxxxx"
Example — Response
{
"ok": true,
"qr_id": "wUF83EsmkvjkgdsRSCAE",
"label": "API happy path",
"slug": "qr-1ap99pp",
"status": "active",
"type": "url",
"project_id": "NDcK35kvVopZCn26gTm3",
"redirect_url": "https://example.com/api-happy",
"redirect_rules": [],
"expires_at": null,
"scan_limit": null,
"scan_count": 12,
"created_at": "2025-11-19T12:36:32.654Z",
"updated_at": "2025-11-19T12:36:36.092Z",
"last_rendered_at": "2025-11-19T12:36:47.697Z"
}
Retrieve a QR Code by Slug
Retrieve a QR code using its public slug. This is useful for routing and public integrations.
GET /v1/qr-codes/find?slug={slug}
Example — cURL
curl -X GET "https://api.qrforge.link/v1/qr-codes/find?slug=qr-1ap99pp" \
-H "x-api-key: api_live_xxxxxxxxxxxxxxxxxxxxx"
Once a QR is archived, this endpoint 404s for it — see the note under "Archive a QR Code" below.
Update a QR Code
Updates the QR Code — most commonly the redirect_url.
PATCH /v1/qr-codes/{qr_id}
Content-Type: application/json
Body
{
"redirect_url": "https://example.com/new-destination"
}
redirect_rules, expires_at, and scan_limit can also be updated (or cleared, by passing null) in the same request — see the field table above.
redirect_url can only be updated when the QR's type is "url" — otherwise the request fails with a 400 invalid_request.
Example — cURL
curl -X PATCH "https://api.qrforge.link/v1/qr-codes/wUF83EsmkvjkgdsRSCAE" \
-H "x-api-key: api_live_xxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"redirect_url": "https://example.com/new-destination"
}'
Example — Response
{
"ok": true,
"qr_id": "wUF83EsmkvjkgdsRSCAE",
"label": "API happy path",
"slug": "qr-1ap99pp",
"status": "active",
"type": "url",
"project_id": "NDcK35kvVopZCn26gTm3",
"redirect_url": "https://example.com/new-destination",
"redirect_rules": [],
"expires_at": null,
"scan_limit": null,
"scan_count": 12,
"created_at": "2025-11-19T12:36:32.654Z",
"updated_at": "2025-11-20T08:15:00.000Z",
"last_rendered_at": "2025-11-20T08:15:30.000Z"
}
Errors
| Status | Error | Cause |
|---|---|---|
| 402 | quota_exceeded_qr | Reactivating an archived QR (status: "active") would push the account's active-QR count over its plan cap. Un-archiving consumes quota just like creating a new QR. |
| 403 | feature_disabled | redirect_rules/expires_at/scan_limit were set but the plan doesn't include Enterprise/Agency-tier advanced redirect features. |
| 400 | invalid_request | redirect_url was set on a QR whose type isn't "url", or redirect_rules exceeded the 20-entry limit. |
Archive a QR Code
To archive a QR Code, update its status:
PATCH /v1/qr-codes/{id}
Body:
{
"status": "archived"
}
Once a QR is archived, it becomes invisible to GET /v1/qr-codes/{id}, GET /v1/qr-codes/find, and (by default) GET /v1/qr-codes — all of them will 404 or omit it, not just the list endpoint. Pass status=archived to the list endpoint to include archived QRs.
Best Practices
- Use meaningful names such as “Homepage QR”, “Campaign A – CTA”.
- Do not delete slugs; they are used in analytics and history.
- Use archiving to manage active quota.
- Always pass a project_id to maintain clean organization.
- Use the slug lookup endpoint for fast public routing integrations.
Next: Analytics API