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