# Retrieve Account
Source: https://docs.useqrkit.com/api-reference/account/retrieve
GET /me
Identifies the workspace behind the API key. Requires no scope — any valid key
works. Integrations use this as the connection test and to build a
human-readable connection label.
# Retrieve Account
Source: https://docs.useqrkit.com/api-reference/account/retrieve-account
/openapi.yaml get /me
Identifies the workspace behind the API key. Requires no scope — any valid key
works. Integrations use this as the connection test and to build a
human-readable connection label.
# Create Batch
Source: https://docs.useqrkit.com/api-reference/batches/create
POST /batches
Creates up to 1,000 QR codes asynchronously. Returns `202` immediately;
poll `GET /batches/{id}` for progress. Available on Plus, Pro, Ultra
and Agency plans. Requires the `qr:write` scope.
If processing is interrupted (for example by a server restart) and the
batch makes no progress for 15 minutes, it is finished as it stands:
items not yet created are marked `error` ("submit it again"), the
batch gets its final status, and `batch.completed` / `batch.failed` is
sent once (for batches under a week old) — so a batch never stays
`processing` forever.
Items use the same `target` rules as single creation. A shared
`design` (or a saved dashboard template via `template_id`) is applied
to every item. Dynamic items count against your plan's QR limit —
capacity is checked before the batch is accepted.
Items may be **GS1 Digital Link** records (`"type": "gs1"`, the same
body as single creation) — the way to register a serialized run. As
with every other item, a per-item `design` is ignored: the batch-level
`design` (or `template_id`) styles the whole batch. Each item is
validated and created independently, so a GS1 refusal (a taken or
retired identifier) fails only that item: its reason appears as that
item's `error` in `GET /batches/{id}`, and the rest of the batch still
completes. GS1 items in `mode: "dynamic"` count against your plan's
separate **GS1 code** limit, not the QR limit; `mode: "static"` ones are
uncapped.
# Create Batch
Source: https://docs.useqrkit.com/api-reference/batches/create-batch
/openapi.yaml post /batches
Creates up to 1,000 QR codes asynchronously. Returns `202` immediately;
poll `GET /batches/{id}` for progress. Available on Plus, Pro, Ultra
and Agency plans. Requires the `qr:write` scope.
If processing is interrupted (for example by a server restart) and the
batch makes no progress for 15 minutes, it is finished as it stands:
items not yet created are marked `error` ("submit it again"), the
batch gets its final status, and `batch.completed` / `batch.failed` is
sent once (for batches under a week old) — so a batch never stays
`processing` forever.
Items use the same `target` rules as single creation. A shared
`design` (or a saved dashboard template via `template_id`) is applied
to every item. Dynamic items count against your plan's QR limit —
capacity is checked before the batch is accepted.
Items may be **GS1 Digital Link** records (`"type": "gs1"`, the same
body as single creation) — the way to register a serialized run. As
with every other item, a per-item `design` is ignored: the batch-level
`design` (or `template_id`) styles the whole batch. Each item is
validated and created independently, so a GS1 refusal (a taken or
retired identifier) fails only that item: its reason appears as that
item's `error` in `GET /batches/{id}`, and the rest of the batch still
completes. GS1 items in `mode: "dynamic"` count against your plan's
separate **GS1 code** limit, not the QR limit; `mode: "static"` ones are
uncapped.
# Download Batch
Source: https://docs.useqrkit.com/api-reference/batches/download
GET /batches/{id}/download
ZIP archive of all completed QR images in the batch. Returns `422` while the batch is still processing.
# Download Batch as ZIP
Source: https://docs.useqrkit.com/api-reference/batches/download-batch-as-zip
/openapi.yaml get /batches/{id}/download
ZIP archive of all completed QR images in the batch. Returns `422` while the batch is still processing.
# Get Batch
Source: https://docs.useqrkit.com/api-reference/batches/get-batch
/openapi.yaml get /batches/{id}
Batch status with item-level results. Poll until `status` is `completed`.
# List Batches
Source: https://docs.useqrkit.com/api-reference/batches/list
GET /batches
# List Batches
Source: https://docs.useqrkit.com/api-reference/batches/list-batches
/openapi.yaml get /batches
# Get Batch
Source: https://docs.useqrkit.com/api-reference/batches/retrieve
GET /batches/{id}
Batch status with item-level results. Poll until `status` is `completed`.
# List Events
Source: https://docs.useqrkit.com/api-reference/events/list-events
/openapi.yaml get /events
Returns the workspace event archive (newest first), with cursor pagination.
Events are retained for 30 days. Requires the `webhooks:read` scope.
# Retrieve Event
Source: https://docs.useqrkit.com/api-reference/events/retrieve-event
/openapi.yaml get /events/{id}
Returns a single event by id. Events are retained for 30 days. Requires the `webhooks:read` scope.
# Create Folder
Source: https://docs.useqrkit.com/api-reference/folders/create
POST /folders
# Create Folder
Source: https://docs.useqrkit.com/api-reference/folders/create-folder
/openapi.yaml post /folders
# Delete Folder
Source: https://docs.useqrkit.com/api-reference/folders/delete
DELETE /folders/{id}
Deletes the folder. The QR codes inside are kept — they just leave the folder.
# Delete Folder
Source: https://docs.useqrkit.com/api-reference/folders/delete-folder
/openapi.yaml delete /folders/{id}
Deletes the folder. The QR codes inside are kept — they just leave the folder.
# List Folders
Source: https://docs.useqrkit.com/api-reference/folders/list
GET /folders
# List Folders
Source: https://docs.useqrkit.com/api-reference/folders/list-folders
/openapi.yaml get /folders
# Update Folder
Source: https://docs.useqrkit.com/api-reference/folders/update
PATCH /folders/{id}
# Update Folder
Source: https://docs.useqrkit.com/api-reference/folders/update-folder
/openapi.yaml patch /folders/{id}
# API Reference
Source: https://docs.useqrkit.com/api-reference/overview
Complete reference for the QRKit API
## Base URL
```
https://api.useqrkit.com/v1
```
## Authentication
All endpoints require a Bearer token in the `Authorization` header. See [Authentication](/authentication) for details.
## Resources
Create, read, update, delete, and download QR codes.
Create multiple QR codes in a single request.
Organize QR codes into folders.
Manage API keys programmatically.
## Endpoints
### QR Codes
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/v1/qr-codes` | [Create a QR code](/api-reference/qr-codes/create) |
| `GET` | `/v1/qr-codes` | [List QR codes](/api-reference/qr-codes/list) |
| `GET` | `/v1/qr-codes/{id}` | [Retrieve a QR code](/api-reference/qr-codes/retrieve) |
| `PATCH` | `/v1/qr-codes/{id}` | [Update a QR code](/api-reference/qr-codes/update) |
| `DELETE` | `/v1/qr-codes/{id}` | [Delete a QR code](/api-reference/qr-codes/delete) |
| `GET` | `/v1/qr-codes/{id}/download` | [Download QR image](/api-reference/qr-codes/download) |
| `GET` | `/v1/qr-codes/{id}/analytics` | [QR code analytics](/api-reference/qr-codes/analytics) |
### Batches
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/v1/batches` | [Create a batch](/api-reference/batches/create) |
| `GET` | `/v1/batches` | [List batches](/api-reference/batches/list) |
| `GET` | `/v1/batches/{id}` | [Get batch status](/api-reference/batches/retrieve) |
| `GET` | `/v1/batches/{id}/download` | [Download batch as ZIP](/api-reference/batches/download) |
### Folders
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/v1/folders` | [Create a folder](/api-reference/folders/create) |
| `GET` | `/v1/folders` | [List folders](/api-reference/folders/list) |
| `PATCH` | `/v1/folders/{id}` | [Update a folder](/api-reference/folders/update) |
| `DELETE` | `/v1/folders/{id}` | [Delete a folder](/api-reference/folders/delete) |
### Tokens
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/v1/tokens` | [Create an API key](/api-reference/tokens/create) |
| `GET` | `/v1/tokens` | [List API keys](/api-reference/tokens/list) |
| `DELETE` | `/v1/tokens/{id}` | [Revoke an API key](/api-reference/tokens/revoke) |
| `POST` | `/v1/tokens/rotate` | [Rotate an API key](/api-reference/tokens/rotate) |
# QR Code Analytics
Source: https://docs.useqrkit.com/api-reference/qr-codes/analytics
GET /qr-codes/{id}/analytics
Scan analytics for a dynamic QR code: totals, daily series and
top breakdowns. Requires the `analytics:read` scope. Static codes
have no scan tracking and return `422`.
# Create QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/create
POST /qr-codes
Creates a QR code, renders its image (SVG, stored on QRKit's CDN) and
returns the full object. Requires the `qr:write` scope.
Each target type takes structured fields (recommended) or a
pre-encoded `destination` string. Dynamic codes count against your
plan's QR limit; static codes do not.
**GS1 Digital Link** codes use a different body, discriminated by
`"type": "gs1"`: they carry an identifier (a GTIN and its qualifiers)
instead of a `target`. See `CreateGs1QrCodeRequest`.
# Create QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/create-qr-code
/openapi.yaml post /qr-codes
Creates a QR code, renders its image (SVG, stored on QRKit's CDN) and
returns the full object. Requires the `qr:write` scope.
Each target type takes structured fields (recommended) or a
pre-encoded `destination` string. Dynamic codes count against your
plan's QR limit; static codes do not.
**GS1 Digital Link** codes use a different body, discriminated by
`"type": "gs1"`: they carry an identifier (a GTIN and its qualifiers)
instead of a `target`. See `CreateGs1QrCodeRequest`.
# Delete QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/delete
DELETE /qr-codes/{id}
Soft-deletes the QR code — it disappears from the dashboard and API immediately. Requires the `qr:write` scope.
# Delete QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/delete-qr-code
/openapi.yaml delete /qr-codes/{id}
Soft-deletes the QR code — it disappears from the dashboard and API immediately. Requires the `qr:write` scope.
# Download QR Image
Source: https://docs.useqrkit.com/api-reference/qr-codes/download
GET /qr-codes/{id}/download
Regenerates the QR image at the requested size and format from the stored design. Requires the `qr:read` scope.
# Download QR Image
Source: https://docs.useqrkit.com/api-reference/qr-codes/download-qr-image
/openapi.yaml get /qr-codes/{id}/download
Regenerates the QR image at the requested size and format from the stored design. Requires the `qr:read` scope.
# GS1 Linkset
Source: https://docs.useqrkit.com/api-reference/qr-codes/gs1-linkset
/openapi.yaml get /qr-codes/{id}/linkset
The RFC 9264 linkset for a dynamic GS1 Digital Link code: every link the
resolver would serve for this identifier, with one anchor per lot/serial
override. Requires the `qr:read` scope.
Returned as `application/linkset+json` **without** this API's `{ data }`
envelope, and with a `Link` header pointing at the GS1 JSON-LD context —
the bytes are the same ones QRKit's resolver serves, so you can hand
them straight to a validator or stand the record up on your own
resolver. Link relations are full GS1 vocabulary IRIs.
Every anchor carries the links registered ON it plus everything
inherited from the level above (section 2.5.10): an override for
`10/LOT1` publishes the GTIN's links as well as its own, most general
first, with a repeated `href` listed once per relation. The default
link is published twice — under `gs1:defaultLink`, and under the
descriptive `gs1:pip` relation a client can ask for by name. Non-https
destinations are dropped rather than served.
Static GS1 codes resolve on your own host and QRKit holds no link record
for them, so they return `422`; so does a non-GS1 code.
# List QR Codes
Source: https://docs.useqrkit.com/api-reference/qr-codes/list
GET /qr-codes
Lists dynamic and static QR codes in the workspace (newest first by
default), with cursor pagination. Requires the `qr:read` scope.
Each code's `target.editable` says whether it can be re-pointed through
`PATCH /v1/qr-codes/{id}`; codes that hold a page rather than a link
(link and review pages, forms, menus, app-store and smart-URL codes,
uploaded files) report `false` and carry their public hosted page URL
in `target.destination`.
# List QR Codes
Source: https://docs.useqrkit.com/api-reference/qr-codes/list-qr-codes
/openapi.yaml get /qr-codes
Lists dynamic and static QR codes in the workspace (newest first by
default), with cursor pagination. Requires the `qr:read` scope.
Each code's `target.editable` says whether it can be re-pointed through
`PATCH /v1/qr-codes/{id}`; codes that hold a page rather than a link
(link and review pages, forms, menus, app-store and smart-URL codes,
uploaded files) report `false` and carry their public hosted page URL
in `target.destination`.
# QR Code Analytics
Source: https://docs.useqrkit.com/api-reference/qr-codes/qr-code-analytics
/openapi.yaml get /qr-codes/{id}/analytics
Scan analytics for a dynamic QR code: totals, daily series and
top breakdowns. Requires the `analytics:read` scope. Static codes
have no scan tracking and return `422`.
# Retrieve QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/retrieve
GET /qr-codes/{id}
Retrieve one QR code by id.
`target.editable` tells you whether the code can be re-pointed through
`PATCH /v1/qr-codes/{id}`. Codes authored in the QRKit dashboard — link
pages, forms, menus, app-store and smart-URL codes, and uploaded
file/audio/video codes — report `editable: false`; their
`target.destination` is the **public hosted page URL** (the same value
as `short_url`) rather than the stored document or file key, and
`linkpage`, `form` and `menupage` additionally carry a small
`target.summary`.
# Retrieve QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/retrieve-qr-code
/openapi.yaml get /qr-codes/{id}
Retrieve one QR code by id.
`target.editable` tells you whether the code can be re-pointed through
`PATCH /v1/qr-codes/{id}`. Codes authored in the QRKit dashboard — link
pages, forms, menus, app-store and smart-URL codes, and uploaded
file/audio/video codes — report `editable: false`; their
`target.destination` is the **public hosted page URL** (the same value
as `short_url`) rather than the stored document or file key, and
`linkpage`, `form` and `menupage` additionally carry a small
`target.summary`.
# Update QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/update
PATCH /qr-codes/{id}
Updates a QR code. Requires the `qr:write` scope.
Dynamic codes: `name`, `target.destination`, `target.utm`, `design`,
`folder_id`, `tags`, `is_active`. Changing the design re-renders the
stored image on the code's own host (its custom domain, if it has one);
changing the destination does **not** require reprinting — the short
URL stays the same. `domain_id` cannot change after creation (`422` for
a different value; the current one is accepted): the host is printed
in the QR.
Dynamic `wifi`/`email`/`sms`/`event` codes are updated with their
structured fields (`target.ssid`/`password`/`encryption`,
`target.email`/`subject`/`message`/`cc`/`bcc`,
`target.phone`/`message`,
`target.title`/`location`/`start`/`end`) rather than
`target.destination`; omitted fields keep their current value.
Dynamic `text` codes take `target.text`.
The link types (`youtube` … `googleform`) take `target.destination`
like `url`. `whatsapp` takes `target.phone` and/or `target.message`
(an omitted one keeps its value) or a WhatsApp link in
`target.destination`; `telegram` takes `target.destination` (a
`@username`, phone or t.me link). Either can also be re-pointed to any
other http(s) page. `location` takes `target.lat` + `target.lng`
and/or `map_app`, `label`, `address` (omitted ones keep their value);
a plain `target.destination` turns it into an ordinary redirect.
Static codes: only `name`, `folder_id` and `design` (the payload is
baked into the printed code).
Dynamic GS1 codes: `destination` (the default link), `links`,
`overrides`, plus `name`, `design`, `folder_id`, `tags` and
`is_active` — all at the TOP level, not under `target`. The identifier
(`primary_key`, `qualifiers`, `attributes`, `domain`, `domain_id`,
`mode`, `gs1_key`) is printed on packaging and immutable: naming any of
them returns `422` with `gs1_code: GS1_IDENTIFIER_IMMUTABLE` rather
than silently ignoring the field. A dynamic GS1 code therefore reports
`target.editable: false`, with `target.destination` set to its Digital
Link URI. A **static** GS1 code bakes its Digital Link into the image
and, like every static code, cannot be re-pointed.
Codes that hold a page rather than a link — link pages (including review
pages created through this API), forms, menus, app-store and smart-URL
codes, and uploaded file/audio/video codes — store a
document or an uploaded file rather than a destination. They are
reported with `target.editable: false` (and `target.destination` set to
their public hosted page URL). Sending `target` for one of those is
refused with `409 target_not_editable` and nothing in the request is
applied; edit them in the dashboard. Their other fields (`name`,
`folder_id`, `tags`, `design`, `is_active`) update normally.
# Update QR Code
Source: https://docs.useqrkit.com/api-reference/qr-codes/update-qr-code
/openapi.yaml patch /qr-codes/{id}
Updates a QR code. Requires the `qr:write` scope.
Dynamic codes: `name`, `target.destination`, `target.utm`, `design`,
`folder_id`, `tags`, `is_active`. Changing the design re-renders the
stored image on the code's own host (its custom domain, if it has one);
changing the destination does **not** require reprinting — the short
URL stays the same. `domain_id` cannot change after creation (`422` for
a different value; the current one is accepted): the host is printed
in the QR.
Dynamic `wifi`/`email`/`sms`/`event` codes are updated with their
structured fields (`target.ssid`/`password`/`encryption`,
`target.email`/`subject`/`message`/`cc`/`bcc`,
`target.phone`/`message`,
`target.title`/`location`/`start`/`end`) rather than
`target.destination`; omitted fields keep their current value.
Dynamic `text` codes take `target.text`.
The link types (`youtube` … `googleform`) take `target.destination`
like `url`. `whatsapp` takes `target.phone` and/or `target.message`
(an omitted one keeps its value) or a WhatsApp link in
`target.destination`; `telegram` takes `target.destination` (a
`@username`, phone or t.me link). Either can also be re-pointed to any
other http(s) page. `location` takes `target.lat` + `target.lng`
and/or `map_app`, `label`, `address` (omitted ones keep their value);
a plain `target.destination` turns it into an ordinary redirect.
Static codes: only `name`, `folder_id` and `design` (the payload is
baked into the printed code).
Dynamic GS1 codes: `destination` (the default link), `links`,
`overrides`, plus `name`, `design`, `folder_id`, `tags` and
`is_active` — all at the TOP level, not under `target`. The identifier
(`primary_key`, `qualifiers`, `attributes`, `domain`, `domain_id`,
`mode`, `gs1_key`) is printed on packaging and immutable: naming any of
them returns `422` with `gs1_code: GS1_IDENTIFIER_IMMUTABLE` rather
than silently ignoring the field. A dynamic GS1 code therefore reports
`target.editable: false`, with `target.destination` set to its Digital
Link URI. A **static** GS1 code bakes its Digital Link into the image
and, like every static code, cannot be re-pointed.
Codes that hold a page rather than a link — link pages (including review
pages created through this API), forms, menus, app-store and smart-URL
codes, and uploaded file/audio/video codes — store a
document or an uploaded file rather than a destination. They are
reported with `target.editable: false` (and `target.destination` set to
their public hosted page URL). Sending `target` for one of those is
refused with `409 target_not_editable` and nothing in the request is
applied; edit them in the dashboard. Their other fields (`name`,
`folder_id`, `tags`, `design`, `is_active`) update normally.
# Create API Key
Source: https://docs.useqrkit.com/api-reference/tokens/create
POST /tokens
Creates an API key. Authenticated with a **Clerk session token**
(dashboard sign-in), not an API key — most users should create keys
from the [dashboard](https://useqrkit.com/dashboard/api-keys) instead.
The full key is returned **once**; only its prefix is retrievable
later. Available on every plan. A workspace can hold at most 10 active
(non-revoked, non-expired) keys — 1 on the Free plan.
# Create API Key
Source: https://docs.useqrkit.com/api-reference/tokens/create-api-key
/openapi.yaml post /tokens
Creates an API key. Authenticated with a **Clerk session token**
(dashboard sign-in), not an API key — most users should create keys
from the [dashboard](https://useqrkit.com/dashboard/api-keys) instead.
The full key is returned **once**; only its prefix is retrievable
later. Available on every plan. A workspace can hold at most 10 active
(non-revoked, non-expired) keys — 1 on the Free plan.
# List API Keys
Source: https://docs.useqrkit.com/api-reference/tokens/list
GET /tokens
Lists the workspace's active API keys (prefixes only). Clerk session auth.
# List API Keys
Source: https://docs.useqrkit.com/api-reference/tokens/list-api-keys
/openapi.yaml get /tokens
Lists the workspace's active API keys (prefixes only). Clerk session auth.
# Revoke API Key
Source: https://docs.useqrkit.com/api-reference/tokens/revoke
DELETE /tokens/{id}
Immediately and permanently invalidates the key. Clerk session auth.
# Revoke API Key
Source: https://docs.useqrkit.com/api-reference/tokens/revoke-api-key
/openapi.yaml delete /tokens/{id}
Immediately and permanently invalidates the key. Clerk session auth.
# Rotate API Key
Source: https://docs.useqrkit.com/api-reference/tokens/rotate
POST /tokens/rotate
Revokes the old key and returns a new one with identical name, scopes, environment and expiry. Clerk session auth.
# Rotate API Key
Source: https://docs.useqrkit.com/api-reference/tokens/rotate-api-key
/openapi.yaml post /tokens/rotate
Revokes the old key and returns a new one with identical name, scopes, environment and expiry. Clerk session auth.
# Create Webhook Endpoint
Source: https://docs.useqrkit.com/api-reference/webhooks/create-webhook-endpoint
/openapi.yaml post /webhooks
Registers a new webhook endpoint. The signing secret (`secret`) is returned
**only on this response** and on `rotate-secret` — store it securely.
Requires the `webhooks:write` scope. Endpoint caps are counted per `source`:
10 for `api` (the default), 50 for each integration source.
# Delete Webhook Endpoint
Source: https://docs.useqrkit.com/api-reference/webhooks/delete-webhook-endpoint
/openapi.yaml delete /webhooks/{id}
Permanently deletes the endpoint and stops all pending deliveries. Requires the `webhooks:write` scope.
# List Webhook Deliveries
Source: https://docs.useqrkit.com/api-reference/webhooks/list-webhook-deliveries
/openapi.yaml get /webhooks/{id}/deliveries
Returns the delivery log for an endpoint (newest first). Requires the `webhooks:read` scope.
# List Webhook Endpoints
Source: https://docs.useqrkit.com/api-reference/webhooks/list-webhook-endpoints
/openapi.yaml get /webhooks
Returns all webhook endpoints for the workspace, across all sources. The `secret` field is masked. Requires the `webhooks:read` scope.
# Retrieve Webhook Endpoint
Source: https://docs.useqrkit.com/api-reference/webhooks/retrieve-webhook-endpoint
/openapi.yaml get /webhooks/{id}
Returns a single webhook endpoint. The `secret` field is masked. Requires the `webhooks:read` scope.
# Rotate Webhook Secret
Source: https://docs.useqrkit.com/api-reference/webhooks/rotate-webhook-secret
/openapi.yaml post /webhooks/{id}/rotate-secret
Generates a new signing secret for the endpoint and returns it (full value,
shown once). The previous secret remains valid as a second `v1=` signature
entry for 24 hours to allow zero-downtime rotation.
Requires the `webhooks:write` scope.
# Send Test Webhook
Source: https://docs.useqrkit.com/api-reference/webhooks/send-test-webhook
/openapi.yaml post /webhooks/{id}/test
Sends a `ping` event to this endpoint synchronously through the real signing and
delivery pipeline, and returns the resulting `WebhookDelivery` plus a `succeeded`
boolean. The endpoint must have `status: enabled`. Requires the `webhooks:write` scope.
# Update Webhook Endpoint
Source: https://docs.useqrkit.com/api-reference/webhooks/update-webhook-endpoint
/openapi.yaml patch /webhooks/{id}
Updates one or more fields of a webhook endpoint. All fields are optional —
supply only the ones you want to change. Set `status` to `enabled` to
re-enable an endpoint that was manually or automatically disabled (resets the
consecutive failure count). Requires the `webhooks:write` scope.
# Authentication
Source: https://docs.useqrkit.com/authentication
API key management, Bearer tokens, and scopes
## Bearer tokens
All API requests must include a Bearer token in the `Authorization` header:
```bash theme={null}
curl https://api.useqrkit.com/v1/qr-codes \
-H "Authorization: Bearer qr_live_your_key_here"
```
Requests without a valid token return a `401 Unauthorized` error.
To check a key without touching any resources, call `GET /v1/me` — it requires
no scope and returns the workspace name, plan, scopes and environment behind
the key, plus the workspace's active `custom_domains` (the ids a create's
`domain_id` accepts):
```bash theme={null}
curl https://api.useqrkit.com/v1/me \
-H "Authorization: Bearer qr_live_your_key_here"
```
## API key format
QRKit API keys use a prefix to indicate the environment:
| Prefix | Environment | Description |
| - | - | - |
| `qr_live_` | Production | For your production integration |
| `qr_test_` | Test | For development and CI — created codes are marked as test-created |
Test keys work identically to live keys and create real QR codes in your
workspace (marked internally as created by a test key), so you can verify
the full flow end to end. Delete test codes when you're done — they appear
in your dashboard like any other code.
## Scopes
Each API key has one or more scopes that control what it can access:
| Scope | Description |
| - | - |
| `qr:read` | Read QR codes and folders |
| `qr:write` | Create, update, and delete QR codes and folders |
| `analytics:read` | Access scan analytics |
| `tokens:manage` | Create, list, and revoke API keys |
| `webhooks:read` | List webhooks and their deliveries |
| `webhooks:write` | Create, update, and delete webhooks |
Follow the principle of least privilege. Only grant scopes that your integration needs.
On the Free plan, requests using the `analytics:read`, `webhooks:read` and
`webhooks:write` scopes return a `403` — analytics and webhooks require a
paid plan. All other scopes work on every plan.
## Creating API keys
API keys are created from the [QRKit dashboard](https://useqrkit.com/dashboard/api-keys) (recommended) or via the [Tokens API](/api-reference/tokens/create). API access is included on every plan — the Free plan is an evaluation tier (50 requests per month, static QR codes only, 1 active key); see [Rate limiting](/rate-limiting) for the full limits. On the Free plan you must also verify your account email before you can create an API key.
When you create a key, the full key value is returned **once**. Store it securely — you won't be able to see it again. The dashboard and API only show the key prefix (e.g., `qr_live_a1b2`) for identification.
## Key rotation
Rotate API keys using the [Rotate endpoint](/api-reference/tokens/rotate) or the dashboard. This creates a new key with the same name, scopes and expiry, and revokes the old one.
Token management endpoints (`/tokens/*`) are authenticated with your QRKit
dashboard session (a Clerk session JWT), not with an API key — a leaked API
key can never mint new keys.
```bash theme={null}
curl -X POST https://api.useqrkit.com/v1/tokens/rotate \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"key_id": "key_abc123"}'
```
## Key expiration
API keys can optionally have an expiration date. Set `expires_in_days` when creating a key:
```json theme={null}
{
"name": "CI Pipeline Key",
"scopes": ["qr:read", "qr:write"],
"expires_in_days": 90
}
```
Expired keys return a `401 Unauthorized` error. Create a new key or use rotation before the current key expires.
## Security best practices
Never hardcode API keys in source code. Use environment variables or a secrets manager.
Use `qr_test_` keys during development and CI/CD. Switch to `qr_live_` only in production.
Set up a rotation schedule for your production keys (e.g., every 90 days).
Grant only the scopes your integration needs. A read-only dashboard only needs `qr:read`.
# Errors
Source: https://docs.useqrkit.com/errors
Error response format and error codes reference
## Error format
All errors follow a consistent JSON structure:
```json theme={null}
{
"error": {
"code": "not_found",
"message": "QR code not found",
"doc_url": "https://docs.useqrkit.com/errors#not_found",
"request_id": "req_abc123"
}
}
```
| Field | Description |
| - | - |
| `code` | Machine-readable error code |
| `message` | Human-readable description |
| `doc_url` | Link to relevant documentation |
| `request_id` | Unique ID for debugging (include this when contacting support) |
## Error codes
| HTTP Status | Code | Description |
| - | - | - |
| `400` | `invalid_request` | The request body is malformed or missing required fields |
| `401` | `unauthorized` | Missing or invalid API key |
| `403` | `forbidden` | The API key does not have the required scope, or your plan does not include this feature |
| `404` | `not_found` | The requested resource does not exist |
| `409` | `conflict` | A resource with that identifier already exists |
| `409` | `target_not_editable` | `PATCH` sent `target` for a code that holds a page rather than a link (link or review page, form, menu, app-store, smart-URL or uploaded-file code). Nothing in the request was applied; edit its content in the dashboard |
| `422` | `unprocessable_entity` | The request is valid but cannot be processed (e.g., plan limit reached) |
| `422` | `destination_unsafe` | The destination is on QRKit's blocklist or flagged by Google Web Risk as phishing or malware. Use a different destination; retrying the same URL fails the same way. |
| `429` | `rate_limit_exceeded` | Too many requests — see [Rate Limiting](/rate-limiting) |
| `500` | `internal_error` | An unexpected error occurred on our end |
## Example responses
### 400 — Invalid request
```json theme={null}
{
"error": {
"code": "invalid_request",
"message": "target: target.destination is required for url QR codes",
"doc_url": "https://docs.useqrkit.com/errors#invalid_request",
"request_id": "req_x1y2z3"
}
}
```
### 401 — Unauthorized
```json theme={null}
{
"error": {
"code": "unauthorized",
"message": "Invalid API key",
"doc_url": "https://docs.useqrkit.com/errors#unauthorized",
"request_id": "req_a1b2c3"
}
}
```
### 404 — Not found
```json theme={null}
{
"error": {
"code": "not_found",
"message": "QR code 'qr_999' not found",
"doc_url": "https://docs.useqrkit.com/errors#not_found",
"request_id": "req_d4e5f6"
}
}
```
### 422 — Plan limit reached
```json theme={null}
{
"error": {
"code": "unprocessable_entity",
"message": "QR code limit reached for your plan. Upgrade to create more.",
"doc_url": "https://docs.useqrkit.com/errors#unprocessable_entity",
"request_id": "req_g7h8i9"
}
}
```
### 429 — Rate limited
```json theme={null}
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 30 seconds.",
"doc_url": "https://docs.useqrkit.com/errors#rate_limit_exceeded",
"request_id": "req_j1k2l3"
}
}
```
## Handling errors
```python Python theme={null}
import requests
response = requests.post(
"https://api.useqrkit.com/v1/qr-codes",
headers={"Authorization": "Bearer qr_live_..."},
json={"target": {"type": "url", "destination": "https://example.com"}},
)
if not response.ok:
error = response.json()["error"]
print(f"Error {response.status_code}: {error['code']} — {error['message']}")
print(f"Request ID: {error['request_id']}")
```
```javascript Node.js theme={null}
const response = await fetch("https://api.useqrkit.com/v1/qr-codes", {
method: "POST",
headers: {
Authorization: "Bearer qr_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
target: { type: "url", destination: "https://example.com" },
}),
});
if (!response.ok) {
const { error } = await response.json();
console.error(`Error ${response.status}: ${error.code} — ${error.message}`);
console.error(`Request ID: ${error.request_id}`);
}
```
# Introduction
Source: https://docs.useqrkit.com/introduction
Generate, customize, and manage QR codes with the QRKit API
## Welcome to QRKit
The QRKit API lets you programmatically create, manage, and track QR codes. Build dynamic QR codes with custom designs, organize them into folders, and track scans — all through a simple REST API.
### What you can do
20+ types, static or dynamic: URLs and social links, WhatsApp, locations, app stores, review pages, WiFi, vCard, email, SMS, events and GS1 Digital Link.
Customize colors, shapes, gradients, and add your logo to every QR code.
Totals, daily series, and breakdowns by country, device, OS and browser.
Group QR codes into folders to keep your workspace organized.
Create up to 1,000 QR codes in a single request.
Everything created via the API shows up in your QRKit dashboard, and vice-versa.
### Base URL
All API requests are made to:
```
https://api.useqrkit.com/v1
```
### Authentication
Every request requires a Bearer token. You can create API keys from the [QRKit dashboard](https://useqrkit.com/dashboard/api-keys) on any plan.
```bash theme={null}
curl https://api.useqrkit.com/v1/qr-codes \
-H "Authorization: Bearer qr_live_..."
```
### Quick links
Create your first QR code in 5 minutes.
Explore all endpoints with the interactive playground.
# MCP Server
Source: https://docs.useqrkit.com/mcp-server
Use QRKit from Claude, ChatGPT, Claude Code, VS Code, Codex, Cursor or opencode through the Model Context Protocol
QRKit runs a remote [MCP](https://modelcontextprotocol.io) server at
`https://api.useqrkit.com/mcp`. It exposes your workspace as 20 tools that share
one implementation with the REST API, so anything documented in the API
reference behaves the same way when an assistant calls it.
Authentication is OAuth 2.1 with PKCE against your QRKit account. There is no
API key and no client secret, and in most clients the server URL is the only
value you paste. The page at [useqrkit.com/mcp](https://useqrkit.com/mcp) has
click-by-click steps for each client.
| Value | |
| - | - |
| Server URL | `https://api.useqrkit.com/mcp` |
| OAuth Client ID | Not needed, except in Cursor: `cbr3QSYxMJQg8jRJ` |
| OAuth Client Secret | None. Leave it empty |
Claude, ChatGPT, Claude Code, VS Code, Codex and opencode identify themselves
with a Client ID Metadata Document that their maker publishes. QRKit accepts
only the clients it has approved, so a client that isn't listed here gets a
sign-in error. The public client ID for Cursor is safe to share: the
authorization code can only be delivered to that client's registered callback
addresses, and PKCE binds each sign-in to the client that started it.
## Claude
In Claude, open **Customize → Connectors → Add custom connector** and paste
the server URL. On Team or Enterprise an owner adds it under **Organization
settings → Connectors**. If Claude asks how to identify itself, keep **Use
Claude's published identity** and leave the client ID and secret empty. Click
**Add**, then **Connect**. Custom connectors work on every Claude plan (one on
Free).
## ChatGPT
Turn on Developer mode under **Settings → Apps & Connectors → Advanced
settings**, then create an app with the server URL. Set authentication to
OAuth and leave the client ID and secret empty; if ChatGPT asks for a
registration method, choose the client metadata document (CIMD). Developer
mode is not available on the free ChatGPT plan.
## Claude Code
```bash theme={null}
claude mcp add --transport http --scope user qrkit https://api.useqrkit.com/mcp
```
Then start Claude Code, type `/mcp`, choose `qrkit` and pick **Authenticate**.
Leave out `--scope user` to add the server to the current project only.
## VS Code
Add the server to your user `mcp.json` (**MCP: Open User Configuration**), or
run **MCP: Add Server** and choose HTTP:
```json theme={null}
{
"servers": {
"qrkit": {
"type": "http",
"url": "https://api.useqrkit.com/mcp"
}
}
}
```
Start the server and allow the sign-in when VS Code asks. The tools are used
from Copilot Chat in agent mode.
## Codex
```bash theme={null}
codex mcp add qrkit --url https://api.useqrkit.com/mcp
codex mcp login qrkit
```
The Codex IDE extension and the ChatGPT desktop app share this configuration;
there, click **Authenticate** next to `qrkit`.
## Cursor
Cursor does not support client metadata documents yet, so it uses the public
client ID. Add this to `~/.cursor/mcp.json`, or `.cursor/mcp.json` in a
project, without a `CLIENT_SECRET`:
```json theme={null}
{
"mcpServers": {
"qrkit": {
"url": "https://api.useqrkit.com/mcp",
"auth": { "CLIENT_ID": "cbr3QSYxMJQg8jRJ" }
}
}
}
```
Then connect `qrkit` from Cursor's MCP settings and sign in.
## opencode
Add the server to `opencode.json`, or `~/.config/opencode/opencode.json` for
every project, then sign in:
```json theme={null}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"qrkit": {
"type": "remote",
"url": "https://api.useqrkit.com/mcp"
}
}
}
```
```bash theme={null}
opencode mcp auth qrkit
```
If opencode asks for a client ID, update it with `opencode upgrade`.
## Scopes
| Scope | Grants |
| - | - |
| `profile` | Your name and avatar, standard sign-in |
| `email` | Your email address, standard sign-in |
| `qr:read` | View QR codes |
| `qr:write` | Create and edit QR codes |
| `analytics:read` | View scan analytics |
| `webhooks:read` | View webhook settings |
No tool can create API keys, change billing, or modify webhooks.
## Tools
| Tool | Notes |
| - | - |
| `get_account` | Organization, plan, granted scopes and active custom domains — assistants are told to call this first |
| `create_qr_code` | Same body rules as `POST /v1/qr-codes` (URL, social links, WhatsApp, Telegram, location, app stores, review page, WiFi, vCard, text, email, SMS, event); Free plan is static-only |
| `create_gs1_qr_code` | GS1 Digital Link — the `type: "gs1"` body of `POST /v1/qr-codes`. The assistant is told to confirm the identifier with you first: it can never be changed |
| `list_qr_codes`, `get_qr_code`, `update_qr_code`, `delete_qr_code` | Mirror the QR code endpoints |
| `download_qr_code` | SVG as text, or an inline PNG up to 1024 px |
| `get_qr_analytics` | Dynamic codes on paid plans |
| `create_batch`, `list_batches`, `get_batch`, `download_batch` | Creating a batch needs Plus and above; the read tools work on any plan. `download_batch` returns where to fetch the ZIP |
| `create_folder`, `list_folders`, `update_folder`, `delete_folder` | Folder management |
| `list_webhooks`, `get_webhook`, `list_webhook_deliveries` | Read-only; signing secrets are masked; paid plans |
## Limits and metering
Every tool call is metered exactly like a REST request — including calls
refused for a plan or validation reason — against the workspace's monthly
quota and per-minute rate limit (see [Rate Limiting](/rate-limiting)). The
server additionally throttles protocol traffic per OAuth access token at 600
requests a minute; that limit does not consume plan quota.
## Disconnecting
Remove the connector in Claude or ChatGPT, or remove the `qrkit` server from
your editor or CLI (for example `claude mcp remove qrkit` or
`codex mcp remove qrkit`). The "MCP connection" entry in your dashboard's
API keys list is bookkeeping for the connection and is not a usable key;
revoking it does not disconnect anything.
# Pagination
Source: https://docs.useqrkit.com/pagination
Cursor-based pagination for list endpoints
## Overview
List endpoints return paginated results using cursor-based pagination. This provides stable, efficient pagination even when data is being added or removed.
## Query parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `limit` | integer | `25` | Number of items per page (max `100`) |
| `cursor` | string | — | Cursor from `next_cursor` in a previous response |
## Response format
List responses follow a consistent format:
```json theme={null}
{
"object": "list",
"data": [
{ "id": "qr_123", "object": "qr_code", "..." : "..." },
{ "id": "qr_124", "object": "qr_code", "..." : "..." }
],
"has_more": true,
"next_cursor": "eyJpZCI6InFyXzEyNCJ9"
}
```
| Field | Description |
| - | - |
| `object` | Always `"list"` |
| `data` | Array of resource objects |
| `has_more` | `true` if there are more pages |
| `next_cursor` | Cursor to pass as the `cursor` parameter for the next page. `null` if no more pages. |
## Fetching all pages
```python Python theme={null}
import requests
def fetch_all_qr_codes(api_key):
all_codes = []
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://api.useqrkit.com/v1/qr-codes",
headers={"Authorization": f"Bearer {api_key}"},
params=params,
)
data = response.json()
all_codes.extend(data["data"])
if not data["has_more"]:
break
cursor = data["next_cursor"]
return all_codes
```
```javascript Node.js theme={null}
async function fetchAllQrCodes(apiKey) {
const allCodes = [];
let cursor = null;
while (true) {
const params = new URLSearchParams({ limit: "100" });
if (cursor) params.set("cursor", cursor);
const response = await fetch(
`https://api.useqrkit.com/v1/qr-codes?${params}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
const data = await response.json();
allCodes.push(...data.data);
if (!data.has_more) break;
cursor = data.next_cursor;
}
return allCodes;
}
```
## Sorting
List endpoints support sorting with `sort` and `order` parameters:
| Parameter | Values | Default |
| - | - | - |
| `sort` | `created_at`, `updated_at`, `name`, `scan_count` | `created_at` |
| `order` | `asc`, `desc` | `desc` |
```bash theme={null}
curl "https://api.useqrkit.com/v1/qr-codes?sort=scan_count&order=desc&limit=10" \
-H "Authorization: Bearer qr_live_..."
```
Sorting is available on the [List QR Codes](/api-reference/qr-codes/list) endpoint. Other list endpoints return results sorted by `created_at` descending.
# Quickstart
Source: https://docs.useqrkit.com/quickstart
Create your first QR code in 5 minutes
## 1. Get your API key
Sign in to QRKit and open [**Dashboard → API Keys**](https://useqrkit.com/dashboard/api-keys). Create a new key with `qr:read` and `qr:write` scopes (API access is included on every plan).
On the Free plan the API creates static QR codes only — use `"type": "static"`
in the example below. Dynamic QR codes (editable destination, scan analytics)
require a paid plan. On the Free plan you must also verify your account email
before you can create an API key.
Your full API key is only shown once at creation. Copy it and store it securely.
## 2. Create a QR code
Make your first API call to create a dynamic QR code:
```bash cURL theme={null}
curl -X POST https://api.useqrkit.com/v1/qr-codes \
-H "Authorization: Bearer qr_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "My Website",
"type": "dynamic",
"target": {
"type": "url",
"destination": "https://example.com"
}
}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.useqrkit.com/v1/qr-codes",
headers={
"Authorization": "Bearer qr_live_your_key_here",
"Content-Type": "application/json",
},
json={
"name": "My Website",
"type": "dynamic",
"target": {
"type": "url",
"destination": "https://example.com",
},
},
)
qr_code = response.json()
print(qr_code["id"])
```
```javascript Node.js theme={null}
const response = await fetch("https://api.useqrkit.com/v1/qr-codes", {
method: "POST",
headers: {
Authorization: "Bearer qr_live_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "My Website",
type: "dynamic",
target: {
type: "url",
destination: "https://example.com",
},
}),
});
const qrCode = await response.json();
console.log(qrCode.id);
```
## 3. View the response
A successful request returns the created QR code object:
On Plus and higher plans with a primary custom domain, dynamic codes use that
domain in `short_url` (and `domain_id` names it) unless you pass `domain_id`.
```json theme={null}
{
"id": "qr_123",
"object": "qr_code",
"kind": "dynamic",
"name": "My Website",
"type": "url",
"short_code": "AB3D5",
"short_url": "https://qrkit.to/AB3D5",
"domain_id": null,
"target": {
"type": "url",
"destination": "https://example.com",
"editable": true
},
"design": {
"colors": { "foreground": "#000000", "background": "#ffffff" },
"gradient": null,
"shapes": { "body": "square", "eye_frame": "square", "eye_ball": "square" },
"eye_colors": null,
"has_logo": false,
"transparent_background": false
},
"folder_id": null,
"tags": [],
"image_url": "https://img.useqrkit.com/qr-AB3D5-1717000000000.svg",
"download_url": "/v1/qr-codes/qr_123/download",
"is_active": true,
"scan_count": 0,
"utm": null,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
```
The QR image itself encodes `short_url` — print it once and change the
destination any time with [Update QR Code](/api-reference/qr-codes/update).
WiFi, email, SMS, text and event codes are **static** instead: the payload
is encoded directly into the image (no `short_url`, no scan tracking) and
their ids start with `sqr_`.
## 4. Download the image
Download the QR code image in SVG or PNG format:
```bash cURL theme={null}
curl https://api.useqrkit.com/v1/qr-codes/qr_123/download?format=png&size=500 \
-H "Authorization: Bearer qr_live_your_key_here" \
--output qrcode.png
```
```python Python theme={null}
response = requests.get(
"https://api.useqrkit.com/v1/qr-codes/qr_123/download",
headers={"Authorization": "Bearer qr_live_your_key_here"},
params={"format": "png", "size": 500},
)
with open("qrcode.png", "wb") as f:
f.write(response.content)
```
```javascript Node.js theme={null}
const response = await fetch(
"https://api.useqrkit.com/v1/qr-codes/qr_123/download?format=png&size=500",
{
headers: { Authorization: "Bearer qr_live_your_key_here" },
}
);
const buffer = await response.arrayBuffer();
```
## Next steps
Learn about API key scopes and environments.
Customize colors, shapes, and logos.
Handle errors gracefully in your integration.
Understand rate limits for your plan.
# Rate Limiting
Source: https://docs.useqrkit.com/rate-limiting
Rate limits by plan, response headers, and handling 429 errors
## Rate limits
Rate limits restrict how many requests you can make per minute to prevent abuse and ensure fair usage.
| Plan | Requests per minute |
| - | -: |
| Free | 5 |
| Starter | 20 |
| Plus | 60 |
| Pro | 120 |
| Ultra | 300 |
| Agency | 300 |
## Monthly quotas
Monthly quotas cap the total number of API requests per calendar month.
| Plan | API access | Requests per month |
| - | - | -: |
| Free | Yes (static QR codes only) | 50 |
| Starter | Yes | 500 |
| Plus | Yes | 3,000 |
| Pro | Yes | 10,000 |
| Ultra | Yes | 50,000 |
| Agency | Yes | 50,000 |
The Free plan is an evaluation tier: 50 requests per month, static QR codes
only, and at most **1 active API key**. Analytics and webhooks
(`analytics:read`, `webhooks:read`, `webhooks:write`) require a paid plan.
Free-tier requests are also subject to per-IP ceilings of 10 requests per
minute and 200 per month, shared across all workspaces behind the same
address.
Both the per-minute limit and the monthly quota apply to your whole workspace — creating extra keys does not increase either. A workspace can hold at most 10 active API keys (1 on the Free plan). If you need higher limits, [contact us](mailto:support@useqrkit.com) about a custom plan.
## Response headers
Every API response includes rate limit headers:
| Header | Description |
| - | - |
| `X-RateLimit-Limit` | Maximum requests allowed per window |
| `X-RateLimit-Remaining` | Requests remaining in current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
| `Retry-After` | Seconds to wait before retrying (only on `429` responses) |
Example headers:
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1705312800
```
## Handling 429 errors
When you exceed the rate limit, the API returns a `429` status with a `Retry-After` header:
```json theme={null}
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 30 seconds.",
"doc_url": "https://docs.useqrkit.com/errors#rate_limit_exceeded",
"request_id": "req_abc123"
}
}
```
### Retry with backoff
```python Python theme={null}
import time
import requests
def make_request(url, headers, json=None):
for attempt in range(5):
response = requests.post(url, headers=headers, json=json)
if response.status_code != 429:
return response
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
return response
```
```javascript Node.js theme={null}
async function makeRequest(url, options) {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
const retryAfter = parseInt(response.headers.get("Retry-After") || 2 ** attempt);
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
}
}
```
## Best practices
Monitor `X-RateLimit-Remaining` to avoid hitting the limit.
When retrying after a 429, use the `Retry-After` header value or exponential backoff.
Use the [Batches API](/api-reference/batches/create) to create up to 1,000 QR codes in a single request instead of making individual calls.
Cache `GET` responses to reduce unnecessary API calls.
# Webhooks
Source: https://docs.useqrkit.com/webhooks
Receive signed event notifications when things happen in your workspace
## Overview
Webhooks push events to your HTTPS endpoint as they happen. Endpoints are managed entirely via the API (`/v1/webhooks`); there is no dashboard UI.
| Constraint | Value |
| - | - |
| Max endpoints per workspace | 10 (`api` source) + 50 per integration source |
| Plan required | Paid (Starter or above) |
| Timeout per delivery | 10 seconds |
Endpoints carry a `source` (`api` by default; integration platforms like Zapier,
Make and n8n set their own when they register endpoints on your behalf). Caps are
counted per source, so integration-managed endpoints never crowd out your own.
Sign each endpoint's secret into your server and verify every delivery before processing. Respond with a `2xx` status within 10 seconds; run any heavy processing asynchronously after sending the response.
## Event types
| Type | Fires when |
| - | - |
| `qr.created` | A QR code is created via API or dashboard |
| `qr.updated` | A QR code's name, target, design, tags, or active state changes |
| `qr.deleted` | A QR code is deleted |
| `qr.scanned` | A dynamic QR code is scanned |
| `batch.completed` | A batch job has finished and at least one item was created (`error_count` says how many failed) |
| `batch.failed` | A batch job has finished and no item could be created |
| `ping` | Sent by the test endpoint (`POST /v1/webhooks/{id}/test`) to verify connectivity |
### Event envelope
Every delivery sends a JSON body with this shape:
```json theme={null}
{
"id": "evt_abc123",
"object": "event",
"type": "qr.created",
"created_at": "2026-06-11T12:00:00.000Z",
"data": { }
}
```
`data` is the same object the REST API returns: a QR code (as from
`GET /v1/qr-codes/{id}`, including `domain_id` and, for GS1 codes, `gs1`) for
`qr.*` events, and a batch (`id`, `object`, `total_count`,
`completed_count`, `error_count`, `status`, `created_at`) for `batch.*`
events. If a batch created through the API is interrupted (for example by a
server restart), it is finished after 15 minutes without progress — its
unfinished items are reported as errors — and its `batch.completed` /
`batch.failed` is still sent.
### Scan event privacy
Scan events (`qr.scanned`) never include IP addresses or precise location. The payload carries only coarse, aggregate-grade fields: `qr_code_id`, `scanned_at`, `country`, `city`, `device`, `os`, `browser`, `referrer`, `language`.
Example `qr.scanned` data object:
```json theme={null}
{
"qr_code_id": "qr_abc123",
"scanned_at": "2026-06-11T12:00:00.000Z",
"country": "US",
"city": "New York",
"device": "mobile",
"os": "iOS",
"browser": "Safari",
"referrer": "https://example.com",
"language": "en-US"
}
```
## Verifying signatures
Every delivery includes a `QRKit-Signature` header:
```
QRKit-Signature: t=1718099696,v1=a3b2c1…
```
The `v1` value is `HMAC-SHA256(secret, "{t}.{raw_request_body}")` computed over the **raw** (unparsed) request body. Always verify against the raw bytes before JSON-parsing.
**Reject deliveries where `t` differs from your server clock by more than 5 minutes** to prevent replay attacks. Use a constant-time comparison to avoid timing side-channels.
During the 24-hour window after calling `rotate-secret`, deliveries carry **two** `v1=` signatures (new secret first, then old). Verifying against either one passes — this lets you roll your secret without any downtime.
Additional headers on every delivery:
| Header | Description |
| - | - |
| `QRKit-Event-Id` | The event `id` (use for deduplication) |
| `QRKit-Event-Type` | The event `type` (e.g. `qr.created`) |
```javascript Node.js theme={null}
const crypto = require('crypto')
function verifyQrkitSignature(header, rawBody, secret, toleranceSec = 300) {
const parts = header.split(',')
const t = Number(parts.find(p => p.startsWith('t='))?.slice(2))
if (!Number.isFinite(t)) return false
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return parts.filter(p => p.startsWith('v1=')).some(p => {
const got = Buffer.from(p.slice(3))
const exp = Buffer.from(expected)
return got.length === exp.length && crypto.timingSafeEqual(got, exp)
})
}
```
```python Python theme={null}
import hmac, hashlib, time
def verify_qrkit_signature(header, raw_body, secret, tolerance_sec=300):
parts = header.split(",")
t = next((p[2:] for p in parts if p.startswith("t=")), None)
if t is None or not t.isdigit():
return False
if abs(int(time.time()) - int(t)) > tolerance_sec:
return False
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return any(hmac.compare_digest(p[3:], expected) for p in parts if p.startswith("v1="))
```
## Delivery & retries
A delivery succeeds when your endpoint returns any `2xx` status within 10 seconds. Failed deliveries (non-2xx, timeout, or connection error) are retried on this schedule:
| Attempt | Delay after previous attempt |
| - | - |
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 24 hours |
After 7 failed attempts (\~33 hours total) the delivery is marked `failed` with no further retries.
**Delivery semantics:**
* **At-least-once** — the same event may be delivered more than once. Deduplicate by the event `id` field (also in the `QRKit-Event-Id` header).
* **Ordering not guaranteed** — process events idempotently. If you need authoritative object state, re-fetch via the REST API or the events archive.
**Auto-disable:** An endpoint with sustained failures is automatically disabled and the workspace owner is emailed. Re-enable it once your server is healthy:
```bash theme={null}
curl -X PATCH "https://api.useqrkit.com/v1/webhooks/{id}" \
-H "Authorization: Bearer qr_live_..." \
-H "Content-Type: application/json" \
-d '{"status": "enabled"}'
```
**Permanent unsubscribe:** Respond `410 Gone` to any delivery to immediately disable the endpoint with no further retries.
## Secret rotation
Rotate your endpoint secret without downtime:
```bash theme={null}
curl -X POST "https://api.useqrkit.com/v1/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer qr_live_..."
```
The response returns the new secret **once** — store it immediately. The old secret continues to sign deliveries for 24 hours, so you can update your server before the old key expires.
## Testing
Send a `ping` event through the real delivery pipeline to verify your endpoint is reachable:
```bash theme={null}
curl -X POST "https://api.useqrkit.com/v1/webhooks/{id}/test" \
-H "Authorization: Bearer qr_live_..."
```
The response includes the full delivery result (status code, response body, latency). Inspect past delivery attempts for any endpoint:
```bash theme={null}
curl "https://api.useqrkit.com/v1/webhooks/{id}/deliveries" \
-H "Authorization: Bearer qr_live_..."
```
## Event archive
QRKit stores all events for 30 days regardless of whether any webhook endpoints are registered. Use the archive to recover events your endpoint missed.
```bash theme={null}
# List recent events, optionally filtered by type
curl "https://api.useqrkit.com/v1/events?type=qr.scanned" \
-H "Authorization: Bearer qr_live_..."
# Fetch a single event by ID
curl "https://api.useqrkit.com/v1/events/evt_abc123" \
-H "Authorization: Bearer qr_live_..."
```
The archive only lists events — it does not re-trigger deliveries. To replay an event to your endpoint, re-fetch the object via the REST API and process it in your application.