0. The free endpoint — no key, no account
One GET request returns a QR code. There is nothing to sign up for and no key to pass. Use it in a script, a side project, or an <img> tag.
curl "https://qrcodeharbor.com/api/qr?data=https://example.com&size=512" -o qr.png
Or straight into a page — it is CORS-open and cached, so hotlinking is fine:
<img src="https://qrcodeharbor.com/api/qr?data=https://example.com&format=svg&shape=rounded" alt="QR code">
| Parameter | Default | Notes |
|---|---|---|
data | — | Required. The text or URL to encode, up to 1200 characters. |
format | png | png or svg. |
size | 512 | 64–2048 px. The paid API goes to 4096. |
fg / bg | #000000 / #ffffff | Hex colours. |
ecc | M | L, M, Q or H. |
margin | 4 | Quiet zone in modules, 0–16. Do not set it to 0 for print. |
shape | square | square, dots or rounded (SVG only). |
eye | square | square or dot (SVG only). |
Limits, stated plainly: 60 requests a minute per IP address, and static codes only. It creates nothing, stores nothing and reads no account — identical parameters always return an identical image, which is why it can be cached for a day and cost us almost nothing. If you need editable codes, scan analytics, bulk generation or webhooks, that is the Business API documented below, and it has real quotas behind a key.
If you build a wrapper or a library around this, we would genuinely like to hear about it — tell us and we will link to it.
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.
Built on this API today. Three apps from the same team call it in production, so the endpoints below are the ones we depend on ourselves:
- Letturia — dynamic codes for reading-event posters and tickets.
- Debuto — QR codes for creators' link-in-bio pages.
- Smart Money API — a trackable referral QR for each affiliate partner, with scan analytics in its dashboard.
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 (all 38 symbologies on Business) | 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.
Key permissions
When you create a key you choose what it can do. A full-access key can call every endpoint on this page. An app connection key is the one to give another app, such as Debuto: it can create, list, edit and pause your dynamic codes and read their scans, but it can't delete codes or manage webhooks. You can also tick individual permissions. Revoke any key on its own at any time; your other keys keep working.
| Permission | Description |
|---|---|
qr:generate | Static QR codes, bulk jobs, AI images, barcodes and downloads. |
qr:dynamic:create | Create dynamic QR codes. |
qr:dynamic:read | List dynamic QR codes and read one. |
qr:dynamic:update | Change a code's destination, pause it, set expiry, scheduled start or password. |
qr:dynamic:delete | Delete dynamic QR codes. |
qr:analytics:read | Read scan analytics. |
webhooks:manage | Register, read, replay and delete scan webhooks. |
Creating a key from the API: POST /api/keys with {"name": "Debuto", "scopes": "integration"}. scopes is "full" (the default), "integration", or a list of the permissions above. A call the key isn't allowed to make returns 403 insufficient_scope and doesn't count toward your monthly requests.
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, pdf or eps. | 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/dynamic?limit=50&cursor=&kind=qr|link | List the dynamic QR codes and short links on your account, newest first, with destination, label, scan count and pause/expiry/schedule/password state. Paginate with cursor (opaque; send back next_cursor while has_more is true). Rules are not included — fetch a single code for those. | 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|eps | Download the QR code image in any supported format. Works for both static codes and the auto-generated download for dynamic codes. eps is rebuilt from the raw module matrix (solid ink, square modules) for print shops. | 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: destination, paused (true/false), expires_at (unix-ms, ISO date or null), active_from (unix-ms, ISO date or null — scanners see a “not live yet” page before this instant, up to 5 years ahead) and password (4–72 chars, empty to remove). Send any subset. 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 |
DELETE /api/v1/qr/dynamic/{id} | Delete a dynamic QR code with its scan history and webhooks. The id is retired, never reused, so printed copies can never start pointing at somebody else's link. Needs the qr:dynamic:delete permission. To stop a code without deleting it, PATCH paused: true. | API key |
POST /api/v1/qr/bulk | Create up to 1,000 static QR codes in one call — either from a JSON array of items (each row can be a different type) or from a sequential range ({n} counter with zero-padding — tickets, asset tags, table numbers). Each item gets its own download_url supporting svg/png/pdf/eps. | API key |
POST /api/v1/qr/ai | Generate the QR code as an AI image and return a URL to it. Modes: photoreal (a real photograph with a genuine QR composited onto a surface in it — most reliable), artistic, standard. Metered by AI credits, not by the API request quota. See AI generation. | API key |
POST /api/v1/barcode | Generate a barcode (svg or png). 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 |
GET /api/v1/webhooks/{id} | Retrieve one webhook plus its last 20 delivery attempts (event id, attempt, status, error, timing). | API key |
DELETE /api/v1/webhooks/{id} | Delete a webhook and its delivery history. | API key |
POST /api/v1/webhooks/{id}/replay | Re-send the most recent scan event (or a named event_id) to the same URL, with the same signature and event id. | 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.
Limit: up to 20 active API keys per account. Creating a 21st returns 409 — revoke one first.
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: ..." } }
7b. AI generation
POST /api/v1/qr/ai generates an image containing your QR code and stores it, returning a public URL. It is the same engine as AI QR Art on the site, behind an API key.
Modes
| Mode | How it works | Use when |
|---|---|---|
photoreal | Generates an ordinary photograph, then composites a real QR matrix onto a surface in it. The code is genuine rather than model-generated. | Almost always. It looks like a photograph and it is the most reliable to scan. |
artistic | Weaves the scene into the code itself, then composites the true matrix over the result. | You want a stylised, graphic look rather than a photo. |
standard | QR-ControlNet model at a scannability-first conditioning scale. | Crisp, simple illustrated scenes. |
Scenes (photoreal only)
scene chooses what the code becomes part of. Each scene prompts for a blank, camera-facing surface — that is not cosmetic: a code seen at a steep angle cannot be scanned, so the prompt demands a straight-on view and the compositor keeps its own tilt minimal.
| Scene | Result |
|---|---|
card | A printed card standing in the scene (default). |
coffee | Dusted into the crema of a coffee, like cocoa through a stencil. Only the dark modules are inked, so the foam shows through. |
menu | Printed on a menu cover someone is holding. |
sign | Printed on a flat sign board. |
packaging | On a product label panel. |
screen | Displayed on a phone screen. |
Example
curl -X POST https://qrcodeharbor.com/api/v1/qr/ai \
-H "Authorization: Bearer $QRH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "photoreal",
"scene": "coffee",
"style": "photo",
"prompt": "a cup of coffee on a wooden table, close up",
"data": "https://example.com/menu"
}'
{
"id": "a3f9c1d7e2b4",
"url": "https://qrcodeharbor.com/media/ai/<uid>/a3f9c1d7e2b4.png",
"mode": "photoreal",
"scene": "coffee",
"scannable": true,
"decoded": "https://example.com/menu",
"credits_left": 41
}
Do not ask for a QR code in your prompt. Describe only the picture. A model-drawn QR cannot be scanned and fights the real matrix that gets composited over it.
Verification and billing. Every image is machine-decoded before it is returned. If it does not decode back to your data, you get 422 and no credit is charged. Credits are separate from your monthly API request quota; there is also a ceiling of 100 AI generations per day.
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 (10) | qrcode, microqrcode, datamatrix, pdf417, micropdf417, azteccode, maxicode, codablockf, dotcode, hanxin |
| Postal (6) | postnet, onecode, royalmail, auspost, japanpost, kix |
Plan gating
Business API keys can render every one of the 38 symbologies above — the same list the free web tool offers, including gs1-128, gs1datamatrix and the GS1 DataBar family (databaromni, databarexpanded, databarlimited). Founder keys get the Pro list of 11: ean13, ean8, upca, upce, code128, code39, itf14, datamatrix, pdf417, azteccode, isbn (grandfathered Founder accounts get the full Business list). Requesting a symbology outside your plan's list returns 403 forbidden.
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
}
Limit: up to 50 webhooks per account. Creating a 51st returns 400 validation_error — delete one first.
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>
X-Harbor-Event-Id: evt_4f9c2ab17d3e4c0b8a1e5d6f7c8b9a01
X-Harbor-Attempt: 1
{
"event": "qr.scan",
"event_id": "evt_4f9c2ab17d3e4c0b8a1e5d6f7c8b9a01",
"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.
Retries and event ids
At-least-once, with retries. A scan event is attempted immediately and then retried if it does not land:
- Up to 4 attempts — the first, then retries roughly 0.5s, 2s and 5s later. Each attempt has its own 5-second timeout.
- Retried on: no response at all (DNS, TLS, connection, timeout), HTTP
429, or any5xx. - Not retried on any other
4xx— a rejected request is a configuration problem, and repeating it only hammers your server. - Any
2xxcounts as delivered. All attempts for one event share a single ~15-second wall-clock budget and no new attempt is started once it is spent, so a very slow endpoint gets fewer than 4 attempts.
All of that happens in the seconds after the scan. There is no later redelivery hours afterwards — once the budget is spent the event is only recoverable through the replay endpoint below.
Dedupe on the event id. Every scan event gets one event_id, sent both in the X-Harbor-Event-Id header and inside the signed body. Retries and replays of that event reuse the same id and the same bytes, so treating a repeated id as already-processed is enough to make at-least-once delivery safe on your side. X-Harbor-Attempt is the 1-based attempt number within the current delivery.
Auto-disable. A webhook is disabled after 20 consecutive failed deliveries. A delivery that exhausted all of its retries counts as one failure, not one per attempt. Scans are still recorded in analytics regardless. Re-create the webhook once your endpoint is healthy again.
Delivery history
GET /api/v1/webhooks/{id} returns the webhook plus its last 20 attempts, newest first. Attempts older than 30 days are pruned opportunistically — the clean-up rides on a later delivery for the same webhook, so a webhook that stops firing keeps its last attempts until it fires again.
{
"id": "whk_8f2c1a9d1e6b4a2f",
"code_id": "AbC123x",
"url": "https://yourapp.com/hooks/qr-scan",
"status": "active",
"fail_count": 0,
"created_at": 1751380920000,
"last_fired_at": 1751467320000,
"last_status": 200,
"deliveries": [
{ "event_id": "evt_4f9c...9a01", "attempt": 2, "status": 200, "error": null, "duration_ms": 143, "at": 1751467320812 },
{ "event_id": "evt_4f9c...9a01", "attempt": 1, "status": 503, "error": null, "duration_ms": 87, "at": 1751467320180 },
{ "event_id": "evt_1b7d...44c2", "attempt": 1, "status": null, "error": "The operation was aborted", "duration_ms": 5001, "at": 1751380921004 }
]
}
status is the HTTP status your endpoint returned, or null when there was no response at all — in which case error carries the reason.
Replay
POST /api/v1/webhooks/{id}/replay re-sends an event you already received (or missed). With no body it replays the most recent replayable event — note that a replay is itself recorded, so after replaying an older event by id, that one becomes the most recent. Pass an event_id whenever you mean a specific event.
curl -X POST https://qrcodeharbor.com/api/v1/webhooks/whk_8f2c1a9d1e6b4a2f/replay \
-H "Authorization: Bearer qch_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "event_id": "evt_4f9c2ab17d3e4c0b8a1e5d6f7c8b9a01" }'
{
"id": "whk_8f2c1a9d1e6b4a2f",
"event_id": "evt_4f9c2ab17d3e4c0b8a1e5d6f7c8b9a01",
"delivered": true,
"attempts": 1,
"status": 200,
"error": null
}
- A replay POSTs the exact original bytes, with the same signature and the same
X-Harbor-Event-Id— so a receiver that already handled the event can drop it as a duplicate. - It follows the same retry ladder, and its attempts appear in the delivery history.
- A replay's outcome never moves
fail_countor auto-disables the webhook: only live scan deliveries count toward that. - Only events still inside the 30-day history window can be replayed. Anything else returns
404 not_found.
Manage webhooks with GET /api/v1/webhooks (list), GET /api/v1/webhooks/{id} (detail plus delivery history) and DELETE /api/v1/webhooks/{id} (remove — this also deletes the webhook's delivery history).
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 | eps | 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. eps is a print-vector rendering of the raw module matrix — solid ink and background colours, square modules, quiet zone — so style shapes and gradients are not reproduced in EPS. |
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 those formats. EPS is the exception by design (see the format row above).
Sequential ranges: range
POST /api/v1/qr/bulk accepts a range object instead of items when you need numbered codes — event tickets, asset tags, table numbers, serialised labels. {n} in template (and in the optional name_template) is replaced by a counter from start to end inclusive, stepping by step (default 1) and zero-padded to pad digits. type is url (default) or text. The row count is validated against your plan's per-job limit before anything is generated.
POST /api/v1/qr/bulk
{
"format": "eps",
"range": {
"template": "https://tickets.example.com/t/{n}",
"start": 1,
"end": 500,
"pad": 4,
"name_template": "ticket-{n}"
}
}
→ 201 { "count": 500, "items": [ { "name": "ticket-0001", "id": "qr_…", "download_url": "…/download?format=eps" }, … ] }
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 / day and 250 barcodes / day — separate counters, one for
POST /api/v1/qr/staticand one forPOST /api/v1/barcode. - 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. |
insufficient_scope | 403 | The API key doesn't have the permission this endpoint needs. Create a key with that permission, or use a full-access key. |
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.