QR Code Harbor API Live — Business plan

Generate and manage QR codes programmatically. Included with the Business plan.

Download OpenAPI spec (openapi.json)

1. Overview

The QR Code Harbor API lets you create, update, and read QR codes programmatically instead of using the web generator by hand. It's built for teams that need to generate QR codes at scale — from a print shop batching hundreds of table-tent codes to a SaaS app that mints a unique QR code per user record.

The API is available to Business plan subscribers and is fully supported for production use.

2. Who it's for

  • Agencies — spin up branded QR codes for every client campaign without touching the dashboard.
  • Restaurants — generate a dynamic menu QR code per table or location and update the destination without reprinting.
  • Print shops — batch-generate QR codes for run-of-press jobs (business cards, flyers, packaging).
  • Events — issue a unique check-in or badge QR code per attendee from your registration system.
  • Real estate — auto-create a listing QR code (photos, virtual tour link) whenever a new property goes live.
  • SaaS platforms — embed a QR code creation step directly into your own product's onboarding or export flow.
  • Schools — generate WiFi or resource-link QR codes per classroom or department.
  • E-commerce — attach a unique QR code to each SKU or package for warranty registration or reorder links.

3. Availability

API access requires the Business (or Founder) plan.

FeatureStatus
Static QR API — all 37 content typeslive
Dynamic QR APIlive
Scan analytics APIlive
SVG exportlive
PNG / PDF export (QR codes only)live
Bulk generation (JSON array, up to 1,000 rows)live
Bulk CSV file upload / ZIP downloadnot offered — use the JSON bulk endpoint
Barcode API (38 symbologies, plan-gated)live — SVG and PNG
Scan-event webhooks (dynamic codes)live
Styling: rounded/dot module & eye shapes, gradientslive — limited subset, see §10

4. Authentication

Every request must include your API key using one of these two equivalent header options:

Authorization: Bearer qch_live_xxxxxxxxxxxxxxxx

or

X-API-Key: qch_live_xxxxxxxxxxxxxxxx

Keys are created from Account → API Keys (Business plan only — see account.html). A key is shown in full only once at creation time. Treat it like a password: never share it, never commit it to a public repo, and never embed it in client-side/browser code. If a key is compromised, revoke it immediately from the API Keys card and create a new one.

5. Quickstart

Create a static URL QR code with a single cURL request:

curl -X POST https://qrcodeharbor.com/api/v1/qr/static \
  -H "Authorization: Bearer qch_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "url",
    "data": {
      "url": "https://example.com/menu"
    },
    "fg": "#191919",
    "bg": "#FFFFFF",
    "size": 512,
    "margin": 2,
    "errorCorrectionLevel": "M",
    "format": "svg"
  }'

Example response:

{
  "id": "qr_8f2c1a9d1e6b4a2f9c0d5e7b1a3f6c8d",
  "mode": "static",
  "type": "url",
  "format": "svg",
  "download_url": "https://qrcodeharbor.com/api/v1/qr/qr_8f2c1a9d1e6b4a2f9c0d5e7b1a3f6c8d/download?format=svg",
  "created_at": 1751380920000
}

Fetch the image itself (SVG by default) with a follow-up GET on download_url — see Endpoints below.

6. Endpoints

Method & pathDescriptionAuth
POST /api/v1/qr/staticCreate a static QR code (content baked in; cannot be edited later). Accepts format: svg, png, or pdf.API key
POST /api/v1/qr/dynamicCreate a dynamic QR code (redirects through qrcodeharbor.com/r/{id}; destination is editable, scans are tracked).API key
GET /api/v1/qr/{id}Retrieve metadata for a static QR code you created.API key
GET /api/v1/qr/{id}/download?format=svg|png|pdfDownload the QR code image in any supported format. Works for both static codes and the auto-generated download for dynamic codes.API key
GET /api/v1/qr/dynamic/{id}Retrieve metadata (destination, scan count, short URL) for a dynamic QR code.API key
PATCH /api/v1/qr/dynamic/{id}Update a dynamic QR code's destination URL. Static codes cannot be edited — create a new one instead.API key
GET /api/v1/qr/dynamic/{id}/analyticsRead scan analytics for a dynamic QR code (device, location, referrer, daily/hourly time series, recent scans).API key
POST /api/v1/qr/bulkCreate up to 1,000 static QR codes in one call from a JSON array of rows (each row can be a different type). Each item gets its own download_url supporting svg/png/pdf.API key
POST /api/v1/barcodeGenerate a barcode (SVG only). symbology must be one your plan allows — see Barcodes.API key
GET /api/v1/webhooksList your scan-event webhooks.API key
POST /api/v1/webhooksRegister a webhook that fires on every scan of a dynamic QR code you own.API key
DELETE /api/v1/webhooks/{id}Delete a webhook.API key
GET /api/keysList your account's API keys (never returns the full secret).Sign-in (Firebase)
POST /api/keysCreate a new API key for your account (also available from Account → API Keys).Sign-in (Firebase)
DELETE /api/keys/{id}Revoke an API key. Requests using it are rejected immediately afterward.Sign-in (Firebase)

Note: the three /api/keys endpoints manage your account's API keys themselves, so they're authenticated with your normal QR Code Harbor sign-in (a Firebase Authorization: Bearer <id-token>), not with an API key. Every other endpoint on this page is authenticated with an API key as described in Authentication.

Not offered: CSV file upload with ZIP-download output. Use the JSON array POST /api/v1/qr/bulk endpoint instead — it supports the same volume (up to 1,000 rows per call) and returns per-item download links in the response.

7. QR type schemas

Each create request has a type and a data object. All 37 content types from the web generator are supported — full parity. Field sets per type:

url

{ "type": "url", "data": { "url": "https://example.com" } }

text

{ "type": "text", "data": { "text": "Any plain text content" } }

email

{
  "type": "email",
  "data": {
    "to": "hello@acme.com",
    "subject": "Question about your product",
    "body": "Hi there,"
  }
}

phone

{ "type": "phone", "data": { "phone": "+1-555-0100" } }

sms

{
  "type": "sms",
  "data": { "phone": "+1-555-0100", "message": "Hey, here's the link" }
}

wifi

{
  "type": "wifi",
  "data": {
    "ssid": "Office-Guest",
    "password": "changeme123",
    "encryption": "WPA",
    "hidden": false
  }
}

vcard

Requires firstName/lastName (or the simpler name), or company. Optional extraFields array mirrors the web generator's "add field" feature — each item is { "type": "phone"|"email"|"url"|"social"|"note", "value": "..." }.

{
  "type": "vcard",
  "data": {
    "firstName": "Jane",
    "lastName": "Doe",
    "company": "Acme Inc",
    "title": "Product Manager",
    "phone": "+1-555-0100",
    "email": "jane@acme.com",
    "website": "https://acme.com",
    "address": "123 Main St, Springfield",
    "extraFields": [ { "type": "social", "value": "https://linkedin.com/in/janedoe" } ]
  }
}

whatsapp

{
  "type": "whatsapp",
  "data": { "phone": "+1-555-0100", "message": "Hi! I have a question" }
}

facebook / instagram / youtube / twitter / linkedin / tiktok / pinterest

Each takes a single url field — a full profile URL, or (for facebook/instagram/youtube/twitter/linkedin/tiktok/pinterest) just a handle/username, which is expanded against the platform's base URL the same way the web generator does. twitter also accepts an optional tweetId to link a specific post.

{ "type": "instagram", "data": { "url": "@janedoe" } }
{ "type": "twitter", "data": { "url": "@janedoe", "tweetId": "1234567890" } }

snapchat

{ "type": "snapchat", "data": { "username": "janedoe" } }

event

start/end use the format YYYY-MM-DDTHH:MM (local time, no timezone conversion — matches the web generator).

{
  "type": "event",
  "data": {
    "title": "Product Launch",
    "location": "123 Main St, Springfield",
    "start": "2026-09-10T18:00",
    "end": "2026-09-10T21:00",
    "description": "Join us for the launch party"
  }
}

location

Requires either lat/lng, or a free-text address (used as a map search query). Optional format: geo (default, a geo: URI), googleMaps (a Google Maps search link), or both.

{
  "type": "location",
  "data": { "lat": 40.7580, "lng": -73.9855, "label": "HQ" }
}
{
  "type": "location",
  "data": { "address": "Times Square, New York, NY", "format": "googleMaps" }
}

booking

service is one of calendly, eventbrite, opentable, booking, airbnb, resy, tripadvisor, or other. handle can be a username/slug or a full URL.

{ "type": "booking", "data": { "service": "calendly", "handle": "janedoe" } }

payment

app is one of paypal, venmo, cashapp, zelle, wise, revolut, stripe, googlepay, upi, paytm, pix, mbway, bizum, alipay, wechatpay, or other. id is the provider-specific username/ID/link; amount and note are optional (support varies by provider — see the web generator's payment hints for what each provider expects).

{ "type": "payment", "data": { "app": "paypal", "id": "janedoe", "amount": "25.00" } }

crypto

Requires address. coinId is a CoinGecko-style id (e.g. bitcoin, ethereum) used to pick a native URI scheme when known; network (erc20|bep20|trc20|solana|polygon|native) overrides that. Coins/networks without a recognized URI scheme fall back to a plain-text payload (symbol, network, address, amount, memo) rather than a bogus scheme no wallet would recognize.

{ "type": "crypto", "data": { "coinId": "bitcoin", "symbol": "BTC", "address": "bc1q..." } }

googleReviews

{ "type": "googleReviews", "data": { "url": "https://g.page/r/xxxxx/review" } }

coupon

Provide either url (a landing page) or code (+ optional description) for a plain-text coupon payload.

{ "type": "coupon", "data": { "code": "SAVE20", "description": "20% off your next order" } }

pdf / video / image / appstore / audio / calendar

Each takes a single url field (a link to the PDF, video, image, app store listing, audio file, or .ics calendar respectively).

{ "type": "pdf", "data": { "url": "https://example.com/menu.pdf" } }
{ "type": "deeplink", "data": { "url": "myapp://product/123", "params": "ref=qr" } }

mecard

Requires name in "Last,First" format (MECARD convention).

{
  "type": "mecard",
  "data": { "name": "Doe,Jane", "phone": "+1-555-0100", "email": "jane@acme.com" }
}

zoom

{ "type": "zoom", "data": { "meetingId": "123 456 7890", "password": "abc123" } }

spotify / discord / telegram

{ "type": "spotify", "data": { "url": "https://open.spotify.com/track/xyz" } }
{ "type": "discord", "data": { "invite": "https://discord.gg/abc123" } }
{ "type": "telegram", "data": { "username": "@janedoe" } }

bookmark

{ "type": "bookmark", "data": { "title": "QR Code Harbor", "url": "https://qrcodeharbor.com" } }

document

text, up to 4,296 characters. Unlike the web generator (which optionally LZ-compresses with a LZQR: prefix for its own decoder), the API always returns the plain-text payload — there's no guarantee an arbitrary third-party scanner has that decompressor.

{ "type": "document", "data": { "text": "Meeting notes: ..." } }

8. Barcodes

POST /api/v1/barcode renders a barcode using the same bwip-js engine that powers the interactive barcode tool, running headlessly in the API (no browser/canvas involved).

Set format to "svg" (default) or "png". bwip-js's browser build only exposes toCanvas (needs a real DOM canvas — not usable server-side) and toSVG (pure string output, used to generate the barcode), so PNG is produced by rasterizing that SVG server-side with @resvg/resvg-wasm, a WASM build of the Rust resvg renderer that runs directly in the Cloudflare Worker (no DOM/canvas/native binary involved). PNG requests return the raw image bytes (content-type: image/png) instead of the JSON envelope shown below.

{
  "symbology": "code128",
  "data": "HELLO-12345",
  "format": "svg",
  "options": { "scale": 3, "includetext": true, "fg": "#000000", "bg": "#ffffff" }
}

Response (format: "svg"):

{
  "symbology": "code128",
  "format": "svg",
  "svg": "<svg viewBox=\"0 0 435 240\" ...>...</svg>",
  "created_at": 1751380920000
}

format: "png" returns the PNG image bytes directly (no JSON wrapper). An optional options.pngScale (1-4, default 2) multiplies the barcode's native pixel size for a crisper raster — it's independent of options.scale, which controls bwip-js's own module density.

Supported symbologies (38)

Full parity with barcode.html's symbology list — same bcid strings, same validation rules.

CategorySymbologies
Retail (12)ean13, ean8, upca, upce, isbn, issn, gs1-128, gs1datamatrix, gs1qrcode, databaromni, databarexpanded, databarlimited
Linear (10)code128, code39, code93, rationalizedCodabar, msi, interleaved2of5, itf14, code11, pharmacode, telepen
2D (11)qrcode, microqrcode, datamatrix, pdf417, micropdf417, azteccode, maxicode, codablockf, dotcode, hanxin
Postal (6)postnet, onecode, royalmail, auspost, japanpost, kix

Plan gating

Business and Founder both currently unlock the same 13 symbologies (Founder's underlying limits mirror Pro's list plus Business-only extras where grandfathered): ean13, ean8, upca, upce, code128, code39, itf14, datamatrix, pdf417, azteccode, isbn, plus gs1-128 and gs1datamatrix on Business. Requesting a symbology outside your plan's list returns 403 forbidden. Contact support if your use case needs one of the remaining symbologies.

Known format quirks

Data is passed straight to bwip-js, which enforces each symbology's real-world format — a few are stricter than they look:

  • isbn / issn — bwip-js wants the dashed form (e.g. 978-0-306-40615-7), not a bare digit string.
  • postnet — exactly 5, 9, or 11 digits, no hyphen (e.g. 552371237, not 55555-1237).
  • onecode (USPS Intelligent Mail) — exactly 20, 25, 29, or 31 digits.

A rejected format returns 400 validation_error with bwip-js's own error message.

9. Webhooks

Register a webhook to get notified in near-real-time when a dynamic QR code you own is scanned (Business/Founder plans). One webhook per dynamic code.

curl -X POST https://qrcodeharbor.com/api/v1/webhooks \
  -H "Authorization: Bearer qch_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "code_id": "AbC123x", "url": "https://yourapp.com/hooks/qr-scan" }'

Response (the secret is shown only once — store it):

{
  "id": "whk_8f2c1a9d1e6b4a2f",
  "code_id": "AbC123x",
  "url": "https://yourapp.com/hooks/qr-scan",
  "secret": "9f1c...e2a0",
  "status": "active",
  "created_at": 1751380920000
}

Delivery

On every scan of the linked code, functions/r/[id].js fires a POST to your url after the visitor's redirect has already been sent — it never adds latency to the scan, and a slow or failing endpoint never affects the person scanning the code.

POST https://yourapp.com/hooks/qr-scan
Content-Type: application/json
X-Webhook-Event: qr.scan
X-Webhook-Signature: <hex HMAC-SHA256 of the raw body, using your secret>

{
  "event": "qr.scan",
  "code_id": "AbC123x",
  "webhook_id": "whk_8f2c1a9d1e6b4a2f",
  "data": {
    "ts": 1751380920000,
    "country": "US", "city": "New York", "region": "NY",
    "device": "Mobile", "os": "iOS", "browser": "Safari",
    "language": "en-US", "referer": "instagram.com"
  }
}

Verify authenticity by recomputing HMAC-SHA256(secret, rawBody) and comparing it (constant-time) to X-Webhook-Signature.

Best effort, at most once — no retries. This is a single fire-and-forget attempt with a 5-second timeout, not a durable queue. If your endpoint is down or slow, that scan's webhook delivery is simply lost (the scan itself is still recorded in analytics regardless). A webhook is automatically disabled after 20 consecutive failed deliveries; re-create it once your endpoint is healthy again.

Manage webhooks with GET /api/v1/webhooks (list) and DELETE /api/v1/webhooks/{id} (remove).

10. Styling options

These are top-level fields on the POST /api/v1/qr/static request body (siblings of type/data), not nested under a style object. All are optional.

FieldTypeNotes
fghex colorQR module color, e.g. #000000 (default).
bghex colorBackground color, e.g. #ffffff (default).
sizeinteger (px)Output width/height in pixels. 64–4096, default 512.
marginintegerQuiet-zone margin in modules. 0–40, default 4.
errorCorrectionLevelL | M | Q | HHigher levels tolerate more damage/logos but increase density. Default M.
formatsvg | png | pdfSelects the format used to build the response's download_url. Default svg. Any stored code can also be re-downloaded later in a different format by changing the format query parameter on the download URL.

Advanced styling: style

An optional top-level style object on POST /api/v1/qr/static selects a server-rendered subset of the web generator's design panel — rounded/dot module shapes, dot-style eyes, and linear/radial gradient fills. This works without a browser because it's built directly from the QR module matrix (the raw grid of dark/light squares) as hand-generated SVG shapes, rather than drawn to a <canvas>.

{
  "type": "url",
  "data": { "url": "https://example.com" },
  "style": {
    "dotShape": "rounded",
    "eyeShape": "dot",
    "gradient": {
      "type": "linear",
      "rotation": 45,
      "colors": [[0, "#F59E0B"], [1, "#EF4444"]]
    }
  }
}
FieldTypeNotes
style.dotShapesquare | dots | roundedShape used for each dark data module (outside the three corner eyes). Default square (identical to plain, unstyled output).
style.eyeShapesquare | dotShape of the three corner finder patterns ("eyes"): sharp-cornered frame+pupil, or fully circular ring+pupil. Default square.
style.gradientobjectOptional. type: linear (default) or radial. rotation: degrees, linear only, default 0. colors: array of at least 2 [offset, hexColor] pairs, offset 0–1. Overrides fg when present.

PNG and PDF downloads (GET /api/v1/qr/{id}/download) of a styled code are rasterized from that same styled SVG, so styling is preserved in every format.

Not available via the API: qr-code-styling's classy, classy-rounded, and extra-rounded module styles, which connect adjacent dark modules into a single continuous blob outline. That requires neighbor-aware path-merging geometry that this matrix-to-SVG renderer does not implement — only per-module shapes (square/dots/rounded) are supported. Center logo/image embedding is also not available server-side (the API has no image-fetching/compositing step). We're not claiming these because they genuinely aren't implemented — if that changes, this section will be updated.

11. Rate limits

  • Business/Founder plan: 10,000 API requests / month (every authenticated call counts, including reads).
  • 250 QR codes or barcodes / day combined, via POST /api/v1/qr/static and POST /api/v1/barcode (they share the same daily counter).
  • 50 bulk jobs / month via POST /api/v1/qr/bulk, each capped at ≤ 1,000 rows per call.
  • A short-window burst limit also applies per API key (up to 120 requests/minute) as an abuse backstop; sustained bursts return 429 with a Retry-After header.

12. Errors

All errors share a consistent JSON shape:

{
  "error": {
    "code": "validation_error",
    "message": "url must be a valid http(s) URL."
  }
}
CodeHTTP statusMeaning
missing_api_key401No API key supplied in the Authorization or X-API-Key header.
invalid_api_key401Key is malformed, revoked, or unknown.
plan_required403Account isn't on the Business or Founder plan.
forbidden403The QR code/webhook id exists but isn't owned by this API key, or (for POST /api/v1/barcode) the requested symbology isn't included in your plan.
not_found404QR code, webhook, or code_id doesn't exist.
validation_error400Request body or query string is missing/invalid fields for the given type/symbology (includes unsupported format values and bwip-js format rejections).
rate_limit_exceeded429Monthly request cap, daily QR/barcode-generation cap, monthly bulk-job cap, or the per-key burst limit was exceeded. Check the Retry-After header.

13. Examples

Full runnable examples (cURL, Node.js, Python) live in the repo's examples/ folder — see examples/README.md for setup instructions. Includes single-create scripts in examples/curl/, examples/node/, and examples/python/, plus bulk-generation examples in the Node and Python folders.

14. Privacy

Important: The Static QR API is server-side. When you call it, the content you're encoding (a URL, WiFi password, vCard details, etc.) is sent to and processed by QR Code Harbor's servers to generate the code. This is different from the free in-browser generator on the homepage, which runs entirely client-side and never sends your QR content anywhere.

Scan analytics are only collected for dynamic QR codes, which redirect through qrcodeharbor.com by design so we can log the scan event.

Do not put secrets, passwords, or sensitive personal information directly into QR content sent via the API.