KeptQR

API docs · v1

API reference

Every operation, parameter, response and error code, generated from the API’s own OpenAPI document.

Base URL
https://api.keptqr.com/v1
Updated
On this page

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.

#workspace

Workspace

The key, its workspace, plan, limits and usage.

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

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.

Example request
curl https://api.keptqr.com/v1/me \
  -H "Authorization: Bearer $KEPTQR_API_KEY"
#codes

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 25

  • cursorstringQuery

    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, static

  • statusstringQuery

    Only codes with this status. Default: active and disabled codes.

    one of active, disabled, deleted

  • qstringQuery

    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.

Example request
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/spring

  • folder_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, linkdefault qr

  • namestringrequired

    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.

Example request
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.

Example request
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.

Example request
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.

Example request
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.

Example request
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 25

  • cursorstringQuery

    next_cursor from the previous page. Only valid with the same filters (limit may change).

Responses

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.

Example request
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, pngdefault svg

  • sizeintegerQuery

    PNG only: width and height in pixels. Default 1024. An SVG is always 1024.

    one of 512, 1024, 2048, 4096

  • If-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.

Example request
curl https://api.keptqr.com/v1/codes/{id}/image \
  -H "Authorization: Bearer $KEPTQR_API_KEY"
#folders

Folders

List, create, rename and delete folders.

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 25

  • cursorstringQuery

    next_cursor from the previous page. Only valid with the same filters (limit may change).

Responses

  • 200A page of folders, newest first.

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.

Example request
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.

Example request
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.

Example request
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.

Example request
curl -X DELETE https://api.keptqr.com/v1/folders/{id} \
  -H "Authorization: Bearer $KEPTQR_API_KEY"
#brand-kits

Brand kits

The workspace's brand kits, to start new codes from.

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

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.

Example request
curl https://api.keptqr.com/v1/brand-kits \
  -H "Authorization: Bearer $KEPTQR_API_KEY"
#stats

Stats

Scan totals by day, country and device, per code or for the workspace.

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-14

  • tostring (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.

Example request
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-14

  • tostring (date)Query

    Last UTC day, included. Default and latest: today (UTC).

    e.g. 2026-10-14

  • topintegerQuery

    How many top codes, 1–50.

    1 to 50default 10

Responses

  • 200The workspace's scans over the window, with its top codes.

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.

Example request
curl https://api.keptqr.com/v1/stats \
  -H "Authorization: Bearer $KEPTQR_API_KEY"
#errors

Error codes

Every error answers with the error object described in Getting started. The type decides the HTTP status; branch on the code.

StatusCode
400invalid_requestbatch_too_largeA batch has more than 100 items.
400invalid_requestidempotency_key_invalidThe Idempotency-Key isn't 1 to 255 printable ASCII characters.
400invalid_requestidempotency_key_reusedThe Idempotency-Key was used in the last 24 hours with a different request. Use a new key.
400invalid_requestinvalid_cursorThe cursor isn't one this list gave for these filters. Start again from the first page.
400invalid_requestinvalid_destinationThe destination isn't a link a code can redirect to.
400invalid_requestinvalid_jsonThe request body isn't a JSON object.
400invalid_requestinvalid_parameterA parameter or body field isn't valid. param names it.
400invalid_requestmissing_parameterA PATCH body has none of the fields it can change.
400invalid_requestwindow_too_largeThe stats window is longer than 366 days.
401authenticationexpired_keyThe API key has expired, or its overlap after a roll has ended.
401authenticationinvalid_keyThere is no API key with that value.
401authenticationmalformed_keyThe Authorization header doesn't hold a well-formed KeptQR API key.
401authenticationmissing_keyNo API key was sent. Send it as Authorization: Bearer <key>.
401authenticationrevoked_keyThe API key was revoked.
403plan_requiredapi_access_requiredThe workspace's plan doesn't include the API.
403permissionapi_suspendedAPI access is suspended for this workspace. Contact support.
403permissionbrowser_not_allowedThe request carried an Origin header. The API can't be called from a browser: call it from your server.
403plan_requiredbulk_creation_requiredThe workspace's plan doesn't include bulk creation.
403permissioninsufficient_scopeA read key called an operation that needs a read-and-write key.
403permissionseat_limit_read_onlyThe workspace has more members than its plan allows, so its keys can only read until it's back within the limit.
404not_foundbrand_kit_not_foundThere is no brand kit with that id in this workspace.
404not_foundcode_not_foundThere is no code with that id in this workspace.
404not_foundfolder_not_foundThere is no folder with that id in this workspace.
404not_foundroute_not_foundThere is no such route or method.
405invalid_requestmethod_not_allowedAn OPTIONS request. The API answers no browser preflight.
409conflictbrand_kit_unavailableThe brand kit can't be used while the workspace has more brand kits than its plan includes.
409conflictcode_deletedThe code is deleted.
409conflictcode_disabledKeptQR disabled the code, so it can't be changed and has no image.
409conflictediting_frozenThe workspace has more dynamic codes than its plan allows, so destination edits are paused until it's back within the limit.
409conflictidempotency_in_progressA request with this Idempotency-Key is still running. Retry once it has finished.
409conflictnot_dynamicThe operation needs a dynamic code, and this code is static.
409conflictplan_limit_reachedThe workspace has as many dynamic codes as its plan allows.
413invalid_requestbody_too_largeThe request body is over 1 MB.
422unsafe_destinationdestination_flaggedGoogle Web Risk lists the destination as unsafe, so nothing was saved.
422invalid_requestimage_unscannableThe code's image doesn't scan reliably at this size and design, or its content doesn't fit a QR code.
429quota_exceededmonthly_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.
429rate_limitedrequests_per_minuteThe workspace's per-minute request limit is used up. Retry after Retry-After.
429rate_limitedtoo_many_failed_authToo many recent requests from this address with a missing, malformed, invalid, revoked or expired key. Retry after Retry-After.
429rate_limitedwrites_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.
500api_errorinternal_errorSomething went wrong on our side. Quote the request id if you contact us.
503unverifiable_destinationdestination_unverifiableThe destination's safety check couldn't run, so nothing was saved. Retry after Retry-After.
503api_errorservice_unavailableA service the API depends on is briefly unavailable, or the image queue is full. Retry after Retry-After.
503api_errortimeoutThe 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

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:

  1. Variant 1

    • namestringrequired

      Their display name, or "A teammate" when they have none. Never an email.

    • typestringrequired

      A teammate, in the dashboard.

      always user

  2. 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_result

  • resultsarray of one ofrequired

    One per item, in order.

    One of these:

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

    2. 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_key

        • messagestringrequired

          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_01jabcdefghjkmnpqrstvwxyz0

        • typestringrequired

          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_kit

  • updated_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 false

  • next_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.123Z

  • created_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.123Z

  • designDesignrequired

    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.123Z

  • folder_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, static

  • mediumstringrequired

    qr: a code to print or show as a QR image. link: a short link to share as text.

    one of qr, link

  • namestringrequired

    The code's name in the dashboard.

  • objectstringrequired

    Always "code".

    always code

  • short_urlstringrequirednullable

    What a dynamic code encodes and redirects from. null for static codes.

    e.g. https://keptqr.to/k7m2xq9

  • slugstringrequirednullable

    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, deleted

  • updated_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. US

    • scansintegerrequired

      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-14

    • scansintegerrequired

      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_stats

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

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 true

  • warningsarray 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/spring

  • folder_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, linkdefault qr

  • namestringrequired

    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 true

  • idstring (uuid)required

    The deleted code's id.

  • objectstringrequired

    Always "code".

    always code

  • testboolean

    A test key's delete: nothing was deleted.

    always true

DeletedFolder

A deleted folder. Its codes moved to No folder.

  • deletedbooleanrequired

    Always true.

    always true

  • idstring (uuid)required

    The deleted folder's id.

  • objectstringrequired

    Always "folder".

    always folder

  • testboolean

    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, dot

  • corner_square_stylestringrequired

    The shape of the three corner squares' outer rings.

    one of square, dot, extra-rounded

  • dot_stylestringrequired

    The shape of the modules.

    one of square, rounded, dots, classy, classy-rounded, extra-rounded

  • eccstringrequired

    Error correction: L, M, Q or H. Higher levels survive more damage and make denser codes.

    one of L, M, Q, H

  • fg_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, dot

  • corner_square_stylestring

    The shape of the three corner squares' outer rings.

    one of square, dot, extra-rounded

  • dot_stylestring

    The shape of the modules.

    one of square, rounded, dots, classy, classy-rounded, extra-rounded

  • eccstring

    Error correction: L, M, Q or H. Higher levels survive more damage and make denser codes.

    one of L, M, Q, H

  • fg_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.123Z

  • changed_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_change

  • old_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_key

    • messagestringrequired

      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_01jabcdefghjkmnpqrstvwxyz0

    • typestringrequired

      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.123Z

  • idstring (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, write

    • expires_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, test

    • namestringrequired

      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_AbCd

    • scopestringrequired

      "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_context

  • planobjectrequired

    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_key

    • messagestringrequired

      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_01jabcdefghjkmnpqrstvwxyz0

    • typestringrequired

      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, null

  • fromstring (date)required

    The first UTC day read, included.

    e.g. 2026-10-14

  • requested_fromstring (date)required

    from after defaulting, before any clamp.

    e.g. 2026-10-14

  • tostring (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. US

    • scansintegerrequired

      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-14

    • scansintegerrequired

      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_stats

  • top_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.