Shortnd Docs

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

FieldTypeDefaultNotes
urlIdintegerURL-bound QRs. Mutually exclusive with data.
datastringStandalone payload, ≤ 2 KB.
sizeinteger512Pixel dimensions, 200–2048.
formatenumpngpng / svg / webp.
foregroundColorhex#0000006-digit hex.
backgroundColorhex#FFFFFF6-digit hex.
logoUrlstring (URL)nullEmbedded logo at center. Use a high-EC level when set.
errorCorrectionLevelenumML / M / Q / H. Higher = more redundancy, larger code.
stylePresetstringdefaultTheming 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);
QueryNotes
limitDefault 25, max 100.
offsetDefault 0.
urlIdFilter to QRs for one URL.
standalonetrue to filter to QRs without a urlId.

Read, update branding, soft-delete

MethodPathScope
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);
QueryDefaultNotes
since30 days agoISO-8601
untilnowISO-8601
groupBydayhour or day
limit10Top-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:

TierQR codes / month
Free / Sandbox25
Developer1,000
Scale25,000
EnterpriseUnlimited

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.