QR Codes
Generate, brand, manage, and analyze QR codes — both URL-bound and standalone.
/api/v1/qr covers everything QR: branded QRs for short links, standalone QRs for vCards / Wi-Fi / arbitrary payloads, and per-QR scan analytics.
Scopes: qr:read (list / read / scans), qr:write (create / update / delete).
Every QR is org-scoped via qr_codes.organization_id. URL-bound QRs additionally tie back to the parent URL via urlId.
Create a QR for an existing URL
The most common path: generate a QR that encodes a Shortnd short link. The encoded payload is built from the URL's shortCode + the public host (https://shortnd.com/<shortCode>).
curl -X POST https://shortnd.com/api/v1/qr \
-H "Authorization: Bearer $SHORTND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urlId": 12345,
"size": 512,
"format": "png",
"foregroundColor": "#000000",
"backgroundColor": "#FFFFFF",
"errorCorrectionLevel": "M"
}'const res = await fetch('https://shortnd.com/api/v1/qr', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SHORTND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
urlId: 12345,
size: 512,
format: 'png',
foregroundColor: '#000000',
backgroundColor: '#FFFFFF',
errorCorrectionLevel: 'M',
}),
});
const { data } = await res.json();import os, requests
res = requests.post(
"https://shortnd.com/api/v1/qr",
headers={"Authorization": f"Bearer {os.environ['SHORTND_API_KEY']}"},
json={
"urlId": 12345,
"size": 512,
"format": "png",
"foregroundColor": "#000000",
"backgroundColor": "#FFFFFF",
"errorCorrectionLevel": "M",
},
)
qr = res.json()["data"]body := strings.NewReader(`{"urlId":12345,"size":512,"format":"png","foregroundColor":"#000000","backgroundColor":"#FFFFFF","errorCorrectionLevel":"M"}`)
req, _ := http.NewRequest("POST", "https://shortnd.com/api/v1/qr", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHORTND_API_KEY"))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)$ch = curl_init("https://shortnd.com/api/v1/qr");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SHORTND_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"urlId" => 12345,
"size" => 512,
"format" => "png",
"foregroundColor" => "#000000",
"backgroundColor" => "#FFFFFF",
"errorCorrectionLevel" => "M",
]),
]);
$qr = json_decode(curl_exec($ch), true)["data"];The URL must belong to the calling org. Response (truncated):
{
"success": true,
"data": {
"id": "qr_uuid",
"urlId": "12345",
"organizationId": "org_uuid",
"qrData": "https://shortnd.com/abc123",
"format": "png",
"size": 512,
"errorCorrection": "M",
"foregroundColor": "#000000",
"backgroundColor": "#FFFFFF",
"logoUrl": null,
"downloadCount": 0,
"scanCount": 0,
"downloadUrl": "/api/qr/qr_uuid/download",
"previewUrl": "/qr/qr_uuid",
"createdAt": "2026-05-08T10:30:00Z"
}
}Create a standalone QR
Pass data instead of urlId to encode an arbitrary payload — vCards, Wi-Fi credentials, third-party URLs, raw text. Up to 2 KB.
curl -X POST https://shortnd.com/api/v1/qr \
-H "Authorization: Bearer $SHORTND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": "BEGIN:VCARD\nVERSION:3.0\nFN:Tosh\nTEL:+1-555-0123\nEMAIL:hi@acme.com\nEND:VCARD",
"size": 512,
"format": "svg"
}'const vcard = [
'BEGIN:VCARD',
'VERSION:3.0',
'FN:Tosh',
'TEL:+1-555-0123',
'EMAIL:hi@acme.com',
'END:VCARD',
].join('\n');
await fetch('https://shortnd.com/api/v1/qr', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SHORTND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: vcard, size: 512, format: 'svg' }),
});import os, requests
vcard = "\n".join([
"BEGIN:VCARD", "VERSION:3.0",
"FN:Tosh", "TEL:+1-555-0123", "EMAIL:hi@acme.com",
"END:VCARD",
])
requests.post(
"https://shortnd.com/api/v1/qr",
headers={"Authorization": f"Bearer {os.environ['SHORTND_API_KEY']}"},
json={"data": vcard, "size": 512, "format": "svg"},
).raise_for_status()vcard := "BEGIN:VCARD\nVERSION:3.0\nFN:Tosh\nTEL:+1-555-0123\nEMAIL:hi@acme.com\nEND:VCARD"
payload, _ := json.Marshal(map[string]any{"data": vcard, "size": 512, "format": "svg"})
req, _ := http.NewRequest("POST", "https://shortnd.com/api/v1/qr", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHORTND_API_KEY"))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)$vcard = "BEGIN:VCARD\nVERSION:3.0\nFN:Tosh\nTEL:+1-555-0123\nEMAIL:hi@acme.com\nEND:VCARD";
$ch = curl_init("https://shortnd.com/api/v1/qr");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SHORTND_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["data" => $vcard, "size" => 512, "format" => "svg"]),
]);
curl_exec($ch);Standalone QRs return urlId: null and are still scoped to your org. They count against the same qr_generated meter as URL-bound QRs.
Customization fields
| Field | Type | Default | Notes |
|---|---|---|---|
urlId | integer | — | URL-bound QRs. Mutually exclusive with data. |
data | string | — | Standalone payload, ≤ 2 KB. |
size | integer | 512 | Pixel dimensions, 200–2048. |
format | enum | png | png / svg / webp. |
foregroundColor | hex | #000000 | 6-digit hex. |
backgroundColor | hex | #FFFFFF | 6-digit hex. |
logoUrl | string (URL) | null | Embedded logo at center. Use a high-EC level when set. |
errorCorrectionLevel | enum | M | L / M / Q / H. Higher = more redundancy, larger code. |
stylePreset | string | default | Theming preset for qr-code-styling. |
List & filter
curl 'https://shortnd.com/api/v1/qr?limit=25&offset=0&urlId=12345' \
-H "Authorization: Bearer $SHORTND_API_KEY"const res = await fetch('https://shortnd.com/api/v1/qr?limit=25&offset=0&urlId=12345', {
headers: { Authorization: `Bearer ${process.env.SHORTND_API_KEY}` },
});
const { data } = await res.json();import os, requests
res = requests.get(
"https://shortnd.com/api/v1/qr",
params={"limit": 25, "offset": 0, "urlId": 12345},
headers={"Authorization": f"Bearer {os.environ['SHORTND_API_KEY']}"},
)
data = res.json()["data"]req, _ := http.NewRequest("GET",
"https://shortnd.com/api/v1/qr?limit=25&offset=0&urlId=12345", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHORTND_API_KEY"))
http.DefaultClient.Do(req)$ch = curl_init("https://shortnd.com/api/v1/qr?limit=25&offset=0&urlId=12345");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHORTND_API_KEY")],
]);
curl_exec($ch);| Query | Notes |
|---|---|
limit | Default 25, max 100. |
offset | Default 0. |
urlId | Filter to QRs for one URL. |
standalone | true to filter to QRs without a urlId. |
Read, update branding, soft-delete
| Method | Path | Scope |
|---|---|---|
GET | /api/v1/qr/{id} | qr:read |
PATCH | /api/v1/qr/{id} | qr:write |
DELETE | /api/v1/qr/{id} | qr:write |
PATCH accepts the customization fields above (size, format, colors, logoUrl, errorCorrectionLevel). The encoded payload is immutable — to retarget a URL-bound QR, create a new one and retire the old one.
DELETE is a soft delete. The row remains for analytics continuity but is filtered out of list responses.
List QRs for a URL
A convenience over GET /api/v1/qr?urlId=… for buyers who already have a URL id:
curl https://shortnd.com/api/v1/urls/12345/qr \
-H "Authorization: Bearer $SHORTND_API_KEY"const res = await fetch('https://shortnd.com/api/v1/urls/12345/qr', {
headers: { Authorization: `Bearer ${process.env.SHORTND_API_KEY}` },
});
const { data } = await res.json();import os, requests
res = requests.get(
"https://shortnd.com/api/v1/urls/12345/qr",
headers={"Authorization": f"Bearer {os.environ['SHORTND_API_KEY']}"},
)
data = res.json()["data"]req, _ := http.NewRequest("GET", "https://shortnd.com/api/v1/urls/12345/qr", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHORTND_API_KEY"))
http.DefaultClient.Do(req)$ch = curl_init("https://shortnd.com/api/v1/urls/12345/qr");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHORTND_API_KEY")],
]);
curl_exec($ch);Per-QR scan analytics
GET /api/v1/qr/{id}/scans mirrors the per-URL clicks shape so the same renderer in your app can handle both. Bot scans are excluded (is_bot = false).
curl 'https://shortnd.com/api/v1/qr/qr_uuid/scans?since=2026-04-08T00:00:00Z&groupBy=day&limit=10' \
-H "Authorization: Bearer $SHORTND_API_KEY"const url = new URL('https://shortnd.com/api/v1/qr/qr_uuid/scans');
url.search = new URLSearchParams({
since: '2026-04-08T00:00:00Z',
groupBy: 'day',
limit: '10',
}).toString();
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SHORTND_API_KEY}` },
});
const { data } = await res.json();import os, requests
res = requests.get(
"https://shortnd.com/api/v1/qr/qr_uuid/scans",
params={"since": "2026-04-08T00:00:00Z", "groupBy": "day", "limit": 10},
headers={"Authorization": f"Bearer {os.environ['SHORTND_API_KEY']}"},
)
data = res.json()["data"]req, _ := http.NewRequest("GET",
"https://shortnd.com/api/v1/qr/qr_uuid/scans?since=2026-04-08T00:00:00Z&groupBy=day&limit=10",
nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHORTND_API_KEY"))
http.DefaultClient.Do(req)$qs = http_build_query([
"since" => "2026-04-08T00:00:00Z",
"groupBy" => "day",
"limit" => 10,
]);
$ch = curl_init("https://shortnd.com/api/v1/qr/qr_uuid/scans?$qs");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHORTND_API_KEY")],
]);
curl_exec($ch);| Query | Default | Notes |
|---|---|---|
since | 30 days ago | ISO-8601 |
until | now | ISO-8601 |
groupBy | day | hour or day |
limit | 10 | Top-N per breakdown dimension (max 50) |
Response shape:
{
"success": true,
"data": {
"qrCodeId": "qr_uuid",
"urlId": "12345",
"since": "2026-04-08T00:00:00Z",
"until": "2026-05-08T10:30:00Z",
"totals": { "totalScans": 412, "uniqueScans": 388, "uniqueIps": 351 },
"breakdowns": {
"country": [{ "country": "US", "scans": 220 }],
"device": [{ "deviceType": "mobile", "scans": 380 }],
"browser": [{ "browser": "Safari", "scans": 199 }],
"scanMethod": [{ "scanMethod": "camera", "scans": 320 }, { "scanMethod": "app_scanner", "scans": 92 }],
"scannerApp": [{ "scannerApp": "iOS Camera", "scans": 188 }]
},
"timeseries": [
{ "bucket": "2026-04-08T00:00:00Z", "scans": 24, "uniques": 22 }
]
}
}scanMethod values: camera, file_upload, manual_entry, app_scanner. Useful for measuring physical-print-to-scan conversion vs. in-app scans.
Image bytes
Every QR record returns a downloadUrl pointing at /api/qr/{id}/download. That endpoint streams the raw image bytes in the QR's stored format. It sits outside /api/v1 today (legacy, public, scope-free) — treat it as a public CDN-style URL safe to embed anywhere. A future v1 wrapper with scope-checked download is on the roadmap.
The previewUrl (/qr/{id}) renders an HTML preview page suitable for inline previews in marketing flows.
Billing meter
Every successful POST /api/v1/qr emits one qr_generated event. Tier bundles:
| Tier | QR codes / month |
|---|---|
| Free / Sandbox | 25 |
| Developer | 1,000 |
| Scale | 25,000 |
| Enterprise | Unlimited |
Overage: $0.05/QR (Developer) → $0.03/QR (Scale). Branded QRs (logo + custom colors) cost the same as plain — the meter counts records, not rendering complexity.
PATCH and DELETE do not charge — only generation.
QR events on webhooks
Subscribe to qr.created, qr.scan_threshold, and qr.deleted to react to QR lifecycle events. See Webhooks for the signature scheme.