API documentation
Generate branded QR codes at scale over a simple REST API. All endpoints are versioned under /api/v1 and return JSON.
All requests require a Bearer token API key in the Authorization header. Create and manage keys in your dashboard under /dashboard/api-keys. Keys are shown once at creation, so store them securely.
Authorization: Bearer qrop_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are created in your dashboard. API access requires an active API plan; see the pricing page for available plans.
Errors return a JSON body of the form { "error": "..." } with an appropriate HTTP status.
| 401 | Missing or invalid API key. |
| 402 | Credits exhausted or no active subscription. |
| 400 | Malformed request (e.g. batch outside 1–25 items). |
| 422 | Invalid data type, format, resolution, logo, or colours with insufficient contrast to stay scannable. |
| 403 | Tracked codes and webhooks: you have reached your limit of active codes or of endpoints. |
| 404 | A tracked code, campaign or webhook that does not exist or is not yours. |
| 409 | Editing a tracked code that has already expired. |
| 410 | Renewing a tracked code that has expired or is paused. Create a new one instead. |
| 429 | Per-second burst or per-minute sustained rate limit exceeded. See the Retry-After header for when to retry. |
Each API key is limited independently per plan. Both windows are enforced in parallel, so a 429 fires if either is exceeded. Check the Retry-After header for the reset time.
| Plan | Req / sec | Req / min | Max resolution |
|---|---|---|---|
| Starter | 5 | 100 | 2 048px |
| Pro | 15 | 400 | 2 048px |
| Growth | 25 | 1 000 | 2 048px |
| Enterprise | 100 | 5 000 | 4 096px |
Generate a single QR code. Consumes 1 credit.
| Field | Type | Description | |
|---|---|---|---|
| data | string | required | Content to encode (URL, text, vCard string, etc.). |
| type | string | required | One of: url, text, vcard, wifi, email, sms, phone, geo. |
| format | string | required | png, svg, or pdf. |
| resolution | number | required | Square size in px, 1–4096. Plan caps: 2048px Starter, Pro and Growth · 4096px Enterprise. Returns 422 if exceeded. |
| foregroundColor | string | optional | Hex colour, default #000000. |
| backgroundColor | string | optional | Hex colour, default #FFFFFF. |
| finderColor | string | optional | Hex colour for the finder (corner) patterns. |
| logoUrl | string | optional | Fetched server-side, max 2MB, PNG/JPEG/WebP only. Forces error correction to H. |
| logoDataUrl | string | optional | Inline base64 image (data:image/png;base64,..., jpeg, or webp), max 2MB, PNG/JPEG/WebP only. Use this to upload a logo directly instead of hosting it at a fetchable URL. Takes precedence over logoUrl if both are sent. |
| errorCorrection | string | optional | L, M, Q, or H. Auto-upgraded to H when a logo is present. |
| moduleShape | string | optional | square (default), dots, or rounded: shape of the data modules. |
| finderShape | string | optional | square (default), rounded, or circle: shape of the three finder (corner) patterns. |
| imageRoundness | number | optional | Outer corner radius of the whole QR image as a fraction of its resolution (0–0.5, default 0). |
Request
{
"data": "https://example.com",
"type": "url",
"format": "png",
"resolution": 1024,
"foregroundColor": "#000000",
"backgroundColor": "#FFFFFF"
}Response 200
{
"id": "gen_a1b2c3",
"image": "data:image/png;base64,iVBORw0KG...",
"format": "png",
"resolution": 1024,
"creditsConsumed": 1
}Example request
curl -X POST https://www.qrop.co.za/api/v1/generate \ -H "Authorization: Bearer qrop_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "data": "https://example.com", "type": "url", "format": "png", "resolution": 1024 }'
Generate up to 25 QR codes in one request. Each item consumes 1 credit; credits are reserved for the full batch up front (no partial processing).
| Field | Type | Description | |
|---|---|---|---|
| items | array | required | 1–25 items, each with data and type (see /api/v1/generate). |
| format | string | required | png, svg, or pdf: applies to every item in the batch. |
| resolution | number | required | Square size in px, 1–4096: applies to every item. Same plan caps as /api/v1/generate. |
| logoUrl | string | optional | Fetched once and applied to every item in the batch, max 2MB, PNG/JPEG/WebP only. Not supported with svg format. |
| logoDataUrl | string | optional | Inline base64 image, same logo applied to every item. Takes precedence over logoUrl if both are sent. |
The batch logo is per-request, not per-item. Use separate batch calls (or /api/v1/generate) if items need different logos.
Request
{
"items": [
{ "data": "https://example.com/1", "type": "url" },
{ "data": "https://example.com/2", "type": "url" }
],
"format": "png",
"resolution": 512,
"logoUrl": "https://example.com/company-logo.png"
}Response 200
{
"results": [
{ "id": "gen_x1_0", "image": "data:image/png;base64,...", "success": true },
{ "id": "gen_x2_1", "image": "data:image/png;base64,...", "success": true }
],
"creditsConsumed": 2,
"failedCount": 0
}Example request
curl -X POST https://www.qrop.co.za/api/v1/generate/batch \ -H "Authorization: Bearer qrop_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "data": "https://example.com/1", "type": "url" }, { "data": "https://example.com/2", "type": "url" } ], "format": "png", "resolution": 512 }'
A batch with 0 or more than 25 items returns 400.
Generating large volumes? Items in a batch are rendered one at a time server-side, so a bigger batch size wouldn't generate any faster; it would just mean a slower call and a larger response body. For volumes beyond one batch, issue multiple 25-item batch calls in parallel from your client instead, staying within your plan's rate limit (see /api/v1/usage and the Rate limits section above).
Return current period usage, remaining allocation, carried credits from previous periods, and your rate limits.
Response 200
{
"plan": "growth",
"currentPeriodStart": "2026-06-01T00:00:00Z",
"currentPeriodEnd": "2026-07-01T00:00:00Z",
"includedCredits": 50000,
"includedCreditsUsed": 12450,
"carriedCredits": 8200,
"topUpCredits": 0,
"rateLimit": { "requestsPerSecond": 25, "requestsPerMinute": 1000 }
}Example request
curl https://www.qrop.co.za/api/v1/usage \ -H "Authorization: Bearer qrop_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Rotate the calling API key. A new key is returned (shown once); the old key keeps working for a 24-hour grace period before it stops.
Response 200
{
"newKey": "qrop_live_yyyyyyyyyyyyyyyyyyyy",
"oldKeyValidUntil": "2026-06-25T10:00:00Z"
}Example request
curl -X POST https://www.qrop.co.za/api/v1/keys/rotate \ -H "Authorization: Bearer qrop_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Point a QR code at a Qrop-owned redirect URL instead of the destination directly, so scans get logged and the destination stays editable after printing. A tracked code costs 1 credit for every 30 days it stays live, and campaigns that group codes are free. Tracked codes and campaigns need purchased credits or a plan, so free accounts cannot create them. Each account holds a limited number of active codes and has a monthly scan allowance, which API plans raise. Limits per plan are on the pricing page, and the tracking guide explains how it works.
Create a tracked code, then encode the redirectUrl it returns with POST /api/v1/generate. The durations you can choose are 1, 3, 7, 14, 30, 60, 90, 120 days, and labels (up to five name and value pairs) let you split a campaign report by placement.
| POST /api/v1/tracked | Create a tracked code. Body: destinationUrl, dataType and originalPayload, with optional durationDays, campaignId and labels. |
| GET /api/v1/tracked | Your codes, newest first, up to 100. Add ?campaignId= to filter. |
| GET, PATCH, DELETE /api/v1/tracked/{id} | Read a code, edit its destinationUrl, labels, campaignId or isActive, or delete it along with its scans. |
| PATCH /api/v1/tracked/{id}/renew | Extend an active code from its current expiry. Charged like creating it. |
| GET /api/v1/tracked/{id}/analytics | Totals and breakdowns by country, region, city, device, operating system, browser, language, hour (South African time), weekday and day. |
| GET /api/v1/tracked/{id}/events | Individual scans, newest first, 100 a page by default (500 at most), paged with a cursor. No IP address. |
| POST, GET /api/v1/campaigns | Create or list campaigns, which group codes at no cost. |
| GET /api/v1/campaigns/{id}/analytics | The same breakdowns across a campaign, plus scans by label and a total for each code. |
curl -X POST https://www.qrop.co.za/api/v1/tracked \ -H "Authorization: Bearer qrop_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "destinationUrl": "https://example.com/spring-sale", "dataType": "url", "originalPayload": "https://example.com/spring-sale", "durationDays": 30, "labels": { "location": "Store 4" } }' # 200 response # { "id": "aB3xY9pQ", "redirectUrl": "https://www.qrop.co.za/r/aB3xY9pQ", # "expiresAt": "2026-11-19T00:00:00Z", ... }
Have Qrop post tracked-code events to your own system. Add an endpoint under Settings in the dashboard or with POST /api/v1/webhooks. Webhooks need purchased credits or a plan, and each account can have up to 5 endpoints. The endpoint must use https, and Qrop will not call private or local addresses.
Events: scan.logged, scan_limit.reached, code.expiring (seven days before a code expires) and code.expired. A request carries one or more events as JSON, delivered within about a minute. Answer with any 2xx within 5 seconds. Anything else, including a redirect, counts as a failure and is retried after 1 minute, 5 minutes, then every 15 minutes, for about an hour in all. After that Qrop stops retrying that batch. An endpoint that fails for 3 days is switched off and you are emailed. Delivery is at least once, so use the event id to ignore repeats. To fetch scans you missed, use GET /api/v1/tracked/{id}/events.
Each request has a Qrop-Signature header, t=<unix seconds>,v1=<hex>. The hex is an HMAC-SHA256 of the timestamp, a dot and the raw request body, using your endpoint’s secret (shown once when you create it). Check it, and reject a timestamp more than five minutes old.
If your server was down, use Resend missed scans on the endpoint in Settings to send it the scans from the last 24 hours or 7 days. They are marked replayed and arrive within a couple of minutes.
If your server sits behind a gateway that needs its own token, add up to 5 custom headers to an endpoint, such as Authorization: Bearer your-token. Qrop sends them with every request next to the signature. They are stored encrypted, only the names are shown after you save, and you replace the set to change it. Names Qrop sets itself (such as Host, Content-Type and anything starting Qrop-) are not allowed.
import { createHmac, timingSafeEqual } from "crypto"; function isFromQrop(rawBody, header, secret) { const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("="))); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest("hex"); return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); }
Why am I getting a 402 error?
A 402 means your API key has no active plan, or your plan credits (carried and included) and any top-up balance are all used up for the current period. Plans have no overage, so nothing is billed beyond the allowance. Check /api/v1/usage for your current standing, then buy an API top-up pack or upgrade your plan from the billing dashboard.
How do I raise my rate limit?
Rate limits are tied to your API plan, see the Rate limits table above for the per-second and per-minute caps on each tier. Upgrading to a higher plan raises both limits immediately; there is no separate rate limit purchase.
Why did my request return a 422 instead of generating a QR code?
422 means the request was understood but rejected before any credit was consumed, usually an invalid data type or format, a resolution above your plan's cap, or a foreground/background colour combination with too little contrast to scan reliably. The error message states the specific reason.
Can I use the same API key for the web dashboard and the API?
API keys are for the REST API only, they authenticate requests to /api/v1/*. The web dashboard at qrop.co.za uses your regular account login, not an API key.
What happens to in-flight requests when I rotate a key?
The old key keeps working for a 24-hour grace period after rotation, so requests already using it won't break immediately. Update your integration to the new key within that window before the old one stops authenticating.