QR code API and MCP server
Create, change and measure dynamic QR codes from your own code, or let an AI assistant do it for you. Both are free for members while Pro is being built.
What the API does
A dynamic QR code holds a short link, neoreader.com/q/SLUG, that redirects to a destination you can change at any time. The API lets you do everything the dashboard does with those codes:
- create, list, read, update and delete codes;
- change a destination without reprinting the code;
- set a start date, an expiry, a scan limit, a password and UTM tags;
- read scan statistics: totals, a daily series, countries, devices, operating systems and browsers.
Scans are counted without storing IP addresses. The API returns JSON and works with any language that can send an HTTPS request. It does not draw QR images; use the QR code generator to design and download the image for a code's short URL.
Get a token
- Sign in at
/loginwith Google or an emailed link. - Open Account, then API Tokens, and create a token with a name you will recognise.
- Copy it straight away. It is shown once, and you can revoke it at any time.
Send the token in an Authorization header on every request. Treat it like a password: keep it out of public repositories and browser code.
The base URL is https://neoreader.com/api/v1. Every request needs Accept: application/json. In the examples, replace YOUR_TOKEN with your token.
curl https://neoreader.com/api/v1/me \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Endpoints
GET /me
Your name, email, plan, how many codes you have and how many you may have.
curl https://neoreader.com/api/v1/me \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{
"data": {
"name": "Jane Doe",
"email": "[email protected]",
"plan": "free",
"codes_count": 12,
"code_limit": 250
}
}
GET /codes
A page of your codes, newest first. Optional query parameters: q searches title, slug and destination; folder matches a folder name; status is one of active, paused, blocked, expired, not_started or limit; per_page is 1 to 100 (default 25); page picks the page.
curl "https://neoreader.com/api/v1/codes?status=active&folder=Restaurant&per_page=10" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{
"data": [ { "slug": "k7Xp2m", "short_url": "https://neoreader.com/q/k7Xp2m", "title": "Spring menu", "status": "active", "...": "..." } ],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "per_page": 10, "total": 1, "last_page": 1 }
}
POST /codes
Creates a code and returns it with status 201. title and destination are required. The destination must be a full https:// or http:// address, or a mailto: or tel: link; links back to another NeoReader short link and private network addresses are refused.
curl -X POST https://neoreader.com/api/v1/codes \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"title":"Spring menu","destination":"https://example.com/menu","folder":"Restaurant","utm":{"utm_source":"qr","utm_medium":"print"}}'
{
"data": {
"slug": "k7Xp2m",
"short_url": "https://neoreader.com/q/k7Xp2m",
"title": "Spring menu",
"destination": "https://example.com/menu",
"folder": "Restaurant",
"status": "active",
"is_active": true,
"starts_at": null,
"expires_at": null,
"scan_limit": null,
"has_password": false,
"utm": { "utm_source": "qr", "utm_medium": "print", "utm_campaign": null },
"scans_count": 0,
"last_scanned_at": null,
"created_at": "2026-11-03T10:15:00+00:00",
"updated_at": "2026-11-03T10:15:00+00:00"
}
}
Code fields
| Field | Type | Notes |
|---|---|---|
title | string | Up to 120 characters. Only you see it. |
destination | string | Up to 2,000 characters. Change it any time. |
folder | string or null | Up to 60 characters. |
is_active | boolean | false pauses the code. A blocked code cannot be turned back on. |
starts_at, expires_at | date and time or null | ISO 8601, for example 2026-12-31T23:59:00Z. The expiry must be after the start. |
scan_limit | integer or null | The code stops redirecting after this many scans. |
password | string or null | Write-only, 4 to 100 characters. Responses show has_password instead. Send null to remove it. |
utm | object or null | utm_source, utm_medium and utm_campaign, added to the destination on every scan. |
Responses also include slug, short_url, status, scans_count, last_scanned_at, created_at and updated_at. status is one of active, paused, blocked, not_started, expired or limit.
GET /codes/{slug}
One code. A slug that is not yours returns 404.
curl https://neoreader.com/api/v1/codes/k7Xp2m \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{
"data": {
"slug": "k7Xp2m",
"short_url": "https://neoreader.com/q/k7Xp2m",
"title": "Spring menu",
"destination": "https://example.com/menu",
"folder": "Restaurant",
"status": "active",
"is_active": true,
"starts_at": null,
"expires_at": null,
"scan_limit": null,
"has_password": false,
"utm": { "utm_source": "qr", "utm_medium": "print", "utm_campaign": null },
"scans_count": 0,
"last_scanned_at": null,
"created_at": "2026-11-03T10:15:00+00:00",
"updated_at": "2026-11-03T10:15:00+00:00"
}
}
PATCH /codes/{slug}
Changes the fields you send and leaves the rest alone. The slug and short URL never change.
curl -X PATCH https://neoreader.com/api/v1/codes/k7Xp2m \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"destination":"https://example.com/summer-menu","expires_at":"2026-12-31T23:59:00Z"}'
{ "data": { "slug": "k7Xp2m", "destination": "https://example.com/summer-menu", "expires_at": "2026-12-31T23:59:00+00:00", "...": "..." } }
DELETE /codes/{slug}
Deletes the code and its scan history for good. Returns 204 with no body. The printed code stops working, so pause it instead if you may need it again.
curl -X DELETE https://neoreader.com/api/v1/codes/k7Xp2m \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
HTTP/1.1 204 No Content
GET /codes/{slug}/stats
Scan statistics for the last days days (1 to 365, default 30, today included). unique counts one visitor once per day.
curl "https://neoreader.com/api/v1/codes/k7Xp2m/stats?days=7" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{
"data": {
"slug": "k7Xp2m",
"days": 7,
"from": "2026-10-28",
"to": "2026-11-03",
"totals": { "scans": 42, "unique": 31 },
"all_time_scans": 120,
"daily": [ { "date": "2026-10-28", "scans": 5, "unique": 4 }, "..." ],
"countries": [ { "country": "US", "scans": 30 }, { "country": "GB", "scans": 8 } ],
"devices": [ { "device": "mobile", "scans": 39 } ],
"os": [ { "os": "iOS", "scans": 25 } ],
"browsers": [ { "browser": "Safari", "scans": 24 } ]
}
}
Rate limits
Each token may make 120 requests per minute. Requests without a valid token share a lower limit per IP address. Over the limit you get 429 Too Many Attempts with a Retry-After header giving the seconds to wait. Each account may hold up to 250 dynamic codes.
Errors
Errors are JSON with a message. Validation errors also list each field under errors.
| Status | Meaning |
|---|---|
401 | The token is missing, wrong or revoked. |
403 | The account is disabled. |
404 | No such code on your account. |
422 | A field is invalid, you reached your code limit, or you tried to turn a blocked code back on. |
429 | Too many requests. Wait for the seconds in Retry-After. |
{
"message": "Enter a full web address starting with https://",
"errors": { "destination": ["Enter a full web address starting with https://"] }
}
MCP server
NeoReader also runs a hosted Model Context Protocol server, so an AI assistant can manage your codes in plain language: “Point my menu code at the new PDF”, “Which of my codes got the most scans this month?” It uses the same tokens, limits and ownership rules as the API.
The server is called NeoReader QR and lives at https://neoreader.com/mcp (streamable HTTP). Authenticate with the header Authorization: Bearer YOUR_TOKEN.
| Tool | What it does |
|---|---|
list_codes | Lists your codes, with optional search, folder and status filters. |
create_code | Creates a code from a title and a destination. |
update_code | Changes a code's destination, title, folder, on/off state or expiry. |
get_code_stats | Scan totals and breakdowns for the last N days. |
pause_code | Stops a code redirecting without deleting it. |
delete_code | Deletes a code for good. Marked as destructive, so most assistants ask you first. |
Claude Code
claude mcp add --transport http neoreader https://neoreader.com/mcp --header "Authorization: Bearer YOUR_TOKEN"
Claude Desktop
Add this to claude_desktop_config.json (Settings, Developer, Edit Config), then restart the app. It uses the mcp-remote bridge, which needs Node.js.
{
"mcpServers": {
"neoreader": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://neoreader.com/mcp", "--header", "Authorization: Bearer YOUR_TOKEN"]
}
}
}
Cursor
Add this to ~/.cursor/mcp.json, or to .cursor/mcp.json inside a project.
{
"mcpServers": {
"neoreader": {
"url": "https://neoreader.com/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}
Free for members
The API and the MCP server are free for every member while Pro is being built. Questions or something missing? Get in touch.