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 eyJ..." \
-H "Content-Type: application/json" \
-d '{
"urlId": 12345,
"size": 512,
"format": "png",
"foregroundColor": "#000000",
"backgroundColor": "#FFFFFF",
"errorCorrectionLevel": "M"
}'
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 eyJ..." \
-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"
}'
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 eyJ..."
| 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 eyJ..."
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 eyJ..."
| 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.