Developers
A QR code API. And an MCP server.
Make verified picture codes from your own software or from an AI agent. One call takes an image and a link and returns a code that scans, activated on your plan, with analytics. Included in the Business plan; the quarter plan can read codes and stats.
On this page
- Authentication
- Create a code
- Get stats
- Change destination
- Landing pages
- List leads
- MCP server
- Errors and limits
- Changelog
Machine-readable: /openapi.json (OpenAPI 3.1)
Authentication
Base URL https://ariaqr.com/api/v1, JSON in and out. Create a key on your account page, under API & MCP, and send it as a Bearer token. The secret is shown once; revoke and re-create any time. Keys need an active plan: on the quarter plan they read codes, stats, pages and leads; creating or changing anything needs Business. The Node.js and Python helper below is reused by every example on this page, and handles the errors listed under Errors and limits.
# Every request sends the key as a Bearer token.
curl -i https://ariaqr.com/api/v1/codes \
-H "Authorization: Bearer aq_your_key_here"// Node 18+ (built-in fetch). Reused by every Node.js example below.
const KEY = 'aq_your_key_here';
const BASE = 'https://ariaqr.com/api/v1';
async function aria(path, { method = 'GET', body } = {}) {
const res = await fetch(BASE + path, {
method,
headers: { Authorization: 'Bearer ' + KEY, ...(body && { 'Content-Type': 'application/json' }) },
body: body && JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (res.ok) return data;
if (res.status === 401) throw new Error('Bad or revoked API key.');
if (res.status === 402) throw new Error('Plan needed, or no activations left: ' + data.error);
if (res.status === 429) {
// "Rate limit: 60 requests a minute. Retry in 12s."
const wait = Number(/Retry in (\d+)s/.exec(data.error)?.[1] ?? 60);
await new Promise((r) => setTimeout(r, wait * 1000));
return aria(path, { method, body });
}
throw new Error(res.status + ': ' + data.error);
}
console.log((await aria('/codes')).codes.length, 'codes');# pip install requests. Reused by every Python example below.
import re
import time
import requests
KEY = "aq_your_key_here"
BASE = "https://ariaqr.com/api/v1"
def aria(path, method="GET", **kwargs):
res = requests.request(
method, BASE + path, headers={"Authorization": f"Bearer {KEY}"}, timeout=330, **kwargs
)
if res.ok:
return res.json()
error = res.json().get("error", "") if "json" in res.headers.get("content-type", "") else res.text
if res.status_code == 401:
raise RuntimeError("Bad or revoked API key.")
if res.status_code == 402:
raise RuntimeError(f"Plan needed, or no activations left: {error}")
if res.status_code == 429:
# "Rate limit: 60 requests a minute. Retry in 12s."
m = re.search(r"Retry in (\d+)s", error)
time.sleep(int(m.group(1)) if m else 60)
return aria(path, method, **kwargs)
raise RuntimeError(f"{res.status_code}: {error}")
print(len(aria("/codes")["codes"]), "codes")Create a code
POST /api/v1/codes runs the same engine and the same verification as the site. By default the code is activated on your plan and comes back ready to print. The image must be square (1:1, within 2%), PNG, JPEG or WebP, up to 10 MB.
curl -X POST https://ariaqr.com/api/v1/codes \
-H "Authorization: Bearer aq_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://example.com/artwork.png",
"url": "https://example.com/menu",
"edge": "soft"
}'const code = await aria('/codes', {
method: 'POST',
body: {
image_url: 'https://example.com/artwork.png',
url: 'https://example.com/menu',
edge: 'soft',
},
});
console.log(code.short_url, code.download_url);code = aria("/codes", "POST", json={
"image_url": "https://example.com/artwork.png",
"url": "https://example.com/menu",
"edge": "soft",
})
print(code["short_url"], code["download_url"])HTTP/1.1 201 Created
{
"job_id": "…",
"verified": true,
"activated": true,
"code": "a7Bx9Q",
"short_url": "aqr.li/a7Bx9Q",
"preview_url": "https://ariaqr.com/api/preview/…",
"download_url": "https://ariaqr.com/api/download/…",
"stats_url": "https://ariaqr.com/stats/a7Bx9Q?k=…",
"pay_url": null
}Other kinds use kind and fields, with the same field names as the generator: whatsapp, vcard, wifi, upi, email, phone, sms, maps, event, text. Send image_base64 instead of image_url if the image is not online. Set activate: false to generate without spending an activation; the response then carries a pay_url. Add team_id to make the code in a team workspace.
GET /api/v1/codes lists your codes, newest first, as { "codes": [ … ] }. Each entry has the code, short_url, destination, kind, active, created_at, expires_at and stats_url.
Get stats for a code
GET /api/v1/codes/{code} returns the code with its analytics: scans, clicks, unique visitors, 90 days of daily counts, and breakdowns by country, region, city, device and browser. Counts are pseudonymous; there is no per-person data. An unknown code, or one that is not on your account, is a 404.
curl https://ariaqr.com/api/v1/codes/a7Bx9Q \
-H "Authorization: Bearer aq_your_key_here"const s = await aria('/codes/a7Bx9Q');
console.log(s.destination, s.stats.scans, 'scans from', s.stats.unique, 'visitors');
console.log('top country:', s.stats.countries[0]?.label);s = aria("/codes/a7Bx9Q")
print(s["destination"], s["stats"]["scans"], "scans from", s["stats"]["unique"], "visitors")
top = s["stats"]["countries"][:1]
print("top country:", top[0]["label"] if top else None){
"code": "a7Bx9Q",
"short_url": "aqr.li/a7Bx9Q",
"destination": "https://example.com/menu",
"kind": "website",
"summary": "https://example.com/menu",
"active": true,
"tier": "paid",
"activation": "plan",
"created_at": "2026-10-06T09:12:44.000Z",
"expires_at": null,
"stats_url": "https://ariaqr.com/stats/a7Bx9Q?k=…",
"stats": { // trimmed: the full object also has regions, postals, langs, heat, points, recent…
"total": 214, "scans": 198, "clicks": 16, "previews": 3,
"unique": 171, "repeatRate": 0.12, "last7": 74, "prev7": 51,
"days": [{ "day": "2026-10-05", "scans": 31, "clicks": 2, "unique": 27 }],
"countries": [{ "key": "US", "label": "United States", "count": 96, "pct": 48.5 }],
"cities": [{ "key": "Austin", "label": "Austin", "count": 22, "pct": 11.1 }],
"os": [{ "key": "iOS", "label": "iOS", "count": 120, "pct": 60.6 }],
"browsers": [{ "key": "Safari", "label": "Safari", "count": 104, "pct": 52.5 }]
}
}Change where a code goes
PATCH /api/v1/codes/{code} with { "destination": "https://…" } points the printed code somewhere else, no reprint. The new address is safety-checked first; one that fails comes back as a 400 with the reason. The response is the updated code, in the same shape as the list entries.
curl -X PATCH https://ariaqr.com/api/v1/codes/a7Bx9Q \
-H "Authorization: Bearer aq_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "destination": "https://example.com/spring-menu" }'const updated = await aria('/codes/a7Bx9Q', {
method: 'PATCH',
body: { destination: 'https://example.com/spring-menu' },
});
console.log(updated.destination);updated = aria("/codes/a7Bx9Q", "PATCH", json={"destination": "https://example.com/spring-menu"})
print(updated["destination"])Landing pages
Landing pages are built, edited and published through the MCP server, with tools such as create_page, update_page, publish_page and page_stats. Building and publishing pages through the MCP server needs Business; on the quarter plan the read tools, such as list_pages and page_stats, still work. The REST API reads back what a page collects, below. The page builder works with Claude and ChatGPT too: Build landing pages with Claude or ChatGPT.
List a page’s leads
GET /api/v1/pages/{id}/leads returns what a landing page’s forms collected, newest first, with the 30-day conversion and leads per printed code. Query: limit (1 to 500, default 50), since_days (1 to 365) and format=csv for the whole list as a CSV file. Only the page’s owner can read it; any other page id is a 404.
# JSON, newest first
curl "https://ariaqr.com/api/v1/pages/your_page_id/leads?limit=100&since_days=30" \
-H "Authorization: Bearer aq_your_key_here"
# every lead as CSV
curl -o leads.csv "https://ariaqr.com/api/v1/pages/your_page_id/leads?format=csv" \
-H "Authorization: Bearer aq_your_key_here"const report = await aria('/pages/your_page_id/leads?limit=100&since_days=30');
console.log(report.last_30d.leads, 'leads,', report.last_30d.conversion_pct + '% conversion');
for (const lead of report.leads) console.log(lead.received, lead.source, lead.answers);
// CSV is plain text, so it skips the JSON helper:
const csv = await (await fetch(BASE + '/pages/your_page_id/leads?format=csv', {
headers: { Authorization: 'Bearer ' + KEY },
})).text();report = aria("/pages/your_page_id/leads", params={"limit": 100, "since_days": 30})
print(report["last_30d"]["leads"], "leads,", report["last_30d"]["conversion_pct"], "% conversion")
for lead in report["leads"]:
print(lead["received"], lead["source"], lead["answers"])
# CSV is plain text, so it skips the JSON helper:
csv_text = requests.get(
BASE + "/pages/your_page_id/leads", params={"format": "csv"},
headers={"Authorization": f"Bearer {KEY}"}, timeout=60,
).text{
"page": { "id": "…", "title": "Corner Café menu", "live": true },
"total_kept": 42,
"last_30d": { "leads": 18, "views": 640, "conversion_pct": 2.8, "from_qr": 14 },
"by_code": [{ "code": "a7Bx9Q", "scans": 410, "leads": 14, "conversion_pct": 3.4 }],
"leads": [{
"id": "…",
"received": "2026-10-05T18:41:09.000Z",
"form": "Book a table",
"source": "qr",
"code": "a7Bx9Q",
"city": "Austin",
"country": "United States",
"answers": { "Name": "Jordan Lee", "Email": "jordan@example.com", "Guests": "4" }
}],
"retention_days": 365
}MCP server
Streamable HTTP at https://ariaqr.com/api/mcp, same Bearer key. Code tools: create_code, list_codes, get_code_stats, set_destination. Landing-page tools: page_guide, list_templates, list_pages, get_page, create_page, update_page, add_photo, set_address, publish_page, unpublish_page, page_stats, list_leads, set_lead_webhook (publishing needs the quarter plan or Business; lead forms need Business).
Claude (claude.ai and the Claude apps) and ChatGPT add servers by URL and can’t send a header, so use a connector URL, made under Account → API → Connector URLs: https://ariaqr.com/api/mcp/k/aqc_…, with no sign-in. Its token only works with the MCP server, never the REST API; it has the same plan check and rate limit as a key, and stops working the moment you revoke it. Connector URLs made from an API key still work for now, but replace them. Step-by-step for each app: Build landing pages with Claude or ChatGPT.
// Cursor: ~/.cursor/mcp.json (or any MCP client that sends headers)
{
"mcpServers": {
"ariaqr": {
"url": "https://ariaqr.com/api/mcp",
"headers": { "Authorization": "Bearer aq_…" }
}
}
}# Claude Code
claude mcp add --transport http ariaqr https://ariaqr.com/api/mcp \
--header "Authorization: Bearer aq_…"Then ask: “Make an AriaQR code from this product photo that opens our booking page, and tell me how many scans it got this week.” Or: “Make a landing page for my café and publish it.”
Errors and limits
Errors are JSON, { "error": "…" }, with the matching HTTP status. Handle 401, 402 and 429 as the helper under Authentication does: a 429 says how many seconds to wait, and a retry then goes through.
| Status | When | Example message |
|---|---|---|
| 400 | Bad input: no image, a non-square or too-large image, a missing url, a destination that is not a full URL or fails the safety check. | Image must be square (1:1). Yours is 1200×800. |
| 401 | No key, a wrong key or a revoked key. | Missing or invalid API key. Send `Authorization: Bearer aq_…`. |
| 402 | The account has no active quarter or Business plan, or no activations left, or a create or change call on the quarter plan (it can only read). | API access needs an active quarter plan or Business plan. https://ariaqr.com/pricing |
| 403 | A team_id you are not a member of. | You are not a member of that team. |
| 404 | A code or landing page that is not on this account. | No such code on this account. |
| 429 | More than 60 requests in a minute on one key. | Rate limit: 60 requests a minute. Retry in 12s. |
| 500 / 503 | Something failed on our side. Safe to retry; a failed build is never charged. | Your final code could not be built just now. Try again in a minute; nothing extra will be charged. |
- 60 requests a minute per key.
- Images up to 10 MB, PNG, JPEG or WebP, and square. Use the site to place a code inside a larger picture.
- Each activation counts against the plan’s allowance. Generating with activate: false does not.
- A create call can take up to a few minutes while the code is built and verified; give your client a long timeout.
Changelog
Changes to the REST API and the MCP server that you would notice, newest first.
· MCP
The quarter plan can publish landing pages: up to 5 new pages a quarter, without form blocks. publish_page explains when a form block or the allowance stops a publish. Lead forms stay on Business.
· Docs and spec
This page now has runnable cURL, Node.js and Python examples, and the whole REST API is described in OpenAPI 3.1 at /openapi.json.
· MCP
publish_page waits for the new address to open and returns link_opens_now, so the link you are handed works when you tap it. Business includes up to 100 new landing pages a month.
· REST and MCP
New: GET /api/v1/pages/{id}/leads returns what a landing page’s forms collected, with 30-day conversion and leads per printed code; ?format=csv exports the whole list. New MCP tools list_leads and set_lead_webhook. Page guide updated for lead forms and 15 form templates.
· MCP
Landing pages publish to aqr.li addresses. The in-chat page preview looks like a phone. Profile blocks take a cover banner, and animated GIFs stay animated.
· REST
Business accounts can create API keys and call the API.
· MCP
Landing-page tools added to the MCP server: page_guide, list_templates, list_pages, get_page, create_page, update_page, add_photo, set_address, publish_page, unpublish_page, page_stats. Claude and ChatGPT can now connect by URL.
· REST
New codes use the short domain: short_url comes back as aqr.li/<code>.
· REST and MCP
Codes on a team follow team membership: someone removed from a team loses API and MCP access to that team’s codes.
· REST and MCP
The square-image rule is spelled out: create_code and POST /api/v1/codes need a square image (1:1, within 2%) and answer a 400 that gives the size otherwise.
· First release
REST: POST and GET /api/v1/codes, GET and PATCH /api/v1/codes/{code}. MCP server with create_code, list_codes, get_code_stats and set_destination. Bearer keys with a limit of 60 requests a minute.
Questions
What does the AriaQR API do?+
It makes a verified picture QR code from an image URL and a destination in one call, activates it on your plan, and returns the preview, the 2048px download, the short link and an analytics link. It can also list your codes, read scan analytics and change where a code points.
What is the AriaQR MCP server?+
An MCP (Model Context Protocol) endpoint at https://ariaqr.com/api/mcp that exposes the same capabilities as tools, plus the landing-page builder, so Claude, ChatGPT, Cursor and other agents can make and manage codes and pages for you. Authenticate with an API key as a Bearer token, or, in Claude and ChatGPT, add a connector URL (https://ariaqr.com/api/mcp/k/ followed by a connector token) made under Account → API → Connector URLs. The token only works with the MCP server, not the REST API, and you can revoke it any time.
How do I get an API key?+
Start Business for full access, or the quarter plan to read codes, stats, pages and leads, then create a key on your account page. The secret is shown once. Keys can be revoked any time.
What are the limits?+
60 requests a minute per key, images up to 10 MB, and every activation counts against your plan’s allowance. Generation without activation is free and unlimited within the rate limit.