Qrop logoQrop

API documentation

Generate branded QR codes at scale over a simple REST API. All endpoints are versioned under /api/v1 and return JSON.

Authentication

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.

Error codes

Errors return a JSON body of the form { "error": "..." } with an appropriate HTTP status.

401Missing or invalid API key.
402Credits exhausted or no active subscription.
400Malformed request (e.g. batch outside 1–25 items).
422Invalid data type, format, resolution, logo, or colours with insufficient contrast to stay scannable.
403Tracked codes and webhooks: you have reached your limit of active codes or of endpoints.
404A tracked code, campaign or webhook that does not exist or is not yours.
409Editing a tracked code that has already expired.
410Renewing a tracked code that has expired or is paused. Create a new one instead.
429Per-second burst or per-minute sustained rate limit exceeded. See the Retry-After header for when to retry.
Rate limits

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.

PlanReq / secReq / minMax resolution
Starter51002 048px
Pro154002 048px
Growth251 0002 048px
Enterprise1005 0004 096px
post/api/v1/generate

Generate a single QR code. Consumes 1 credit.

FieldTypeDescription
datastringrequiredContent to encode (URL, text, vCard string, etc.).
typestringrequiredOne of: url, text, vcard, wifi, email, sms, phone, geo.
formatstringrequiredpng, svg, or pdf.
resolutionnumberrequiredSquare size in px, 1–4096. Plan caps: 2048px Starter, Pro and Growth · 4096px Enterprise. Returns 422 if exceeded.
foregroundColorstringoptionalHex colour, default #000000.
backgroundColorstringoptionalHex colour, default #FFFFFF.
finderColorstringoptionalHex colour for the finder (corner) patterns.
logoUrlstringoptionalFetched server-side, max 2MB, PNG/JPEG/WebP only. Forces error correction to H.
logoDataUrlstringoptionalInline 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.
errorCorrectionstringoptionalL, M, Q, or H. Auto-upgraded to H when a logo is present.
moduleShapestringoptionalsquare (default), dots, or rounded: shape of the data modules.
finderShapestringoptionalsquare (default), rounded, or circle: shape of the three finder (corner) patterns.
imageRoundnessnumberoptionalOuter 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
  }'
post/api/v1/generate/batch

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).

FieldTypeDescription
itemsarrayrequired1–25 items, each with data and type (see /api/v1/generate).
formatstringrequiredpng, svg, or pdf: applies to every item in the batch.
resolutionnumberrequiredSquare size in px, 1–4096: applies to every item. Same plan caps as /api/v1/generate.
logoUrlstringoptionalFetched once and applied to every item in the batch, max 2MB, PNG/JPEG/WebP only. Not supported with svg format.
logoDataUrlstringoptionalInline 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).

get/api/v1/usage

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"
post/api/v1/keys/rotate

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"
Tracked / dynamic QR codes

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/trackedCreate a tracked code. Body: destinationUrl, dataType and originalPayload, with optional durationDays, campaignId and labels.
GET /api/v1/trackedYour 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}/renewExtend an active code from its current expiry. Charged like creating it.
GET /api/v1/tracked/{id}/analyticsTotals and breakdowns by country, region, city, device, operating system, browser, language, hour (South African time), weekday and day.
GET /api/v1/tracked/{id}/eventsIndividual scans, newest first, 100 a page by default (500 at most), paged with a cursor. No IP address.
POST, GET /api/v1/campaignsCreate or list campaigns, which group codes at no cost.
GET /api/v1/campaigns/{id}/analyticsThe 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", ... }
Webhooks

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));
}
Common questions

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.

We use strictly necessary cookies to run Qrop, and (only with your consent) advertising cookies. See our privacy policy for details.