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.
| Feature | Status |
|---|---|
| Static QR API — all 37 content types | live |
| Dynamic QR API | live |
| Scan analytics API | live |
| SVG export | live |
| PNG / PDF export (QR codes only) | live |
| Bulk generation (JSON array, up to 1,000 rows) | live |
| Bulk CSV file upload / ZIP download | not 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, gradients | live — 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 & path | Description | Auth |
|---|---|---|
POST /api/v1/qr/static | Create a static QR code (content baked in; cannot be edited later). Accepts format: svg, png, or pdf. | API key |
POST /api/v1/qr/dynamic | Create 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|pdf | Download 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}/analytics | Read scan analytics for a dynamic QR code (device, location, referrer, daily/hourly time series, recent scans). | API key |
POST /api/v1/qr/bulk | Create 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/barcode | Generate a barcode (SVG only). symbology must be one your plan allows — see Barcodes. | API key |
GET /api/v1/webhooks | List your scan-event webhooks. | API key |
POST /api/v1/webhooks | Register 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/keys | List your account's API keys (never returns the full secret). | Sign-in (Firebase) |
POST /api/keys | Create 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" } }
{
"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" } ]
}
}
{
"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" } }
deeplink
{ "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.
| Category | Symbologies |
|---|---|
| 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, not55555-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.
| Field | Type | Notes |
|---|---|---|
fg | hex color | QR module color, e.g. #000000 (default). |
bg | hex color | Background color, e.g. #ffffff (default). |
size | integer (px) | Output width/height in pixels. 64–4096, default 512. |
margin | integer | Quiet-zone margin in modules. 0–40, default 4. |
errorCorrectionLevel | L | M | Q | H | Higher levels tolerate more damage/logos but increase density. Default M. |
format | svg | png | pdf | Selects 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"]]
}
}
}
| Field | Type | Notes |
|---|---|---|
style.dotShape | square | dots | rounded | Shape used for each dark data module (outside the three corner eyes). Default square (identical to plain, unstyled output). |
style.eyeShape | square | dot | Shape of the three corner finder patterns ("eyes"): sharp-cornered frame+pupil, or fully circular ring+pupil. Default square. |
style.gradient | object | Optional. 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/staticandPOST /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
429with aRetry-Afterheader.
12. Errors
All errors share a consistent JSON shape:
{
"error": {
"code": "validation_error",
"message": "url must be a valid http(s) URL."
}
}
| Code | HTTP status | Meaning |
|---|---|---|
missing_api_key | 401 | No API key supplied in the Authorization or X-API-Key header. |
invalid_api_key | 401 | Key is malformed, revoked, or unknown. |
plan_required | 403 | Account isn't on the Business or Founder plan. |
forbidden | 403 | The 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_found | 404 | QR code, webhook, or code_id doesn't exist. |
validation_error | 400 | Request body or query string is missing/invalid fields for the given type/symbology (includes unsupported format values and bwip-js format rejections). |
rate_limit_exceeded | 429 | Monthly 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.