QR Codes API
Overview
The Codes API is the core of qr3.app. Use it to create, update, and delete dynamic and static QR codes.
Base URL: https://qr3.app/v1/codes
Authoritative REST contract
The following contract applies to every client:
POST /v1/codesaccepts onlyurl,vcard,wifi,email,sms, andlocation. Fields are flat:url; at leastvcard_first_nameorvcard_last_name;wifi_ssid;email_to;sms_phone; or bothlocation_latandlocation_lng.- Only
urlcodes can be dynamic. Create requests may optionally includeexpires_atas an ISO-8601 timestamp;ab_enabled,ab_target_url_bandab_weight_a(A/B destinations) are accepted for dynamicurlcodes;redirect_after_expiryis not part of the create contract. GET /v1/codessupports onlylimit,cursor, andstatus(live,paused,flagged,draft).POST /v1/codes/batchacceptsurl,vcard, andwifi. Limits are 10 for Free, 500 for Pro, and 1,000 for Business, Agency, and Enterprise per request. URL scans run synchronously for at most 50 URL items; above 50,skip_url_scan: trueis required.
Create a QR Code
POST /v1/codes
curl -X POST https://qr3.app/v1/codes \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{ "type": "url", "url": "https://example.com", "title": "My first QR code", "tags": ["marketing", "q1"], "is_dynamic": true }'const code = await qr3.codes.create({ type: 'url', url: 'https://example.com', title: 'My first QR code', tags: ['marketing', 'q1'], is_dynamic: true,});qr3 create https://example.com --title "My QR code" --tags marketing,q1Response (HTTP 201):
{ "data": { "id": "qr_a1b2c3d4", "short_code": "r7f3Kx", "redirect_url": "https://qr3.app/r7f3Kx", "image_svg_url": "https://qr3.app/v1/codes/r7f3Kx/qr.svg", "image_png_url": "https://qr3.app/v1/codes/r7f3Kx/qr.png", "image_pdf_url": "https://qr3.app/v1/codes/r7f3Kx/qr.pdf", "image_eps_url": "https://qr3.app/v1/codes/r7f3Kx/qr.eps", "type": "url", "status": "live", "is_dynamic": true, "total_scans": 0, "created_at": "2026-03-15T10:00:00.000Z" }, "meta": { "request_id": "req_xyz" }}Batch creation
POST /v1/codes/batch
curl -X POST https://qr3.app/v1/codes/batch \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{ "items": [ { "type": "url", "url": "https://product-1.example.com", "tags": ["batch"] }, { "type": "url", "url": "https://product-2.example.com", "tags": ["batch"] }, { "type": "wifi", "wifi_ssid": "GuestWiFi", "wifi_password": "secret123" } ], "skip_url_scan": false }'Response (HTTP 201):
{ "data": { "created": [ { "id": "qr_...", "short_code": "abc123", "redirect_url": "https://qr3.app/abc123" }, { "id": "qr_...", "short_code": "def456", "redirect_url": "https://qr3.app/def456" }, { "id": "qr_...", "short_code": "ghi789", "status": "live" } ], "total": 3, "failed": 0 }}Per-request limits are 10 for Free, 500 for Pro, and 1,000 for Business, Agency, and Enterprise. For more than 50 URL items, set skip_url_scan: true (see the contract above).
List QR Codes
GET /v1/codes
GET /v1/codes?limit=20&status=liveSupported query parameters: limit, cursor, and status (live, paused, flagged, or draft).
Get a QR Code
GET /v1/codes/:id
GET /v1/codes/qr_a1b2c3d4Update a QR Code
PATCH /v1/codes/:id
Update contract: url may be changed only for an existing code with type: "url", and its value must start with http:// or https://. Other code types must not receive url; invalid requests return 422.
curl -X PATCH https://qr3.app/v1/codes/qr_a1b2c3d4 \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{"url": "https://new-destination.com", "status": "live"}'Landing page & external links
For dynamic url codes you can set is_landing_page: true (on create or via PATCH). A scan then renders a qr3-hosted page listing the code’s public files and external links instead of redirecting. External links are sent as a links array:
{ "is_landing_page": true, "links": [ { "label": "Datasheet", "url": "https://example.com/datasheet.pdf" }, { "label": "Certificate", "url": "https://example.com/certificate.pdf" } ]}- 0–20 links per code;
labelis 1–100 characters;urlmust behttp(s)(≤ 2048 characters). - Each URL is checked with Google Web Risk — an unsafe URL returns
422. - If Web Risk is unreachable when you save, the link is still accepted but flagged for re-scan. A daily job re-checks stored links (and periodically re-checks clean ones) and automatically pauses the code if a link is later found unsafe.
- Sending
"links": []clears all links. See the Landing page guide.
Delete a QR Code
DELETE /v1/codes/:id
Soft-deletes the code. Active scans are preserved for analytics.
A second DELETE of the same code returns 404, even if both requests arrive at the same time. The qr.deleted webhook is sent exactly once.
QR Code Types
| Type | Content encoded |
|---|---|
url | URL or redirect short link |
vcard | vCard 3.0 contact information |
wifi | Wi-Fi connection (SSID, password, encryption) |
email | mailto: with pre-filled subject and body |
sms | smsto: with pre-filled message |
location | geo: coordinates |
QR Code Images
Every code exposes four image URLs:
GET /v1/codes/:shortCode/qr.svg → SVG vector (scalable)GET /v1/codes/:shortCode/qr.png → PNG rasterGET /v1/codes/:shortCode/qr.pdf → Vector PDF (square by default, or A4)GET /v1/codes/:shortCode/qr.eps → EPS vector (pro print workflows)Optional query params: size (2–20 modules per pixel; SVG/PNG/EPS only), format (square default — code plus its required quiet zone, no A4 whitespace — or a4 for a printable sheet), title.
Colors and error correction
All four image routes accept three optional parameters. They apply to this one request and take precedence over colors stored on the code (next section).
| Parameter | Values | Default | SVG, PNG | PDF, EPS |
|---|---|---|---|---|
fg | hex RRGGBB or RGB | 000000 | drawn | drawn as a print color |
bg | hex like fg, or transparent | ffffff | drawn | drawn as a print color |
ecc | L, M, Q, H | M | applies | applies |
- Format: case-insensitive, the
#is optional. If you send it, encode it as%23. - Invalid values count as not set.
?fg=purplereturns the color stored on the code, or the normal black if none is stored, with200and never an error. Only an explicit?fg=000000forces black. bg=transparentreturns an SVG, PDF or EPS without a background and a PNG with a real alpha channel. The surface you place the code on must be light and leave a 4-module quiet zone clear on all sides. A dark code on a dark surface cannot be scanned.- PDF and EPS write black and grey as grayscale (black plate only) and any other color as CMYK in whole percent, for example
1F4E79as C74 M36 Y0 K53. As soon as a color is chosen, an opaque fill sits behind the code and its quiet zone, white or in thebgcolor, as in the SVG. Without colors both files stay unchanged. Conversion and limits: Colors in print. - Contrast: the image route does not check it. Use at least 4:1 and dark modules on a light background.
#1F4E79on white has 8.7:1,#ff6600on white only 2.9:1. eccchanges the module pattern, not the content. A printed code keeps working, but never mix old and new print files.QorHmake the code more robust, e.g. on corrugated board. A logo always forcesH.- With a logo the area behind the logo stays white, even on a colored or transparent background.
- Cache: only the actual standard image (black on white, error correction M, no logo) is served as immutable for 24 hours; every other rendering for 5 minutes. For PDF and EPS any given background counts as a deviation:
bg=ffffffdraws a white fill there that the standard file does not have. - Plans: colors and error correction are available on every plan, including Free.
# Dark blue code on whitecurl "https://qr3.app/v1/codes/r7f3Kx/qr.svg?fg=1F4E79" -o qr-blue.svg
# Transparent PNG for a layout on a light surfacecurl "https://qr3.app/v1/codes/r7f3Kx/qr.png?size=10&bg=transparent" -o qr-transparent.png
# More robust for corrugated board: error correction Qcurl "https://qr3.app/v1/codes/r7f3Kx/qr.pdf?ecc=Q" -o qr-q.pdf// @qr3/sdk 1.2.0 or later — imageUrl() only builds the URL, it sends no request// Dark blue code on whiteqr3.codes.imageUrl('r7f3Kx', { format: 'svg', fg: '1F4E79' });// → https://qr3.app/v1/codes/r7f3Kx/qr.svg?fg=1F4E79
// Transparent PNG for a layout on a light surfaceqr3.codes.imageUrl('r7f3Kx', { format: 'png', size: 10, bg: 'transparent' });
// More robust for corrugated board: error correction Qqr3.codes.imageUrl('r7f3Kx', { format: 'pdf', ecc: 'Q' });Store colors on the code
PATCH /v1/codes/:id stores colors on the code as appearance. The image routes then draw them by default, without any parameter.
curl -X PATCH https://qr3.app/v1/codes/qr_a1b2c3d4 \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{"appearance": {"foreground_color": "#1F4E79"}}'// @qr3/sdk 1.2.0 or laterconst code = await qr3.codes.update('qr_a1b2c3d4', { appearance: { foreground_color: '#1F4E79' },});code.appearance; // { foreground_color: '#1F4E79', background_color: null }code.issues; // contrast check findings, absent when there are noneResponse (HTTP 200, shortened):
{ "data": { "id": "qr_a1b2c3d4", "short_code": "r7f3Kx", "appearance": { "foreground_color": "#1F4E79", "background_color": null } }, "meta": { "request_id": "req_xyz", "issues": [] }}- Values:
foreground_coloras#RRGGBB,background_coloras#RRGGBBortransparent. Other keys return400. - Merging: a field you leave out keeps its stored value.
nullresets one field,"appearance": nullresets both. Black and white are not stored; the response shows them asnull. - Every code response carries
appearance, and so do theqr.createdandqr.updatedwebhooks. - Order in the image routes: query parameter first, then the stored color, then the standard.
?fg=000000therefore returns the black print file of a colored code. - Embedded images: An image URL follows the stored colors wherever it does not set them itself with
fgandbg: a URL without parameters for both colors,?fg=000000for the background,?ecc=Qfor both as well. After a color change, a page that embeds such a URL can still show the old image: for up to 24 hours if the URL returned the standard image until then (black on white, error correction M, no logo), otherwise for up to 5 minutes. The remedy is a parameter of your own on the URL that changes with every color change, such as?v=2or the code’supdated_atas in the dashboard. The image routes ignore unknown parameters. - Error correction is never stored. Choose it per download with
?ecc=. - Only via
PATCH:POST /v1/codes, the batch and the import rejectappearancewith422. - PDF and EPS draw stored colors as print colors, the same as the parameters. Because white is never stored, a code with a stored foreground color gets a white fill there, as in the SVG.
Contrast check
The API checks the pair that results from your request plus the stored value:
| Level | When | Response |
|---|---|---|
blocked | contrast below 1.5:1 | 422, nothing is stored |
critical | below 2:1 or a luminance difference below 0.30; a warning together with a logo; every transparent background | 200 with meta.issues |
warning | below 4:1 or a luminance difference below 0.50; light modules on a dark background | 200 with meta.issues |
| ok | everything else | 200, meta.issues is empty |
Each entry in meta.issues has code, severity, field, message and optional hints such as contrast_ratio — the same shape as the compliance issues of a Digital Product Passport. A transparent background is never blocked, because a light code on dark packaging is a real use case. The surface still needs clear contrast and a free quiet zone of 4 modules on every side.
Uploading and removing a logo (POST and DELETE /v1/codes/:id/logo) run the same check on the stored colors and also return the findings in meta.issues: with a logo a warning becomes critical, without one it is a warning again.
If another request changes the same code at the same moment, the API applies your changes to the latest state. Only if that fails three times in a row does it answer 409; reload the code and try again.
Logo
POST /v1/codes/:id/logo
Multipart request, field file: PNG, JPEG or WebP, up to 1 MB, checked by magic bytes. Normalizes the image to a 512×512 transparent PNG and replaces any existing logo.
curl -X POST https://qr3.app/v1/codes/qr_a1b2c3d4/logo \ -H "Authorization: Bearer qr3_sk_..." \Response (HTTP 201): the updated code, with logo_file_id set.
DELETE /v1/codes/:id/logo
Removes the logo and deletes the stored object. Idempotent — calling it with no logo set still returns 200.
If another request changes the same code’s logo at the same moment (a second upload or a removal), POST and DELETE /v1/codes/:id/logo return 409 (errors/conflict) and change nothing; an uploaded image is discarded. Reload the code and try again. Two simultaneous calls of DELETE /v1/codes/:id/logo are not a conflict; both return 200.
Once set, all four formats — qr.svg, qr.png, qr.pdf and qr.eps — embed the logo’s pixels and raise error correction to H. See Logo in the QR code for the full contract, including which change (add/remove vs. replace) alters the module pattern, and for print guidance.
Comments
Comments enable feedback loops between agencies and their clients.
GET /v1/codes/:id/comments
POST /v1/codes/:id/comments
PATCH /v1/codes/:id/comments/:commentId
DELETE /v1/codes/:id/comments/:commentId
Comments created from the dashboard are attributed to the acting user (author_id); comments created with a plain API key stay unattributed (author_id: null). A comment can be deleted only by its own author or by an org_admin/ws_admin — a plain API key can delete only unattributed, API-created comments.
# Add a commentcurl -X POST https://qr3.app/v1/codes/qr_a1b2c3d4/comments \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{ "body": "Please match the QR code to the green background.", "author_name": "Jane Doe" }'
# List open commentscurl "https://qr3.app/v1/codes/qr_a1b2c3d4/comments?resolved=false" \ -H "Authorization: Bearer qr3_sk_..."
# Mark a comment as resolvedcurl -X PATCH https://qr3.app/v1/codes/qr_a1b2c3d4/comments/cmt_xyz \ -H "Authorization: Bearer qr3_sk_..." \ -H "Content-Type: application/json" \ -d '{ "resolved": true }'