Developer API

Create, update and track your QR codes straight from your own system, without opening the dashboard. The API is REST, answers in JSON and is available on the Business plan.

Overview

Every route starts at https://qrstream.pro/api/v1. You send JSON, you get JSON back, and each request authenticates with an API key — there are no sessions and no cookies.

API access is part of the Business plan. You create your keys in your account, in the “API” section of /account#api. A key is shown once, when it is created: copy it then and keep it in a secret store. If you lose it, revoke it and create another.

What to know up front

  • Every key starts with qrs_ and belongs to your account, not to one code or folder.
  • You can hold up to 10 active keys at a time.
  • Send data as Content-Type: application/json, encoded in UTF-8.
  • Dates are ISO 8601 in UTC, for example 2026-09-12T07:41:02.000Z.
  • Code and folder identifiers are UUIDs.

Authentication

Send the key in the Authorization header. The Bearer form is the recommended one; x-api-key works exactly the same and helps where your client cannot set Authorization.

Authorization: Bearer qrs_9f3a1c7b2e…
x-api-key: qrs_9f3a1c7b2e…

Keys are for server-to-server calls. Never put one in a web page, a mobile app or a public repository: whoever holds the key can read and change all of your codes. For the same reason the API sends no CORS headers, so a browser cannot call it directly.

Permissions

You choose what a key can do when you create it. A read-only key answers GET requests and refuses every write with forbidden. A read and write key can create, edit and delete codes and folders.

PermissionCanCannot
Read onlyGET on codes, folders, analytics and /mePOST, PATCH, DELETE
Read and writeEverything read-only can do, plus create, edit, pause and deleteManage keys or the subscription

The right origin

Always call https://qrstream.pro/api/v1. If you use a custom domain for your dynamic codes, that domain only serves scan redirects and forwards API requests on with a redirect — some libraries drop the authorization header across a redirect, and the request then arrives unauthenticated.

Rate limits

Each key gets 120 requests per minute. The limit is counted per key, not per account, so you can split integrations across keys and keep one from blocking another.

Every response tells you where you stand. Over the limit you get a 429 with Retry-After, the number of seconds to wait before trying again.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
Retry-After: 27
HeaderMeans
X-RateLimit-LimitRequests allowed in a minute (120).
X-RateLimit-RemainingHow many are left in the current minute.
Retry-AfterSeconds to wait, sent only with a 429 response.

For large imports, space your requests out and back off with a growing delay on both 429 and 500.

Errors

Every 4xx and 5xx response uses the same envelope: an error object with a stable code, a message in English for your logs and, on validation, the list of problems found. Always branch on code, never on the message text — wording can change.

{
  "error": {
    "code": "validation_failed",
    "message": "The request could not be understood.",
    "details": [
      { "path": "content.url", "message": "An address is required" }
    ]
  }
}
CodeStatusMeans
unauthorized401Key missing, wrong, disabled or expired.
forbidden403The key lacks the permission asked for, usually a write with a read-only key.
plan_required403The account has no API access. Move to the Business plan.
account_suspended403The account is suspended. Get in touch and we will reopen it.
quota_exceeded403A plan limit was reached, for example the number of dynamic codes.
not_found404The resource does not exist, or is not yours. The two look alike on purpose.
conflict409A folder with that name already exists. Pick another and send the request again.
validation_failed422The request body is not valid; details lists the fields.
rate_limited429You went over the 120 requests a minute.
render_failed500The image could not be rendered. Retry, or ask for the SVG.
internal500Something broke on our side. Retry; if it keeps happening, tell us.

Pagination

Lists are walked with a cursor rather than page numbers, so you never skip a row when codes are added while you are reading.

ParameterValuesDefault
limit1–10025
cursorThe nextCursor from the previous response
{
  "data": [],
  "nextCursor": "MjAyNi0wOC0xNFQwOToxMjo0NFp8N2YxYzBiNTI"
}

A null nextCursor means you have reached the end. Send it back as ?cursor=… to ask for the next page.

Your account

GET /me

Tells you who you are, which plan you are on and how much of your limits you have used. It works with any valid key and is the simplest call for checking a new one.

limits holds the plan's ceilings and usage what you have spent so far. A null in limits means “no ceiling”, not zero.

curl -s "https://qrstream.pro/api/v1/me" \
  -H "Authorization: Bearer $QRS_API_KEY"
{
  "user": {
    "id": "u_3kf9x7b2qd",
    "name": "Ana Popescu",
    "email": "ana@example.com"
  },
  "plan": {
    "slug": "business",
    "name": "Business",
    "source": "subscription",
    "periodEnd": "2026-10-01T00:00:00.000Z"
  },
  "limits": {
    "dynamicCodes": null,
    "scansPerMonth": null,
    "analyticsMonths": 36,
    "logo": true,
    "csvImport": true,
    "customDomain": true,
    "googleAnalytics": true,
    "users": null,
    "storageBins": null,
    "binPhotos": 40,
    "rackLabels": true,
    "apiAccess": true
  },
  "usage": {
    "dynamicCodes": 64,
    "storageBins": 3,
    "scansThisMonth": 12840
  },
  "key": {
    "id": "k_7d2a1f",
    "permissions": {
      "codes": [
        "read",
        "write"
      ],
      "analytics": [
        "read"
      ],
      "folders": [
        "read",
        "write"
      ]
    }
  }
}

Codes

GET /codes

Lists the account's codes, newest first. Filters combine: type (the code type), status (active or paused), folderId, and q to search by name.

curl -s "https://qrstream.pro/api/v1/codes?limit=25&type=website&status=active" \
  -H "Authorization: Bearer $QRS_API_KEY"
{
  "data": [
    {
      "id": "7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33",
      "type": "website",
      "name": "Summer menu",
      "dynamic": true,
      "status": "active",
      "shortUrl": "https://qrstream.pro/r/7Kd2mA",
      "scanCount": 1284,
      "createdAt": "2026-08-14T09:12:44.000Z"
    }
  ],
  "nextCursor": null
}

POST /codes

Creates a code and answers 201. The required fields are type, name and content; content must carry the same type as the code. design is optional — without it the code gets the default design.

dynamic is optional and defaults to false. For the types that only exist as static or only as dynamic it is ignored: the code gets the form its type requires either way.

FieldTypeDescription
typetextThe code type: website, wifi, vcard, pdf, whatsapp and the rest.
dynamicbooleanOptional, false by default. true creates an editable short link; false writes the content into the modules.
nametextThe name in the dashboard, 1–120 characters.
contentobjectThe content, matching the type. See the section below.
designobjectOptional: colours, shapes, logo, frame, correction level.
folderIduuid or nullThe folder the code goes into.
passwordtext or nullPassword for the scan page; dynamic codes only.
expirytextnone, 30d or date.
expiresOndate or nullThe expiry day, required when expiry is date.
curl -s -X POST "https://qrstream.pro/api/v1/codes" \
  -H "Authorization: Bearer $QRS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "website",
    "dynamic": true,
    "name": "Summer menu",
    "content": { "type": "website", "url": "https://example.com/menu" },
    "folderId": null,
    "expiry": "none"
  }'
{
  "id": "7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33",
  "type": "website",
  "name": "Summer menu",
  "dynamic": true,
  "status": "active",
  "shortUrl": "https://qrstream.pro/r/7Kd2mA",
  "payload": "https://qrstream.pro/r/7Kd2mA",
  "target": "https://example.com/menu",
  "content": { "type": "website", "url": "https://example.com/menu" },
  "folderId": null,
  "hasPassword": false,
  "expiresAt": null,
  "scanCount": 0,
  "createdAt": "2026-09-12T07:41:02.000Z"
}

GET /codes/{id}

One code, with every field. An unknown id, or one from another account, answers 404.

curl -s "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33" \
  -H "Authorization: Bearer $QRS_API_KEY"

PATCH /codes/{id}

Only the fields you send are changed. The content of a static code cannot change after it is created — the modules are already printed — but the name, the design, the folder and the password can.

curl -s -X PATCH "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33" \
  -H "Authorization: Bearer $QRS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer menu 2026",
    "content": { "type": "website", "url": "https://example.com/menu?v=2" },
    "folderId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
  }'

DELETE /codes/{id}

Answers 204, with no body. The delete is soft: the code leaves the dashboard and the list, and a dynamic code stops redirecting.

curl -s -i -X DELETE "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33" \
  -H "Authorization: Bearer $QRS_API_KEY"

POST /codes/{id}/pause and /resume

Stops or restarts a dynamic code without deleting it. The response is the updated code, with status paused or active.

curl -s -X POST "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33/pause" \
  -H "Authorization: Bearer $QRS_API_KEY"

curl -s -X POST "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33/resume" \
  -H "Authorization: Bearer $QRS_API_KEY"

The code object

FieldTypeDescription
iduuidThe code's identifier.
typetextThe code type.
nametextThe name in the dashboard.
dynamicbooleanWhether it goes through a short link.
statustextactive or paused.
shortUrltext or nullThe short link; null on static codes.
payloadtextWhat is actually written into the code's modules.
targettext or nullThe final destination of a dynamic code.
contentobjectThe structured content, carrying its own type.
designobjectThe code's full design.
folderIduuid or nullThe folder it sits in.
hasPasswordbooleanWhether the scan page asks for a password. The password itself is never returned.
expiresAtdate or nullWhen it expires.
campaignStartdate or nullStart of the campaign.
campaignEnddate or nullEnd of the campaign.
printRuntext or nullThe print run note.
scanCountnumberTotal scans.
lastScanAtdate or nullThe last scan.
brokenLinkbooleanThe destination did not answer on the last check.
createdAtdateWhen it was created.
updatedAtdateThe last change.

Content, by type

The content object carries its own type, the same one as the code. The most used types are below; the rest are described in the OpenAPI document.

website

{ "type": "website", "url": "https://example.com/menu" }

wifi

encryption can be WPA, WEP or nopass. Leave password empty for an open network.

{
  "type": "wifi",
  "ssid": "Cafe guest",
  "password": "0000pass",
  "encryption": "WPA",
  "hidden": false
}

vcard

Every field is optional, but a card with none of them has nothing to show.

{
  "type": "vcard",
  "first": "Ana",
  "last": "Popescu",
  "phone": "+40 721 000 000",
  "email": "ana@example.com",
  "org": "The Green Cafe",
  "title": "Manager",
  "url": "https://example.com/menu"
}

whatsapp

Send the number in international format; message is the pre-filled message.

{
  "type": "whatsapp",
  "phone": "+40721000000",
  "message": "Hi! I would like to book a table."
}

pdf

fileUrl is the address of a file you have already uploaded. File uploads are not part of v1 — upload the document from the dashboard and reuse its address.

{
  "type": "pdf",
  "fileName": "menu.pdf",
  "fileUrl": "https://blob.qrstream.pro/…/menu.pdf"
}

Images

Every code has two image routes. Both take size between 128 and 2048 pixels, 1024 by default, and need read permission on codes.

RouteContent typeGood for
GET /codes/{id}/image.svgimage/svg+xmlPrint and any size; vector, no loss.
GET /codes/{id}/image.pngimage/pngScreens, emails, documents.
curl -s "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33/image.svg?size=1024" \
  -H "Authorization: Bearer $QRS_API_KEY" -o cod.svg

curl -s "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33/image.png?size=512" \
  -H "Authorization: Bearer $QRS_API_KEY" -o cod.png

On PNG the logo is fetched and embedded by the server before rendering, so it does appear in the image. Frame text is drawn with a system font rather than the brand font, so it can look slightly different from the dashboard preview. When you need it exact, ask for the SVG.

Analytics

GET /codes/{id}/analytics

One code's scans, by day. range can be 7, 30 or 90 days, 30 by default. Dates are ISO and the values in breakdowns are raw — operating-system keys and ISO country codes — so you can group them yourself.

The country and city lists in breakdowns are cut to the busiest 50 values, largest first. Everything past that is still counted in totals.

curl -s "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33/analytics?range=30" \
  -H "Authorization: Bearer $QRS_API_KEY"
{
  "range": 30,
  "code": {
    "id": "7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33",
    "name": "Summer menu",
    "type": "website",
    "scanCount": 1284,
    "lastScanAt": "2026-08-15T17:02:11.000Z"
  },
  "totals": { "scans": 1284, "unique": 902 },
  "series": [
    { "date": "2026-08-14", "scans": 42, "unique": 31 },
    { "date": "2026-08-15", "scans": 57, "unique": 40 }
  ],
  "breakdowns": {
    "os": [
      { "key": "ios", "value": 812 },
      { "key": "android", "value": 401 }
    ],
    "country": [{ "key": "RO", "value": 1180 }],
    "city": [{ "key": "Bucharest", "value": 640 }]
  }
}

GET /codes/{id}/scans.csv

The same scans, row by row, as text/csv. The response is streamed, so it works for codes with a lot of scans too.

curl -s "https://qrstream.pro/api/v1/codes/7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33/scans.csv" \
  -H "Authorization: Bearer $QRS_API_KEY" -o scanari.csv

GET /analytics

The same picture for the whole account: totals, the daily series and the most scanned codes. How far back you can ask depends on your plan.

curl -s "https://qrstream.pro/api/v1/analytics?range=90" \
  -H "Authorization: Bearer $QRS_API_KEY"
{
  "range": 90,
  "totals": { "scans": 41280, "unique": 28904 },
  "series": [{ "date": "2026-06-14", "scans": 402, "unique": 311 }],
  "topCodes": [
    { "id": "7f1c0b52-1b7e-4a1e-9f0a-2d5b6c8e1a33", "name": "Summer menu", "type": "website", "scans": 1284 }
  ],
  "breakdowns": {
    "os": [{ "key": "ios", "value": 24310 }],
    "country": [{ "key": "RO", "value": 39002 }],
    "city": [{ "key": "Bucharest", "value": 18770 }]
  }
}

Folders

Folders group codes in the dashboard. A folder without a scope is an ordinary folder; scope can be storage for bins or location for rack labels. Deleting a folder does not delete the codes inside it.

RoutePermissionDoes
GET /foldersfolders: readEvery folder, with its code count.
POST /foldersfolders: writeCreates a folder; answers 201.
PATCH /folders/{id}folders: writeRenames the folder.
DELETE /folders/{id}folders: writeDeletes the folder; answers 204.
curl -s "https://qrstream.pro/api/v1/folders" \
  -H "Authorization: Bearer $QRS_API_KEY"

curl -s -X POST "https://qrstream.pro/api/v1/folders" \
  -H "Authorization: Bearer $QRS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Campaigns", "scope": null }'

curl -s -X PATCH "https://qrstream.pro/api/v1/folders/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
  -H "Authorization: Bearer $QRS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Campaigns 2026" }'

curl -s -i -X DELETE "https://qrstream.pro/api/v1/folders/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
  -H "Authorization: Bearer $QRS_API_KEY"
{
  "data": [
    {
      "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Campaigns",
      "color": null,
      "scope": null,
      "createdAt": "2026-08-14T09:12:44.000Z",
      "updatedAt": "2026-08-14T09:12:44.000Z"
    }
  ]
}

OpenAPI

The full description of the API is published as an OpenAPI 3.1 document at https://qrstream.pro/api/v1/openapi.json. It is public, needs no key, and you can feed it to a client generator or open it in Postman or Insomnia.

curl -s https://qrstream.pro/api/v1/openapi.json -o openapi.json

That document is the source of truth for the content and design schemas of every code type. This page only covers what you reach for most.

MCP server for AI agents

MCP (Model Context Protocol) is how an AI agent discovers the tools it can use and then calls them on its own. Our MCP server exposes the same operations as the REST API — one tool per endpoint — and works on the account the key belongs to.

The server address

POST https://qrstream.pro/api/mcp
Authorization: Bearer qrs_9f3a1c7b2e…

The server needs the Business plan, like the REST API, and spends the same 120 requests a minute the key has. Connecting a client costs 3 requests, because the agent runs the handshake first: initialize, notifications/initialized and tools/list.

Claude Code

claude mcp add --transport http qrstream https://qrstream.pro/api/mcp --header "Authorization: Bearer qrs_YOUR_KEY"

Cursor — .cursor/mcp.json

{
  "mcpServers": {
    "qrstream": {
      "url": "https://qrstream.pro/api/mcp",
      "headers": {
        "Authorization": "Bearer qrs_YOUR_KEY"
      }
    }
  }
}

Claude Desktop — claude_desktop_config.json

{
  "mcpServers": {
    "qrstream": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://qrstream.pro/api/mcp",
        "--header",
        "Authorization: Bearer ${QRSTREAM_KEY}"
      ],
      "env": {
        "QRSTREAM_KEY": "qrs_YOUR_KEY"
      }
    }
  }
}

Custom connectors in claude.ai require OAuth, so this server, which authenticates with an API key, connects from Claude Code, Cursor, VS Code and Claude Desktop (through mcp-remote --header). OAuth stays a possible later phase.

ToolAccessWhat it does
qrstream_get_accountany keyTells you which plan you are on, how much of your limits you have used and what the key may do.
qrstream_list_codescodes:readLists the account's codes, with filters by type, status, folder and name.
qrstream_get_codecodes:readReturns a single code, with every one of its fields.
qrstream_create_codecodes:writeCreates a new code, static or dynamic, from the content you give it.
qrstream_update_codecodes:writeChanges a code's name, content, design, folder or password.
qrstream_delete_codecodes:writeDeletes a code; a dynamic code stops redirecting once it is gone.
qrstream_pause_codecodes:writePauses a dynamic code without deleting it.
qrstream_resume_codecodes:writeRestarts a paused code.
qrstream_get_code_image_svgcodes:readReturns the code's image as an SVG document, the one to use for print.
qrstream_get_code_image_pngcodes:readReturns the code's image as a PNG, for screens, emails and documents.
qrstream_get_code_analyticsanalytics:readOne code's scans over 7, 30 or 90 days, with totals and breakdowns.
qrstream_get_code_scansanalytics:readOne code's scans, row by row, as CSV text.
qrstream_get_account_analyticsanalytics:readThe same picture for the whole account: totals, the daily series and the most scanned codes.
qrstream_list_foldersfolders:readLists the account's folders, with the number of codes in each.
qrstream_create_folderfolders:writeCreates a new folder, an ordinary one or one for bins or rack labels.
qrstream_rename_folderfolders:writeRenames an existing folder.
qrstream_delete_folderfolders:writeDeletes a folder; the codes inside it stay, but leave the folder.
  • SVG and PNG images go up to 1024 pixels; for anything larger use the REST API's image routes.
  • The scan list stops at 500 rows; take the full export from the REST API's CSV route.
  • Lists are walked with a cursor: when a response carries nextCursor, send it back to ask for the next page.
  • Write tools refuse a read-only key with a clear message, which the agent shows you.