Download OpenAPI specification:
Create, design and manage QR codes from your own code, or connect an AI assistant to your OpenQR account.
OpenQR.io lets you create, update and organise static and dynamic QR codes outside the dashboard. You can work with your account in two ways:
| For | Start here | |
|---|---|---|
| REST API | Your own apps, scripts and integrations | Authentication |
| MCP server | AI assistants such as Claude and ChatGPT | MCP overview |
Both work with the same account, folders and QR codes as the dashboard.
Every active QR code counts towards your plan limit, whether it is static or dynamic, and however it was created. Archiving a code frees its slot. GET /users shows your limit and how many codes you can still create.
Connect Claude, ChatGPT or another AI assistant to your OpenQR.io account, and manage QR codes by asking in plain words: "Create a dynamic QR code for our spring menu in the Restaurant folder", or "How many scans did the flyer code get last month?"
One URL works for every assistant:
https://mcp.openqr.io/mcp
Any registered OpenQR user can connect, on any plan. Your plan limits apply just as they do in the dashboard.
The full list is under MCP tools.
Changes to dynamic codes affect every printed copy. Changing a dynamic code's content or archiving it changes what every printed copy of it does. Assistants are told to confirm with you before doing either.
MCP requests are limited to 120 per minute per account, separately from the REST API.
Assistants can create these types:
| Kind | Types |
|---|---|
| Dynamic | URL, text, call, SMS, email, Wi-Fi, location |
| Static | URL, text, call, SMS, email, location, event, Wi-Fi, vCard |
Landing-page types (such as dynamic vCard, event, links and social) and PDF codes are created in the dashboard.
Logos, frames and design templates are set in the dashboard.
Add OpenQR to Claude as a custom connector. It works on claude.ai, in the Claude desktop app and on mobile, once it has been added.
OpenQR, and the URL https://mcp.openqr.io/mcp. Leave the authentication settings as Claude detects them (OAuth).To use it, turn on OpenQR from the + menu in a chat, then ask, for example: "List my OpenQR folders."
An owner adds the connector once under Organization settings → Connectors, using the same URL. Each member then opens Customize → Connectors, finds OpenQR and clicks Connect to sign in with their own OpenQR account.
See Desktop apps.
Claude's menus change from time to time. If these steps don't match what you see, follow Anthropic's guide: Get started with custom connectors using remote MCP.
ChatGPT connects to MCP servers as plugins, on the web at chatgpt.com. You no longer need to turn on developer mode first.
OpenQR, and under Connection enter the server URL https://mcp.openqr.io/mcp.@, pick OpenQR and ask, for example: "How many QR codes can I still create?"ChatGPT asks you to confirm each action that changes your account, such as creating or archiving a code, before it runs.
On Business, Enterprise and Edu workspaces, adding a custom MCP server needs the Create plugins with MCPs permission. On Business workspaces only admins and owners have it by default, so ask an admin to add OpenQR if you don't see Add custom MCP server.
ChatGPT's menus change often. If these steps don't match what you see, follow OpenAI's guide: Add custom MCP server.
Desktop apps and code editors connect with the same URL. Each one opens your browser so you can sign in to OpenQR and click Allow.
claude mcp add --transport http openqr https://mcp.openqr.io/mcp
Then run /mcp in Claude Code, pick openqr and sign in.
Add OpenQR to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):
{
"mcpServers": {
"openqr": {
"url": "https://mcp.openqr.io/mcp"
}
}
}
Cursor shows a Needs login prompt next to the server. Click it to sign in.
Run MCP: Add Server from the Command Palette, choose HTTP, and enter https://mcp.openqr.io/mcp. Or add it to .vscode/mcp.json:
{
"servers": {
"openqr": {
"type": "http",
"url": "https://mcp.openqr.io/mcp"
}
}
}
VS Code asks you to sign in the first time the server starts.
Connectors added on claude.ai are also available in the desktop app. See Connect Claude.
Any MCP client that supports the Streamable HTTP transport can connect to:
https://mcp.openqr.io/mcp
The server follows the MCP authorization spec:
/oauth/register, so no client ID needs to be issued in advance.S256 only.mcp:use. It is also the default when a client sends no scope.https://mcp.openqr.io/.well-known/oauth-protected-resource and https://mcp.openqr.io/.well-known/oauth-authorization-server.Allowed redirect URIs:
| Client | Redirect URI |
|---|---|
| Apps on the user's computer | http://localhost, http://127.0.0.1 or http://[::1] (any port), or the cursor:// and vscode:// schemes |
| Hosted apps | Only Claude (claude.ai, claude.com) and ChatGPT (chatgpt.com) |
Authorization from any other host is refused with "This app is not supported yet". To ask for your hosted app to be added, email [email protected].
Clients that can't sign in with OAuth can use an API key from the dashboard's API Keys page (Authentication). Send it in either header:
Authorization: Bearer YOUR_API_KEY
X-API-Key: YOUR_API_KEY
An API key gives full API access to your account and doesn't expire on its own, so prefer OAuth when the client supports it. Revoke the key in the dashboard to cut off access.
The OpenQR MCP server provides these tools. Assistants choose a tool from what you ask, so you don't need to name them.
| Tool | What it does | Changes data |
|---|---|---|
list_qr_codes |
Lists QR codes, newest first, 20 per page. Filter by folder, or show archived codes. | No |
get_qr_code |
One QR code with its type, folder and image URL. | No |
list_qr_types |
The supported types and the data fields each one needs. | No |
create_qr_code |
Creates a QR code, optionally in a folder. Counts towards your plan limit. | Yes |
update_qr_code |
Renames a code, moves it to another folder, changes its content, or restyles it (colors, module, finder, finder dot). | Yes |
archive_qr_code |
Archives a code and frees its plan slot. Restore it from the dashboard. | Yes |
preview_qr_code |
A link to the SVG image, and the exact content the code encodes. | No |
get_qr_code_stats |
Scans for a dynamic code: the all-time total, plus daily, country, city and OS breakdowns for one month. | No |
list_folders |
Lists folders, 20 per page. | No |
create_folder |
Creates a folder. The name must be 5–255 characters. | Yes |
rename_folder |
Renames a folder. | Yes |
get_plan_usage |
Your plan, codes used, the limit and codes remaining. | No |
Tools that read data are marked read-only, so clients can run them without asking. Clients usually ask you before running a tool that changes data.
| What you see | Why | What to do |
|---|---|---|
| "This app is not supported yet" | The assistant isn't on the list of approved hosted apps. | Email [email protected] to ask for it to be added. |
| "This connection request has expired" | Sign-in took more than 30 minutes, or Allow was clicked twice. | Go back to the assistant and connect again. |
| A plan limit message when creating a code | Every active code counts, static or dynamic. | Archive codes you no longer need, or upgrade your plan. |
| "Another QR code is still being created" | Several codes were being created at the same time. | Try again in a moment. |
| The assistant asks you to reconnect | The connection was removed under Integrations, or wasn't used for 60 days. | Connect again. |
Too many requests (429) |
More than 120 requests in a minute. | Wait a minute, then continue. |
| The assistant can't create a dynamic vCard, dynamic event, links, social or PDF code | Those types use a landing page or an uploaded file. | Create them in the dashboard. |
Still stuck? Email [email protected] with the assistant's name and the message you saw.
Every request needs an API key, sent as a bearer token:
Authorization: Bearer YOUR_API_KEY

Copy the key when it is created. OpenQR does not store it and cannot show it again. Treat it like a password: keep it on your server and never put it in front-end code or a public repository.
| Status | Meaning |
|---|---|
401 |
The key is missing, wrong or revoked. |
403 |
The key is valid but has no API access, or the item belongs to another account. |
The same key also works with the MCP server for AI clients that cannot sign in with OAuth.
The OpenQR API is a REST API: every request and response body is JSON, and results are reported with standard HTTP status codes.
https://api.openqr.io/api/v1
Accept: application/json with every request. Without it, errors are not returned as JSON.Content-Type: application/json. The one exception is file uploads, which use multipart/form-data.POST to the item's URL, for example POST /qr-codes/{qr_code_id}.Lists return 20 items per page.
meta.next_cursor as the cursor query parameter, or follow links.next. When next_cursor is null, you are on the last page.page=2, page=3 and so on, or follow links.next.Errors return an HTTP status code and a JSON body with a message:
{
"message": "The name field is required."
}
| Status | When |
|---|---|
400 |
Validation failed. message describes the first invalid field. |
401 |
The API key is missing, wrong or revoked. |
403 |
Your plan limit is reached, the key has no API access, or the item belongs to another account. |
404 |
The item does not exist. |
409 |
Another QR code is still being created in your account. Retry after a moment. |
429 |
Too many requests. Wait before retrying. |
500 |
Something went wrong on our side. Retry later, and contact [email protected] if it keeps happening. |
Limits are counted per account, per minute.
| Requests | Limit |
|---|---|
| All API requests (your dashboard session counts too) | 100 per minute |
| Create, preview, preview and save, and update QR code, combined | 60 per minute |
| MCP server requests (counted separately) | 120 per minute |
When you go over a limit you get 429. Wait until the minute has passed, then retry.
Every active QR code counts towards your plan limit, whether it is static or dynamic. When the limit is reached, creating a code returns 403. Archive codes you no longer need, or upgrade your plan in the dashboard. GET /users shows qr_codes_limit and qr_codes_remaining.
Create a folder to organise your QR codes.
Folder to save
| name required | string [ 5 .. 255 ] characters Folder name. |
{- "name": "Marketing"
}{- "data": {
- "id": 6,
- "name": "Marketing",
- "created_at": "2026-07-31T19:32:06.000000Z"
}
}List your folders, 20 per page.
| cursor | string Cursor for the page to fetch, from |
use GuzzleHttp\Client; $client = new Client(); $response = $client->request('GET', 'https://api.openqr.io/api/v1/folders', [ 'headers' => [ 'Authorization' => 'Bearer api_key', 'Accept' => 'application/json', ] ]); echo $response->getBody()->getContents();
{- "data": [
- {
- "id": 6,
- "name": "Marketing",
- "created_at": "2026-07-31T19:32:06.000000Z"
}
], - "links": {
- "first": null,
- "last": null,
- "prev": null,
}, - "meta": {
- "per_page": 20,
- "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyMy0wNy0zMCAxMDoyNjoxMCIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
- "prev_cursor": null
}
}Change a folder's name.
| folder_id required | integer Folder ID. |
Folder to save
| name required | string [ 5 .. 255 ] characters Folder name. |
{- "name": "Marketing"
}{- "data": {
- "id": 6,
- "name": "Marketing",
- "created_at": "2026-07-31T19:32:06.000000Z"
}
}Create a QR code. Every active code counts towards your plan limit; when the limit is reached you get 403.
To show the user the image before saving, use Preview QR code and then Save previewed QR code.
QR code to create
| name required | string [ 5 .. 255 ] characters QR code name, shown in the dashboard. |
| type required | string (QRCodeType) Enum: "url" "url_static" "text" "text_static" "call" "call_static" "sms" "sms_static" "email" "email_static" "wifi" "wifi_static" "geo" "geo_static" "event" "event_static" "vcard" "vcard_static" QR code type. Types ending in Dynamic |
required | URL (url, url_static) (object) or Text (text, text_static) (object) or Call (call, call_static) (object) or SMS (sms, sms_static) (object) or Email (email, email_static) (object) or Wi-Fi (wifi, wifi_static) (object) or Location (geo, geo_static) (object) or Event (event_static) (object) or vCard (vcard_static) (object) or Landing page (event, vcard) (object) Content of the code. The fields depend on |
| qr_code_folder_id | integer or null ID of one of your folders to put the code in. |
| domain_id | integer or null ID of one of your custom domains, used for a dynamic code's short link. Leave it out to use the default domain. |
| qr_page_id | integer or null ID of a landing page built in the dashboard. Required for dynamic |
object (QRCodeDesign) How the QR code looks. Every field is optional. Colors are hex values such as |
{- "name": "Spring menu",
- "type": "url",
- "qr_code_folder_id": 6,
- "design": {
- "foreground_color": "#1f2937",
- "module": "dots",
- "finder": "rounded-corners"
}
}{- "data": {
- "id": "km0yYX0",
- "name": "Demo",
- "status": "public",
- "dynamic": true,
- "qr_code_folder_id": 6,
- "domain_id": null,
- "type": "url",
- "created_at": "2026-07-30T10:26:10.000000Z",
- "updated_at": "2026-07-30T10:26:10.000000Z"
}
}List your QR codes, newest first, 20 per page.
| status | string Default: "public" Enum: "public" "archived"
|
| folder_id | integer Only return codes in this folder. |
| cursor | string Cursor for the page to fetch, from |
use GuzzleHttp\Client; $client = new Client(); $response = $client->request('GET', 'https://api.openqr.io/api/v1/qr-codes', [ 'headers' => [ 'Authorization' => 'Bearer api_key', 'Accept' => 'application/json', ] ]); echo $response->getBody()->getContents();
{- "data": [
- {
- "id": "km0yYX0",
- "name": "Demo",
- "status": "public",
- "dynamic": true,
- "qr_code_folder_id": 6,
- "domain_id": null,
- "type": "url",
- "created_at": "2026-07-30T10:26:10.000000Z",
- "updated_at": "2026-07-30T10:26:10.000000Z"
}
], - "links": {
- "first": null,
- "last": null,
- "prev": null,
}, - "meta": {
- "per_page": 20,
- "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyMy0wNy0zMCAxMDoyNjoxMCIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
- "prev_cursor": null
}
}Get the SVG image of a QR code without saving it. Takes the same body as Create QR code.
A dynamic code's image contains its future short link. The ID behind that link is kept for your account and the code's type, so repeated previews show the same image. To save the code with exactly that link, use Save previewed QR code.
QR code to create
| name required | string [ 5 .. 255 ] characters QR code name, shown in the dashboard. |
| type required | string (QRCodeType) Enum: "url" "url_static" "text" "text_static" "call" "call_static" "sms" "sms_static" "email" "email_static" "wifi" "wifi_static" "geo" "geo_static" "event" "event_static" "vcard" "vcard_static" QR code type. Types ending in Dynamic |
required | URL (url, url_static) (object) or Text (text, text_static) (object) or Call (call, call_static) (object) or SMS (sms, sms_static) (object) or Email (email, email_static) (object) or Wi-Fi (wifi, wifi_static) (object) or Location (geo, geo_static) (object) or Event (event_static) (object) or vCard (vcard_static) (object) or Landing page (event, vcard) (object) Content of the code. The fields depend on |
| qr_code_folder_id | integer or null ID of one of your folders to put the code in. |
| domain_id | integer or null ID of one of your custom domains, used for a dynamic code's short link. Leave it out to use the default domain. |
| qr_page_id | integer or null ID of a landing page built in the dashboard. Required for dynamic |
object (QRCodeDesign) How the QR code looks. Every field is optional. Colors are hex values such as |
{- "name": "Spring menu",
- "type": "url",
- "qr_code_folder_id": 6,
- "design": {
- "foreground_color": "#1f2937",
- "module": "dots",
- "finder": "rounded-corners"
}
}{- "message": "string"
}Save a QR code you previewed with Preview QR code, keeping the ID and short link shown in the preview, so an image you already showed or printed stays valid. Takes the same body as Create QR code, and counts towards your plan limit the same way.
QR code to create
| name required | string [ 5 .. 255 ] characters QR code name, shown in the dashboard. |
| type required | string (QRCodeType) Enum: "url" "url_static" "text" "text_static" "call" "call_static" "sms" "sms_static" "email" "email_static" "wifi" "wifi_static" "geo" "geo_static" "event" "event_static" "vcard" "vcard_static" QR code type. Types ending in Dynamic |
required | URL (url, url_static) (object) or Text (text, text_static) (object) or Call (call, call_static) (object) or SMS (sms, sms_static) (object) or Email (email, email_static) (object) or Wi-Fi (wifi, wifi_static) (object) or Location (geo, geo_static) (object) or Event (event_static) (object) or vCard (vcard_static) (object) or Landing page (event, vcard) (object) Content of the code. The fields depend on |
| qr_code_folder_id | integer or null ID of one of your folders to put the code in. |
| domain_id | integer or null ID of one of your custom domains, used for a dynamic code's short link. Leave it out to use the default domain. |
| qr_page_id | integer or null ID of a landing page built in the dashboard. Required for dynamic |
object (QRCodeDesign) How the QR code looks. Every field is optional. Colors are hex values such as |
{- "name": "Spring menu",
- "type": "url",
- "qr_code_folder_id": 6,
- "design": {
- "foreground_color": "#1f2937",
- "module": "dots",
- "finder": "rounded-corners"
}
}{- "data": {
- "id": "km0yYX0",
- "name": "Demo",
- "status": "public",
- "dynamic": true,
- "qr_code_folder_id": 6,
- "domain_id": null,
- "type": "url",
- "created_at": "2026-07-30T10:26:10.000000Z",
- "updated_at": "2026-07-30T10:26:10.000000Z"
}
}Change a QR code's name, content, folder, domain or design. The type can't be changed.
The update replaces the code's settings. Send qr_code_folder_id, domain_id and design again with every update, or they are cleared.
Changing a dynamic code's content changes what every printed copy opens. Changing a static code's content changes the image, so printed copies keep the old content.
| qr_code_id required | string Examples: km0yYX0 QR code ID. |
New values for the QR code. The type can't be changed.
| name required | string <= 255 characters QR code name. |
required | URL (url, url_static) (object) or Text (text, text_static) (object) or Call (call, call_static) (object) or SMS (sms, sms_static) (object) or Email (email, email_static) (object) or Wi-Fi (wifi, wifi_static) (object) or Location (geo, geo_static) (object) or Event (event_static) (object) or vCard (vcard_static) (object) or Landing page (event, vcard) (object) Content of the code, with the fields of the code's type. See Create QR code. |
| qr_code_folder_id | integer or null ID of one of your folders, or |
| domain_id | integer or null ID of one of your custom domains, or |
| qr_page_id | integer or null ID of the landing page, for dynamic |
object (QRCodeDesign) How the QR code looks. Every field is optional. Colors are hex values such as |
{- "name": "Spring menu",
- "qr_code_folder_id": 6,
- "design": {
- "foreground_color": "#1f2937",
- "module": "dots",
- "finder": "rounded-corners"
}
}{- "data": {
- "id": "km0yYX0",
- "name": "Demo",
- "status": "public",
- "dynamic": true,
- "qr_code_folder_id": 6,
- "domain_id": null,
- "type": "url",
- "created_at": "2026-07-30T10:26:10.000000Z",
- "updated_at": "2026-07-30T10:26:10.000000Z"
}
}Get one QR code.
| qr_code_id required | string Examples: km0yYX0 QR code ID. |
use GuzzleHttp\Client; $client = new Client(); $response = $client->request('GET', 'https://api.openqr.io/api/v1/qr-codes/{qr_code_id}', [ 'headers' => [ 'Authorization' => 'Bearer api_key', 'Accept' => 'application/json', ] ]); echo $response->getBody()->getContents();
{- "data": {
- "id": "km0yYX0",
- "name": "Demo",
- "status": "public",
- "dynamic": true,
- "qr_code_folder_id": 6,
- "domain_id": null,
- "type": "url",
- "created_at": "2026-07-30T10:26:10.000000Z",
- "updated_at": "2026-07-30T10:26:10.000000Z"
}
}Archive a QR code. It no longer counts towards your plan limit, and it appears under status=archived in List QR codes. Restore it from the dashboard.
An archived dynamic code stops redirecting, so every printed copy stops working. Archiving a code that is already archived does nothing.
| qr_code_id required | string Examples: km0yYX0 QR code ID. |
{- "data": {
- "id": "km0yYX0",
- "name": "Demo",
- "status": "public",
- "dynamic": true,
- "qr_code_folder_id": 6,
- "domain_id": null,
- "type": "url",
- "created_at": "2026-07-30T10:26:10.000000Z",
- "updated_at": "2026-07-30T10:26:10.000000Z"
}
}Upload a logo image to use on your QR codes. Use the returned id as design.logo_id.
Logo image to upload
| file required | string <binary> The logo image. Use a PNG or JPEG image, ideally square. |
use GuzzleHttp\Client; $client = new Client(); $response = $client->request('POST', 'https://api.openqr.io/api/v1/files/qr-logos', [ 'headers' => [ 'Authorization' => 'Bearer api_key', 'Accept' => 'application/json', ], 'multipart' => [ [ 'name' => 'file', 'filename' => 'logo.png', 'contents' => fopen('/path/to/logo.png', 'r'), ], ], ] ); echo $response->getBody()->getContents();
{- "data": {
- "id": "XzC1zPwH",
- "name": "logo.png",
- "mime_type": "image/png",
- "public": false
}
}List the logos you can use on QR codes, 20 per page. This includes the logos you uploaded and the ones OpenQR provides (public is true).
| page | integer >= 1 Default: 1 Page number. |
use GuzzleHttp\Client; $client = new Client(); $response = $client->request('GET', 'https://api.openqr.io/api/v1/files/qr-logos', [ 'headers' => [ 'Authorization' => 'Bearer api_key', 'Accept' => 'application/json', ] ]); echo $response->getBody()->getContents();
{- "data": [
- {
- "id": "XzC1zPwH",
- "name": "logo.png",
- "mime_type": "image/png",
- "public": false
}
], - "links": {
- "last": null,
- "prev": null,
}, - "meta": {
- "current_page": 1,
- "from": 1,
- "to": 20,
- "per_page": 20
}
}Get your account's plan, how many QR codes it allows, and how many you can still create. Check qr_codes_remaining before creating several codes.
{- "account_trial_ends_at": "2026-10-21T18:31:32.000000Z",
- "account_on_trial": false,
- "created_qr_codes_count": 3,
- "qr_codes_limit": 60,
- "qr_codes_remaining": 57,
- "subscription": {
- "id": 1,
- "subscription_active": true,
- "on_trail": false,
- "on_grace": false,
- "plan_id": 1,
- "trial_ends_at": null
}, - "plan": {
- "id": 1,
- "name": "Pro",
- "number_of_qr_codes": 50,
- "number_of_scans": 0
}
}