Resources
Rate limits, pagination and versioning
The rules that apply to every endpoint, in one place.
Rate limits
Each API key may send a fixed number of requests per minute. The number comes from the account’s plan, and every key gets its own allowance; keys do not share one.
| Plan | Line | REST API | Requests per minute, per key |
|---|---|---|---|
| Free | QR plans | Not included | 30 |
| Launch | QR plans | Not included | 60 |
| Growth | QR plans | Not included | 60 |
| Pro | QR plans | Included | 120 |
| Free Card | Card plans | Not included | 30 |
| Team | Card plans | Not included | 60 |
| Business | Card plans | Included | 120 |
Only the plans marked “Included” can call the REST API, so in practice a REST key has 120 requests a minute. The other rows apply to the same keys used with AI assistants. GET /me reports the figure for the key you are using, in rateLimit.requestsPerMinute.
Headers
| Header | Meaning |
|---|---|
RateLimit-Limit | The key’s allowance per window. |
RateLimit-Remaining | Requests left in the current window. |
RateLimit-Reset | Seconds until the window resets. |
Going over
The request is refused with 429. Wait RateLimit-Reset seconds and send it again; nothing was done, so repeating it is safe.
{
"success": false,
"message": "API rate limit exceeded. Retry after the window resets — see the RateLimit-Reset header."
}There are no separate daily or monthly request quotas on the API. Plans do limit how many cards and QR codes an account may hold, which shows up as 403 limit_exhausted_cards or 403 limit_exhausted_products, not as a 429.
Pagination
List endpoints return one page at a time, newest first, in this envelope:
{
"object": "list",
"data": [
"…"
],
"pagination": {
"total": 240,
"limit": 25,
"offset": 0,
"hasMore": true
}
}| Name | Where | Meaning |
|---|---|---|
limit | Query | Items per page, 1 to 100. Default 25 (20 for /campaigns). |
offset | Query | How many items to skip. Default 0. |
pagination.total | Response | How many items match in all. |
pagination.hasMore | Response | true when there is another page after this one. |
To read everything, raise offset by limit until hasMore is false:
// Read every card, one page at a time.
const cards = [];
for (let offset = 0; ; offset += 100) {
const res = await fetch(`https://api.qrbold.com/api/public/v1/cards?limit=100&offset=${offset}`, {
headers: { Authorization: `Bearer ${process.env.QRBOLD_API_KEY}` },
});
if (!res.ok) throw new Error(`QRBold API ${res.status}`);
const page = await res.json();
cards.push(...page.data);
if (!page.pagination.hasMore) break;
}
console.log(`${cards.length} cards`);- A
limitover 100 answers422; it is not quietly reduced. - There are no cursors. Paging is by offset only.
- Because lists are newest first, a record created while you are paging shifts later pages by one. For a full export, de-duplicate by
id. - Archived items are left out of lists unless you ask for
status=archived.
The first page of cards:
curl "https://api.qrbold.com/api/public/v1/cards?limit=25&offset=0" \
-H "Authorization: Bearer $QRBOLD_API_KEY"Idempotency
An idempotency key lets you repeat a write safely. Send any unique string in the Idempotency-Key header of a POST, PATCH or DELETE:
Idempotency-Key: 6f4e9d3c-7e3c-4b91-a8ab-123456789abc| Situation | Result |
|---|---|
| First request with a key | Runs normally. The response is remembered for 24 hours. |
| Same key, same request, within 24 hours | The original response is returned again, with the header Idempotent-Replay: true. Nothing runs twice. |
| Same key, different body, path or query | 409 idempotency_key_reused |
| Same key while the first request is still running | 409 idempotency_key_in_flight. Wait a few seconds and repeat. |
| The first request failed with a 5xx | Not remembered. The retry runs for real. |
| A key longer than 255 characters | 400 invalid_idempotency_key |
| No key at all | The request runs every time it is sent. |
- The header is optional, and recommended on every write.
- Keys belong to the API key that sent them. Two API keys can use the same value without clashing.
- A UUID is the usual choice.
Versioning and compatibility
The version is part of the path: /api/public/v1. v1 is the current and only version. There is no version header and no dated versions.
What can change inside v1
| May be added at any time | So your code should |
|---|---|
| New endpoints | Not need to do anything. |
| New fields in a response object | Ignore fields it does not recognise. |
| New optional request fields and query parameters | Not need to do anything. |
| New values for error.code | Fall back to the HTTP status for a code it does not know. |
| New values in sets such as link types or QR types | Not fail on an unknown value. |
| Changed wording of error.message | Never match on message text. |
Removing a field, renaming one, changing a type or making an optional field required would be a breaking change and belongs in a new version path.
QRBold does not currently publish an API changelog or a deprecation schedule. These pages were last checked against the API on 2026-10-09. Where the API’s behaviour is irregular today, it is documented as it is; see responses that break the pattern.
Formats
| Thing | Format |
|---|---|
| Request and response bodies | JSON, UTF-8. Send Content-Type: application/json. |
| Dates and times | ISO 8601 in UTC, e.g. 2026-10-09T09:30:00.000Z. |
| Analytics days | YYYY-MM-DD, in the time zone you asked for. |
| Ids | Opaque strings. Do not parse them or assume a length. |
| Empty values | null, not an absent key, for fields such as templateId and externalId. |
| Booleans in a query string | true or false. |
| Request size | Campaign bodies up to 1 MB. Keep other bodies well under that. |
| Transport | HTTPS only. |