Every request goes to https://api.keptqr.com/v1 with an API key in the Authorization header. Keys, test keys, limits, pagination and retries are explained in Getting started. The same document is served at https://api.keptqr.com/v1/openapi.json, for code generators and API tools.
GET/v1/me
The key, its workspace, plan, limits and this month's usage
Who you are calling as. A good first call: any key can make it, and it shows the key's scope, the workspace's plan and limits, and this month's usage.
- Needs a read key
- getMe
Responses
200The key context.
Body: KeyContext
Errors
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/me \
-H "Authorization: Bearer $KEPTQR_API_KEY"Codes
List, read, edit and delete codes; create dynamic codes, one or up to 100 at a time; read a dynamic code's destination history; download a code's QR image.
GET/v1/codes
List codes
The workspace's codes, newest first, a page at a time. Filters combine; deleted codes appear only with status=deleted.
- Needs a read key
- listCodes
Parameters
limitintegerQuery
Page size, 1–100.
1 to 100default
25cursorstringQuery
next_cursor from the previous page. Only valid with the same filters (limit may change).
folder_idstringQuery
Only codes in this folder; none = codes in no folder.
kindstringQuery
Only dynamic or only static codes. Default: both.
one of
dynamic,staticstatusstringQuery
Only codes with this status. Default: active and disabled codes.
one of
active,disabled,deletedqstringQuery
Case-insensitive substring of the name or slug. * is ignored.
at most 200 characters
Responses
200A page of codes, newest first.
Body: CodeList
Errors
- 400
- A query parameter or the cursor is not valid (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/codes \
-H "Authorization: Bearer $KEPTQR_API_KEY"POST/v1/codes
Create a dynamic code
Creates a dynamic code through the dashboard's own checks. Send an Idempotency-Key to retry safely: a retry with the same key and body within 24 hours replays the first response with Idempotent-Replayed: true. A replayed body is the original (an error keeps the first request's request_id); the X-Request-Id header is new. A test key (kq_test_) runs every check and saves nothing: the response has test: true.
- Needs a read-and-write key
- createCode
Parameters
Idempotency-KeystringHeader
1 to 255 printable ASCII characters. A retry with the same key and body within 24 hours returns the first response (Idempotent-Replayed: true); the same key with another body is 400 idempotency_key_reused; while the first is still running, 409 idempotency_in_progress.
Request body · application/json
CreateCode: A new dynamic code: POST /v1/codes's body.
brand_kit_idstring (uuid)nullable
Start from this brand kit (GET /v1/brand-kits), logo included. Omitted or null: no kit.
designDesignInput
Design fields that override the brand kit's design, or the default design without a kit.
destinationstringrequired
Where the code redirects: https, http, mailto or tel. A bare host gets https://. Checked against Google Web Risk before it's saved.
not emptye.g.
https://example.com/springfolder_idstring (uuid)nullable
The folder to put the code in (GET /v1/folders). Omitted or null: No folder.
mediumstring
qr: a code to print or show as a QR image. link: a short link to share as text. Both redirect the same way.
one of
qr,linkdefaultqrnamestringrequired
The code's name in the dashboard. Trimmed; 1 to 120 characters.
1 to 120 characterse.g.
Spring flyer
Other fields are refused.
Responses
201The code, as GET /v1/codes/{id} shows it, plus warnings.
Body: CodeWriteResult
- Idempotent-Replayed
- true when this is the stored response to an earlier request with the same Idempotency-Key. The body is the original (a replayed error keeps the first request's request_id); the X-Request-Id header is new.
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 400
- The body isn't a JSON object (invalid_json), a field isn't valid (invalid_parameter), the destination isn't allowed (invalid_destination), or the Idempotency-Key is invalid or was used with another request.
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 404
- No such folder or brand kit in this workspace (folder_not_found, brand_kit_not_found).
- 409
- The plan's dynamic-code limit is reached (plan_limit_reached), the brand kit can't be used on this plan (brand_kit_unavailable), or a request with this Idempotency-Key is still running (idempotency_in_progress).
- 422
- Web Risk flagged the destination (unsafe_destination/destination_flagged).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- The destination couldn't be checked (destination_unverifiable), a dependency is unavailable or Redis unavailable for writes (service_unavailable), or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl -X POST https://api.keptqr.com/v1/codes \
-H "Authorization: Bearer $KEPTQR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "https://example.com/spring",
"name": "Spring flyer"
}'POST/v1/codes/batch
Create up to 100 dynamic codes
Creates each item as POST /v1/codes would, in order, and answers 207 with each item's result: status 201 and the code, or the item's error (with this request's request_id). When the plan's limit is reached, the earliest items are created and the rest answer 409 plan_limit_reached. Items not started within 55 seconds answer 503 timeout, with nothing saved for them. Needs a plan with bulk creation. Each item costs one write token, all at once; a batch the write limit refuses costs none. Send an Idempotency-Key to retry safely: a replay answers the same results without spending write tokens. To finish a partly completed batch, resend only the failed items, with a new Idempotency-Key. A test key runs every check and saves nothing.
- Needs a read-and-write key
- Needs a plan that includes bulk creation
- createCodesBatch
Parameters
Idempotency-KeystringHeader
1 to 255 printable ASCII characters. A retry with the same key and body within 24 hours returns the first response (Idempotent-Replayed: true); the same key with another body is 400 idempotency_key_reused; while the first is still running, 409 idempotency_in_progress.
Request body · application/json
CreateCodeBatch: Up to 100 new dynamic codes: POST /v1/codes/batch's body.
itemsarray of CreateCoderequired
1 to 100 items, each a POST /v1/codes body. Each item is validated on its own.
1 to 100 items
Other fields are refused.
Responses
207Each item's result, in order.
Body: BatchResult
- Idempotent-Replayed
- true when this is the stored response to an earlier request with the same Idempotency-Key. The body is the original (a replayed error keeps the first request's request_id); the X-Request-Id header is new.
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 400
- The body isn't a JSON object (invalid_json), items isn't an array of 1 to 100 (invalid_parameter), more than 100 items (batch_too_large), or the Idempotency-Key is invalid or was used with another request.
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access or no bulk creation on the plan (bulk_creation_required), a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 409
- A request with this Idempotency-Key is still running (idempotency_in_progress).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable, the request timed out, or Redis unavailable for writes (service_unavailable). Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl -X POST https://api.keptqr.com/v1/codes/batch \
-H "Authorization: Bearer $KEPTQR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"destination": "https://example.com/spring",
"name": "Spring flyer"
}
]
}'GET/v1/codes/{id}
Get a code
One code by id, whatever its status, deleted dynamic codes included. A deleted static code is gone: 404.
- Needs a read key
- getCode
Parameters
idstring (uuid)Pathrequired
The code's id.
Responses
200The code.
Body: Code
Errors
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 404
- No such code in this workspace (not_found/code_not_found).
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/codes/{id} \
-H "Authorization: Bearer $KEPTQR_API_KEY"PATCH/v1/codes/{id}
Edit a code
Changes any of destination (dynamic codes only), name, folder_id and design, in that order; the first refusal stops. PATCH is not transactional: if a field is refused, the fields in applied were already saved. Retry-safe: resending the same body sets the same values, and an unchanged destination is a no-op. Takes no Idempotency-Key. One write token whatever the number of fields.
- Needs a read-and-write key
- updateCode
Parameters
idstring (uuid)Pathrequired
The code's id.
Request body · application/json
PatchCode: The fields to change: at least one. They are applied in the order destination, name, folder_id, design.
designDesignInput
Merged into the stored design; the logo is kept.
destinationstring
Dynamic codes only. The same rules as on create; an unchanged destination is a no-op.
not empty
folder_idstring (uuid)nullable
null moves the code to No folder.
namestring
The code's name in the dashboard. Trimmed; 1 to 120 characters.
1 to 120 characters
Other fields are refused.
Responses
200The code after the edit, plus warnings.
Body: CodeWriteResult
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 400
- The body isn't valid (invalid_json, invalid_parameter, missing_parameter) or the destination isn't allowed (invalid_destination).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 404
- No such code or folder in this workspace (code_not_found, folder_not_found).
- 409
- The code is deleted (code_deleted), disabled (code_disabled) or static (not_dynamic), or destination edits are frozen while the workspace is over its plan (editing_frozen).
- 422
- Web Risk flagged the destination (unsafe_destination/destination_flagged).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- The destination couldn't be checked (destination_unverifiable), a dependency is unavailable or Redis unavailable for writes (service_unavailable), or the request timed out. Retry after Retry-After.
Error body: PatchError. Codes and meanings are in the error codes table.
curl -X PATCH https://api.keptqr.com/v1/codes/{id} \
-H "Authorization: Bearer $KEPTQR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "<destination>"
}'DELETE/v1/codes/{id}
Delete a code
A dynamic code is soft-deleted: it stops redirecting and its slug is never reused; deleting it again answers 200. A static code is removed, so a second DELETE is 404. Never blocked by the edit freeze.
- Needs a read-and-write key
- deleteCode
Parameters
idstring (uuid)Pathrequired
The code's id.
Responses
200Deleted.
Body: DeletedCode
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 404
- No such code in this workspace (not_found/code_not_found).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable, the request timed out, or Redis unavailable for writes (service_unavailable). Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl -X DELETE https://api.keptqr.com/v1/codes/{id} \
-H "Authorization: Bearer $KEPTQR_API_KEY"GET/v1/codes/{id}/destination-history
A dynamic code's destination history
Every change of the code's destination, newest first, a page at a time, with who made it.
- Needs a read key
- listDestinationHistory
Parameters
idstring (uuid)Pathrequired
The code's id.
limitintegerQuery
Page size, 1–100.
1 to 100default
25cursorstringQuery
next_cursor from the previous page. Only valid with the same filters (limit may change).
Responses
200Destination changes, newest first.
Body: DestinationHistory
Errors
- 400
- A query parameter or the cursor is not valid (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 404
- No such code in this workspace (not_found/code_not_found).
- 409
- A static code has no destination history (conflict/not_dynamic).
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/codes/{id}/destination-history \
-H "Authorization: Bearer $KEPTQR_API_KEY"GET/v1/codes/{id}/image
Get a code's QR image
The code's QR image, drawn from its saved design the same way as the dashboard's download. A dynamic code encodes its short URL; a static code its content. SVG is 1024 × 1024; PNG takes size. Every image is decoded and checked against the code's content before it's sent: a design that doesn't scan is refused with 422 image_unscannable. A stored logo that can't be used (not a readable image) is left out, and the image is still sent with 200. The ETag covers everything the image is drawn from; send it back as If-None-Match to get 304 while the image hasn't changed.
- Needs a read key
- getCodeImage
Parameters
idstring (uuid)Pathrequired
The code's id.
formatstringQuery
The image format: svg (the default) or png.
one of
svg,pngdefaultsvgsizeintegerQuery
PNG only: width and height in pixels. Default 1024. An SVG is always 1024.
one of
512,1024,2048,4096If-None-MatchstringHeader
An ETag from an earlier response (or a list, or *): 304 while it still matches.
Responses
200The image.
Body: image/png or image/svg+xml
- Cache-Control
- private, max-age=300
- Content-Disposition
- inline; filename="qr-{slug}.{ext}" for a dynamic code, "qr-{type}.{ext}" for a static one.
- Content-Security-Policy
- default-src 'none'; img-src data:; sandbox
- ETag
- A strong tag over everything the image is drawn from.
- X-Content-Type-Options
- nosniff
304If-None-Match matched the current ETag: the image hasn't changed. No body.
- Cache-Control
- private, max-age=300
- Content-Security-Policy
- default-src 'none'; img-src data:; sandbox
- ETag
- The current tag.
- X-Content-Type-Options
- nosniff
Errors
- 400
- format or size isn't valid, or size was sent with format=svg (invalid_parameter).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 404
- No such code in this workspace (not_found/code_not_found).
- 409
- The code is deleted or disabled (conflict/code_deleted, code_disabled).
- 422
- The image of this code's design doesn't scan, or its content doesn't fit a QR code (invalid_request/image_unscannable).
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable, the image queue is full (service_unavailable, Retry-After 5), or the request or the render timed out (timeout). Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/codes/{id}/image \
-H "Authorization: Bearer $KEPTQR_API_KEY"GET/v1/folders
List folders
The workspace's folders, newest first, a page at a time.
- Needs a read key
- listFolders
Parameters
limitintegerQuery
Page size, 1–100.
1 to 100default
25cursorstringQuery
next_cursor from the previous page. Only valid with the same filters (limit may change).
Responses
200A page of folders, newest first.
Body: FolderList
Errors
- 400
- limit or the cursor is not valid (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/folders \
-H "Authorization: Bearer $KEPTQR_API_KEY"POST/v1/folders
Create a folder
Names can repeat: a second folder with the same name is a new folder.
- Needs a read-and-write key
- createFolder
Parameters
Idempotency-KeystringHeader
1 to 255 printable ASCII characters. A retry with the same key and body within 24 hours returns the first response (Idempotent-Replayed: true); the same key with another body is 400 idempotency_key_reused; while the first is still running, 409 idempotency_in_progress.
Request body · application/json
namestringrequired
Trimmed; 1 to 80 characters. Names can repeat.
1 to 80 characterse.g.
Spring campaign
Other fields are refused.
Responses
201The new folder.
Body: FolderWriteResult
- Idempotent-Replayed
- true when this is the stored response to an earlier request with the same Idempotency-Key. The body is the original (a replayed error keeps the first request's request_id); the X-Request-Id header is new.
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 400
- The body is not a JSON object, a field is not valid, or the Idempotency-Key is not valid or was used with another request (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 404
- A retry with the same Idempotency-Key whose folder has since been deleted (not_found/folder_not_found).
- 409
- A request with this Idempotency-Key is still in progress (conflict/idempotency_in_progress).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable, the request timed out, or Redis unavailable for writes (service_unavailable). Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl -X POST https://api.keptqr.com/v1/folders \
-H "Authorization: Bearer $KEPTQR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring campaign"
}'PATCH/v1/folders/{id}
Rename a folder
Gives the folder a new name. Its codes stay in it.
- Needs a read-and-write key
- updateFolder
Parameters
idstring (uuid)Pathrequired
The folder's id.
Request body · application/json
namestringrequired
Trimmed; 1 to 80 characters. Names can repeat.
1 to 80 characterse.g.
Spring campaign
Other fields are refused.
Responses
200The renamed folder.
Body: FolderWriteResult
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 400
- The body is not a JSON object or a field is not valid (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 404
- No such folder in this workspace (not_found/folder_not_found).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable, the request timed out, or Redis unavailable for writes (service_unavailable). Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl -X PATCH https://api.keptqr.com/v1/folders/{id} \
-H "Authorization: Bearer $KEPTQR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring campaign"
}'DELETE/v1/folders/{id}
Delete a folder
Its codes move to No folder; they are not deleted.
- Needs a read-and-write key
- deleteFolder
Parameters
idstring (uuid)Pathrequired
The folder's id.
Responses
200The folder was deleted.
Body: DeletedFolder
- RateLimit
- The request and write buckets: "requests";r={remaining};t={seconds}, "writes";r={remaining};t={seconds}.
Errors
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, a read-only key, or the workspace over its seat limit (seat_limit_read_only).
- 404
- No such folder in this workspace (not_found/folder_not_found).
- 429
- Per-minute rate limit, monthly quota, too many failed authentications, or writes_per_minute.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable, the request timed out, or Redis unavailable for writes (service_unavailable). Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl -X DELETE https://api.keptqr.com/v1/folders/{id} \
-H "Authorization: Bearer $KEPTQR_API_KEY"GET/v1/brand-kits
List brand kits
Every brand kit in the workspace, the default first, then by name. Not paginated. Pass a kit's id as brand_kit_id when creating a code; kits are edited in the dashboard.
- Needs a read key
- listBrandKits
Responses
200Every brand kit, the default first.
Body: BrandKitList
Errors
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/brand-kits \
-H "Authorization: Bearer $KEPTQR_API_KEY"GET/v1/codes/{id}/stats
A dynamic code's scan stats
Whole UTC days, both ends included. Default: the last 30 days.
- Needs a read key
- getCodeStats
Parameters
idstring (uuid)Pathrequired
The code's id.
fromstring (date)Query
First UTC day, included. Default: 29 days before to. Moved later to the plan's history limit and, per code, to its creation day.
e.g.
2026-10-14tostring (date)Query
Last UTC day, included. Default and latest: today (UTC).
e.g.
2026-10-14
Responses
200The code's scans over the window.
Body: CodeStats
Errors
- 400
- A date is not valid, from is after to, or the window is over 366 days (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 404
- No such code in this workspace (not_found/code_not_found).
- 409
- A static code has no scan stats (conflict/not_dynamic).
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/codes/{id}/stats \
-H "Authorization: Bearer $KEPTQR_API_KEY"GET/v1/stats
The workspace's scan stats
Whole UTC days, both ends included. Default: the last 30 days.
- Needs a read key
- getWorkspaceStats
Parameters
fromstring (date)Query
First UTC day, included. Default: 29 days before to. Moved later to the plan's history limit and, per code, to its creation day.
e.g.
2026-10-14tostring (date)Query
Last UTC day, included. Default and latest: today (UTC).
e.g.
2026-10-14topintegerQuery
How many top codes, 1–50.
1 to 50default
10
Responses
200The workspace's scans over the window, with its top codes.
Body: WorkspaceStats
Errors
- 400
- A date or top is not valid, from is after to, or the window is over 366 days (invalid_request).
- 401
- Missing, malformed, unknown, revoked or expired key (authentication).
- 403
- Browser call, suspended, no API access on the plan, or not allowed for this key.
- 429
- Per-minute rate limit, monthly quota, or too many failed authentications.
- 500
- Something went wrong on our side.
- 503
- A dependency is unavailable or the request timed out. Retry after Retry-After.
Error body: Error. Codes and meanings are in the error codes table.
curl https://api.keptqr.com/v1/stats \
-H "Authorization: Bearer $KEPTQR_API_KEY"Error codes
Every error answers with the error object described in Getting started. The type decides the HTTP status; branch on the code.
| Status | Type | Code | Meaning |
|---|---|---|---|
| 400invalid_request | invalid_request | batch_too_largeA batch has more than 100 items. | A batch has more than 100 items. |
| 400invalid_request | invalid_request | idempotency_key_invalidThe Idempotency-Key isn't 1 to 255 printable ASCII characters. | The Idempotency-Key isn't 1 to 255 printable ASCII characters. |
| 400invalid_request | invalid_request | idempotency_key_reusedThe Idempotency-Key was used in the last 24 hours with a different request. Use a new key. | The Idempotency-Key was used in the last 24 hours with a different request. Use a new key. |
| 400invalid_request | invalid_request | invalid_cursorThe cursor isn't one this list gave for these filters. Start again from the first page. | The cursor isn't one this list gave for these filters. Start again from the first page. |
| 400invalid_request | invalid_request | invalid_destinationThe destination isn't a link a code can redirect to. | The destination isn't a link a code can redirect to. |
| 400invalid_request | invalid_request | invalid_jsonThe request body isn't a JSON object. | The request body isn't a JSON object. |
| 400invalid_request | invalid_request | invalid_parameterA parameter or body field isn't valid. param names it. | A parameter or body field isn't valid. param names it. |
| 400invalid_request | invalid_request | missing_parameterA PATCH body has none of the fields it can change. | A PATCH body has none of the fields it can change. |
| 400invalid_request | invalid_request | window_too_largeThe stats window is longer than 366 days. | The stats window is longer than 366 days. |
| 401authentication | authentication | expired_keyThe API key has expired, or its overlap after a roll has ended. | The API key has expired, or its overlap after a roll has ended. |
| 401authentication | authentication | invalid_keyThere is no API key with that value. | There is no API key with that value. |
| 401authentication | authentication | malformed_keyThe Authorization header doesn't hold a well-formed KeptQR API key. | The Authorization header doesn't hold a well-formed KeptQR API key. |
| 401authentication | authentication | missing_keyNo API key was sent. Send it as Authorization: Bearer <key>. | No API key was sent. Send it as Authorization: Bearer <key>. |
| 401authentication | authentication | revoked_keyThe API key was revoked. | The API key was revoked. |
| 403plan_required | plan_required | api_access_requiredThe workspace's plan doesn't include the API. | The workspace's plan doesn't include the API. |
| 403permission | permission | api_suspendedAPI access is suspended for this workspace. Contact support. | API access is suspended for this workspace. Contact support. |
| 403permission | permission | browser_not_allowedThe request carried an Origin header. The API can't be called from a browser: call it from your server. | The request carried an Origin header. The API can't be called from a browser: call it from your server. |
| 403plan_required | plan_required | bulk_creation_requiredThe workspace's plan doesn't include bulk creation. | The workspace's plan doesn't include bulk creation. |
| 403permission | permission | insufficient_scopeA read key called an operation that needs a read-and-write key. | A read key called an operation that needs a read-and-write key. |
| 403permission | permission | seat_limit_read_onlyThe workspace has more members than its plan allows, so its keys can only read until it's back within the limit. | The workspace has more members than its plan allows, so its keys can only read until it's back within the limit. |
| 404not_found | not_found | brand_kit_not_foundThere is no brand kit with that id in this workspace. | There is no brand kit with that id in this workspace. |
| 404not_found | not_found | code_not_foundThere is no code with that id in this workspace. | There is no code with that id in this workspace. |
| 404not_found | not_found | folder_not_foundThere is no folder with that id in this workspace. | There is no folder with that id in this workspace. |
| 404not_found | not_found | route_not_foundThere is no such route or method. | There is no such route or method. |
| 405invalid_request | invalid_request | method_not_allowedAn OPTIONS request. The API answers no browser preflight. | An OPTIONS request. The API answers no browser preflight. |
| 409conflict | conflict | brand_kit_unavailableThe brand kit can't be used while the workspace has more brand kits than its plan includes. | The brand kit can't be used while the workspace has more brand kits than its plan includes. |
| 409conflict | conflict | code_deletedThe code is deleted. | The code is deleted. |
| 409conflict | conflict | code_disabledKeptQR disabled the code, so it can't be changed and has no image. | KeptQR disabled the code, so it can't be changed and has no image. |
| 409conflict | conflict | editing_frozenThe workspace has more dynamic codes than its plan allows, so destination edits are paused until it's back within the limit. | The workspace has more dynamic codes than its plan allows, so destination edits are paused until it's back within the limit. |
| 409conflict | conflict | idempotency_in_progressA request with this Idempotency-Key is still running. Retry once it has finished. | A request with this Idempotency-Key is still running. Retry once it has finished. |
| 409conflict | conflict | not_dynamicThe operation needs a dynamic code, and this code is static. | The operation needs a dynamic code, and this code is static. |
| 409conflict | conflict | plan_limit_reachedThe workspace has as many dynamic codes as its plan allows. | The workspace has as many dynamic codes as its plan allows. |
| 413invalid_request | invalid_request | body_too_largeThe request body is over 1 MB. | The request body is over 1 MB. |
| 422unsafe_destination | unsafe_destination | destination_flaggedGoogle Web Risk lists the destination as unsafe, so nothing was saved. | Google Web Risk lists the destination as unsafe, so nothing was saved. |
| 422invalid_request | invalid_request | image_unscannableThe code's image doesn't scan reliably at this size and design, or its content doesn't fit a QR code. | The code's image doesn't scan reliably at this size and design, or its content doesn't fit a QR code. |
| 429quota_exceeded | quota_exceeded | monthly_quotaThe workspace has used its monthly API requests. Requests resume at 00:00 UTC on the 1st, or sooner if the workspace's limit is raised. Your codes keep redirecting. | The workspace has used its monthly API requests. Requests resume at 00:00 UTC on the 1st, or sooner if the workspace's limit is raised. Your codes keep redirecting. |
| 429rate_limited | rate_limited | requests_per_minuteThe workspace's per-minute request limit is used up. Retry after Retry-After. | The workspace's per-minute request limit is used up. Retry after Retry-After. |
| 429rate_limited | rate_limited | too_many_failed_authToo many recent requests from this address with a missing, malformed, invalid, revoked or expired key. Retry after Retry-After. | Too many recent requests from this address with a missing, malformed, invalid, revoked or expired key. Retry after Retry-After. |
| 429rate_limited | rate_limited | writes_per_minuteThe workspace's per-minute write limit is used up (a batch needs a write for every item at once). Retry after Retry-After. | The workspace's per-minute write limit is used up (a batch needs a write for every item at once). Retry after Retry-After. |
| 500api_error | api_error | internal_errorSomething went wrong on our side. Quote the request id if you contact us. | Something went wrong on our side. Quote the request id if you contact us. |
| 503unverifiable_destination | unverifiable_destination | destination_unverifiableThe destination's safety check couldn't run, so nothing was saved. Retry after Retry-After. | The destination's safety check couldn't run, so nothing was saved. Retry after Retry-After. |
| 503api_error | api_error | service_unavailableA service the API depends on is briefly unavailable, or the image queue is full. Retry after Retry-After. | A service the API depends on is briefly unavailable, or the image queue is full. Retry after Retry-After. |
| 503api_error | api_error | timeoutThe request ran out of time, and a write may still have been saved: retry a POST with the same Idempotency-Key, or read before writing again. In a batch: the item wasn't started in time, and nothing was saved for it. | The request ran out of time, and a write may still have been saved: retry a POST with the same Idempotency-Key, or read before writing again. In a batch: the item wasn't started in time, and nothing was saved for it. |
Schemas
The objects the operations send and return. Times are UTC, in ISO 8601.
Actor
Who did it: a teammate by display name ("A teammate" when they have none), an API key by id and name, or null.
One of these, or null:
Variant 1
namestringrequired
Their display name, or "A teammate" when they have none. Never an email.
typestringrequired
A teammate, in the dashboard.
always
user
Variant 2
idstring (uuid)required
The key's id.
namestringrequired
The key's name.
typestringrequired
An API key.
always
api_key
BatchResult
Each item's result: the created code, or its error.
createdintegerrequired
How many items were created.
failedintegerrequired
How many items failed.
objectstringrequired
Always "batch_result".
always
batch_resultresultsarray of one ofrequired
One per item, in order.
One of these:
Variant 1
codeCodeWriteResultrequired
The created code, as POST /v1/codes answers it.
indexintegerrequired
The item's position in items, from 0.
statusnumberrequired
Created.
always
201
Variant 2
errorobjectrequired
The item's error, as a single create would answer it, with this batch's request_id.
codestringrequired
What went wrong, more precisely than type. Branch on this.
e.g.
missing_keymessagestringrequired
A plain sentence for a person. Branch on code, not on this.
paramstring
The parameter or field the error is about, when there is one.
request_idstringrequired
The request's id, as in X-Request-Id. Quote it if you contact us.
e.g.
req_01jabcdefghjkmnpqrstvwxyz0typestringrequired
The error's type, which decides the HTTP status (the error codes table).
e.g.
authentication
indexintegerrequired
The item's position in items, from 0.
statusintegerrequired
The HTTP status of the item's error.
testboolean
A test key's batch: nothing was saved.
always
true
BrandKit
A brand kit: a saved design, with an optional logo, to start codes from.
designDesignrequired
The kit's design: what codes created from it start with.
has_logobooleanrequired
Whether the kit has a logo, which codes created from it embed.
idstring (uuid)required
The kit's id: pass it as brand_kit_id when creating a code.
is_defaultbooleanrequired
Whether the dashboard starts new codes from this kit. The API never applies it unasked.
namestringrequired
The kit's name.
objectstringrequired
Always "brand_kit".
always
brand_kitupdated_atstring (date-time)required
When the kit was last changed.
e.g.
2026-10-15T10:00:00.123Z
BrandKitList
Every brand kit in the workspace.
dataarray of BrandKitrequired
Every brand kit, the default first.
has_morebooleanrequired
Always false: the list isn't paginated.
always
falsenext_cursornullrequired
Always null.
objectstringrequired
Always "list".
always
list
Code
A QR code or short link, dynamic or static.
brand_kit_idstring (uuid)requirednullable
The brand kit the code was created from, or null.
created_atstring (date-time)required
When the code was created.
e.g.
2026-10-15T10:00:00.123Zcreated_byActorrequired
Who created the code.
deleted_atstring (date-time)requirednullable
When a dynamic code was deleted; null unless status is deleted.
e.g.
2026-10-15T10:00:00.123ZdesignDesignrequired
How the code's image is drawn.
destinationstringrequirednullable
Where a dynamic code redirects. null for static and deleted codes.
edited_atstring (date-time)required
When a person or key last changed the code's content: its name, destination or design (not its folder). created_at until then.
e.g.
2026-10-15T10:00:00.123Zfolder_idstring (uuid)requirednullable
The code's folder; null: No folder.
idstring (uuid)required
The code's id.
kindstringrequired
dynamic: redirects through its short URL, and its destination can change. static: its content is encoded in the image itself.
one of
dynamic,staticmediumstringrequired
qr: a code to print or show as a QR image. link: a short link to share as text.
one of
qr,linknamestringrequired
The code's name in the dashboard.
objectstringrequired
Always "code".
always
codeshort_urlstringrequirednullable
What a dynamic code encodes and redirects from. null for static codes.
e.g.
https://keptqr.to/k7m2xq9slugstringrequirednullable
A dynamic code's short-URL path. Never reused. null for static codes.
static_contentobjectrequirednullable
A static code's content. null for dynamic codes.
dataany JSON value
Its stored content record.
typestringrequired
The static code's type (url, wifi, vcard, …).
statusstringrequired
disabled: stopped by KeptQR for abuse. deleted: soft-deleted dynamic code.
one of
active,disabled,deletedupdated_atstring (date-time)required
When anything about the code last changed, including changes KeptQR makes itself, such as a safety recheck.
e.g.
2026-10-15T10:00:00.123Z
CodeList
A page of codes.
dataarray of Coderequired
This page's items, newest first.
has_morebooleanrequired
Whether there are more items after this page.
next_cursorstringrequirednullable
Pass as ?cursor= for the next page; null on the last page.
objectstringrequired
Always "list".
always
list
CodeStats
A dynamic code's scans over a window of days.
code_idstring (uuid)required
The code's id.
countriesarray of objectrequired
Top 20, tracked scans only.
countrystringrequirednullable
ISO 3166-1 alpha-2; null when the scan couldn't be located.
e.g.
USscansintegerrequired
Tracked scans from that country.
at least 0
dailyarray of objectrequired
Every day of the window, oldest first, zero-filled.
daystring (date)required
The UTC day.
e.g.
2026-10-14scansintegerrequired
Human scans that day.
at least 0
devicesobjectrequired
Tracked scans only.
desktopintegerrequired
Tracked scans from computers.
at least 0
mobileintegerrequired
Tracked scans from phones.
at least 0
tabletintegerrequired
Tracked scans from tablets.
at least 0
objectstringrequired
Always "code_stats".
always
code_statstotalsobjectrequired
Scans over the whole window.
automatedintegerrequired
Bots, link previews and datacenter traffic.
at least 0
scansintegerrequired
Human scans, as the dashboard counts them.
at least 0
untracked_scans_in_windowbooleanrequired
Whether some scans in the window weren't tracked (over the plan's monthly tracked scans). The codes kept redirecting.
windowStatsWindowrequired
The days these stats cover.
CodeWriteResult
A code write's answer: the code as GET /v1/codes/{id} shows it, plus warnings.
Everything in Code, plus:
testboolean
A test key's write: nothing was saved, and the id is 404 when fetched.
always
truewarningsarray of stringrequired
Scannability warnings for the saved design (writes only): low_contrast or inverted. The design is saved either way.
CreateCode
A new dynamic code: POST /v1/codes's body.
brand_kit_idstring (uuid)nullable
Start from this brand kit (GET /v1/brand-kits), logo included. Omitted or null: no kit.
designDesignInput
Design fields that override the brand kit's design, or the default design without a kit.
destinationstringrequired
Where the code redirects: https, http, mailto or tel. A bare host gets https://. Checked against Google Web Risk before it's saved.
not emptye.g.
https://example.com/springfolder_idstring (uuid)nullable
The folder to put the code in (GET /v1/folders). Omitted or null: No folder.
mediumstring
qr: a code to print or show as a QR image. link: a short link to share as text. Both redirect the same way.
one of
qr,linkdefaultqrnamestringrequired
The code's name in the dashboard. Trimmed; 1 to 120 characters.
1 to 120 characterse.g.
Spring flyer
Other fields are refused.
CreateCodeBatch
Up to 100 new dynamic codes: POST /v1/codes/batch's body.
itemsarray of CreateCoderequired
1 to 100 items, each a POST /v1/codes body. Each item is validated on its own.
1 to 100 items
Other fields are refused.
DeletedCode
A deleted code.
deletedbooleanrequired
Always true.
always
trueidstring (uuid)required
The deleted code's id.
objectstringrequired
Always "code".
always
codetestboolean
A test key's delete: nothing was deleted.
always
true
DeletedFolder
A deleted folder. Its codes moved to No folder.
deletedbooleanrequired
Always true.
always
trueidstring (uuid)required
The deleted folder's id.
objectstringrequired
Always "folder".
always
foldertestboolean
A test key's delete: nothing was deleted.
always
true
Design
How the QR code is drawn. Never includes the logo itself; has_logo says whether one is embedded. ecc is the stored level (a code with a logo renders at H).
bg_colorstringrequired
The background colour, as #RRGGBB.
matches
^#[0-9A-Fa-f]{6}$corner_dot_stylestringrequired
The shape of the three corner squares' centres.
one of
square,dotcorner_square_stylestringrequired
The shape of the three corner squares' outer rings.
one of
square,dot,extra-roundeddot_stylestringrequired
The shape of the modules.
one of
square,rounded,dots,classy,classy-rounded,extra-roundedeccstringrequired
Error correction: L, M, Q or H. Higher levels survive more damage and make denser codes.
one of
L,M,Q,Hfg_colorstringrequired
The colour of the modules and corners, as #RRGGBB.
matches
^#[0-9A-Fa-f]{6}$has_logobooleanrequired
Whether a logo is embedded. The logo itself is never returned; a code's logo can only come from a brand kit.
logo_backdropbooleanrequired
true: the modules behind the logo are cleared, so the logo sits on the background.
logo_size_pctintegerrequired
The logo's size, as a percentage of the code's width: 15 to 30.
15 to 30
transparent_bgbooleanrequired
true: no background is drawn, and bg_color is not used.
DesignInput
Design fields to set; all optional. has_logo is not an input: a logo comes only from a brand kit.
bg_colorstring
The background colour, as #RRGGBB.
matches
^#[0-9A-Fa-f]{6}$corner_dot_stylestring
The shape of the three corner squares' centres.
one of
square,dotcorner_square_stylestring
The shape of the three corner squares' outer rings.
one of
square,dot,extra-roundeddot_stylestring
The shape of the modules.
one of
square,rounded,dots,classy,classy-rounded,extra-roundedeccstring
Error correction: L, M, Q or H. Higher levels survive more damage and make denser codes.
one of
L,M,Q,Hfg_colorstring
The colour of the modules and corners, as #RRGGBB.
matches
^#[0-9A-Fa-f]{6}$logo_backdropboolean
true: the modules behind the logo are cleared, so the logo sits on the background.
logo_size_pctinteger
The logo's size, as a percentage of the code's width: 15 to 30.
15 to 30
transparent_bgboolean
true: no background is drawn, and bg_color is not used.
Other fields are refused.
DestinationChange
One change of a dynamic code's destination.
changed_atstring (date-time)required
When the change was made.
e.g.
2026-10-15T10:00:00.123Zchanged_byActorrequired
Who made the change.
idstring (uuid)required
The change's id.
new_destinationstringrequired
The destination it changed to.
objectstringrequired
Always "destination_change".
always
destination_changeold_destinationstringrequirednullable
null for the first destination.
DestinationHistory
A page of a dynamic code's destination changes.
dataarray of DestinationChangerequired
This page's items, newest first.
has_morebooleanrequired
Whether there are more items after this page.
next_cursorstringrequirednullable
Pass as ?cursor= for the next page; null on the last page.
objectstringrequired
Always "list".
always
list
Error
Every error response's body.
errorobjectrequired
The error.
codestringrequired
What went wrong, more precisely than type. Branch on this.
e.g.
missing_keymessagestringrequired
A plain sentence for a person. Branch on code, not on this.
paramstring
The parameter or field the error is about, when there is one.
request_idstringrequired
The request's id, as in X-Request-Id. Quote it if you contact us.
e.g.
req_01jabcdefghjkmnpqrstvwxyz0typestringrequired
The error's type, which decides the HTTP status (the error codes table).
e.g.
authentication
Folder
A folder of codes.
created_atstring (date-time)required
When the folder was created.
e.g.
2026-10-15T10:00:00.123Zidstring (uuid)required
The folder's id.
namestringrequired
The folder's name. Names can repeat.
objectstringrequired
Always "folder".
always
folder
FolderList
A page of folders.
dataarray of Folderrequired
This page's items, newest first.
has_morebooleanrequired
Whether there are more items after this page.
next_cursorstringrequirednullable
Pass as ?cursor= for the next page; null on the last page.
objectstringrequired
Always "list".
always
list
FolderWriteResult
A folder write's answer: the folder, with test: true for a test key.
Everything in Folder, plus:
testboolean
A test key's write: nothing was saved, and the id is 404 when fetched.
always
true
KeyContext
The key making the request, its workspace and plan, limits and this month's usage.
keyobjectrequired
The key that made this request.
effective_scopestringrequired
"read" while the workspace has more members than its plan allows.
one of
read,writeexpires_atstringrequired
The effective end (the overlap end for a rolled key). ISO 8601 UTC.
idstring (uuid)required
The key's id.
modestringrequired
"test" for a kq_test_ key, whose writes run every check and save nothing.
one of
live,testnamestringrequired
The name the key was given when it was created.
prefixstringrequired
The key's first characters, as Settings → API shows them;
kq_at_for an app connected with OAuth through the MCP server.e.g.
kq_live_AbCdscopestringrequired
"read" or "write" (read and write), as issued.
one of
read,write
limitsobjectrequired
The workspace's effective limits.
analytics_retention_daysintegerrequirednullable
How many days of scan history the plan keeps. null = no limit.
bulk_creationbooleanrequired
Whether the plan allows POST /v1/codes/batch.
dynamic_code_limitintegerrequired
How many dynamic codes the plan allows, deleted ones not counted.
requests_monthlyintegerrequirednullable
Requests a calendar month (UTC). null = unlimited.
requests_per_minuteintegerrequired
Requests a minute, across all the workspace's keys.
writes_per_minuteintegerrequired
Writes a minute, across all the workspace's keys: one per code or folder write, one per batch item.
objectstringrequired
Always "key_context".
always
key_contextplanobjectrequired
The workspace's plan.
idstringrequired
The plan id, for example "advanced".
namestringrequired
The plan name, for example "Advanced".
usageobjectrequired
This month's usage.
api_requests_this_monthintegerrequired
API requests counted toward the monthly quota this calendar month (UTC), this one included.
dynamic_codesintegerrequired
Dynamic codes in the workspace, deleted ones not counted.
quota_resets_atstringrequired
00:00:00Z on the 1st of next month.
tracked_scans_this_monthintegerrequired
Scans tracked this calendar month (UTC).
workspaceobjectrequired
The key's workspace.
idstring (uuid)required
The workspace's id.
namestringrequired
The workspace's name.
PatchCode
The fields to change: at least one. They are applied in the order destination, name, folder_id, design.
designDesignInput
Merged into the stored design; the logo is kept.
destinationstring
Dynamic codes only. The same rules as on create; an unchanged destination is a no-op.
not empty
folder_idstring (uuid)nullable
null moves the code to No folder.
namestring
The code's name in the dashboard. Trimmed; 1 to 120 characters.
1 to 120 characters
Other fields are refused.
PatchError
PATCH /v1/codes/{id}'s error body: Error, plus applied.
errorobjectrequired
The error.
appliedarray of string
Present when the route itself refused: the fields already saved, in order
codestringrequired
What went wrong, more precisely than type. Branch on this.
e.g.
missing_keymessagestringrequired
A plain sentence for a person. Branch on code, not on this.
paramstring
The parameter or field the error is about, when there is one.
request_idstringrequired
The request's id, as in X-Request-Id. Quote it if you contact us.
e.g.
req_01jabcdefghjkmnpqrstvwxyz0typestringrequired
The error's type, which decides the HTTP status (the error codes table).
e.g.
authentication
StatsWindow
The whole UTC days the stats cover, both ends included.
clamped_bystringrequirednullable
Why from is later than requested_from: the plan's history limit, the code's creation day, or null.
one of
retention,created,nullfromstring (date)required
The first UTC day read, included.
e.g.
2026-10-14requested_fromstring (date)required
from after defaulting, before any clamp.
e.g.
2026-10-14tostring (date)required
The last UTC day read, included.
e.g.
2026-10-14
WorkspaceStats
The workspace's scans over a window of days, with its top codes.
countriesarray of objectrequired
Top 20, tracked scans only.
countrystringrequirednullable
ISO 3166-1 alpha-2; null when the scan couldn't be located.
e.g.
USscansintegerrequired
Tracked scans from that country.
at least 0
dailyarray of objectrequired
Every day of the window, oldest first, zero-filled.
daystring (date)required
The UTC day.
e.g.
2026-10-14scansintegerrequired
Human scans that day.
at least 0
devicesobjectrequired
Tracked scans only.
desktopintegerrequired
Tracked scans from computers.
at least 0
mobileintegerrequired
Tracked scans from phones.
at least 0
tabletintegerrequired
Tracked scans from tablets.
at least 0
objectstringrequired
Always "workspace_stats".
always
workspace_statstop_codesarray of objectrequired
Codes with the most scans in the window; deleted codes excluded.
code_idstring (uuid)required
The code's id.
namestringrequired
The code's name.
scansintegerrequired
Human scans in the window.
at least 0
slugstringrequirednullable
The code's slug.
totalsobjectrequired
Scans over the whole window.
automatedintegerrequired
Bots, link previews and datacenter traffic.
at least 0
scansintegerrequired
Human scans, as the dashboard counts them.
at least 0
untracked_scans_in_windowbooleanrequired
Whether some scans in the window weren't tracked (over the plan's monthly tracked scans). The codes kept redirecting.
windowStatsWindowrequired
The days these stats cover.