Enqode QR API
Create, list, update and delete your team's QR codes and read their scan analytics from your own code, with an API key. API access is included on the Professional, Business and Enterprise plans.
Contents:
- Getting started
- API keys
- Sending your key
- Endpoints
POST /v1/qrGET /v1/qrGET /v1/qr/{qr_id}PATCH /v1/qr/{qr_id}DELETE /v1/qr/{qr_id}GET /v1/analytics/qr/{qr_id}GET /v1/analytics/qr/{qr_id}/locationsGET /v1/analytics/qr/{qr_id}/devicesGET /v1/analytics/qr/{qr_id}/referrersGET /v1/analytics/qr/{qr_id}/campaignsGET /v1/analytics/qr/{qr_id}/eventsGET /v1/analytics/qr/{qr_id}/hourlyGET /v1/analytics/qr/{qr_id}/total
- Errors
- Limits
1. Getting started
- Base URL:
https://api.enqodeqr.com - Requests and responses are JSON. Send
Content-Type: application/jsonwith a body. - Times are ISO 8601 in UTC.
- An API key acts for your team, on your team's QR codes and their scan analytics only. It cannot reach team members, billing or settings.
- Your plan's rules apply as they do in the app.
2. API keys
Create a key in the Enqode QR app, under Settings > Developers, with Create key.
- Keys belong to the team. Only team Admins can create and revoke them; other members can see the list.
- The full key is shown once, when you create it. Copy it then and keep it somewhere safe. We store only a hashed copy, so we cannot show it again: if you lose a key, revoke it and create a new one.
- After that the list shows each key's name, a masked prefix (for example
ek_live_••••R2pQ), when it was created and when it was last used. - Revoke a key from the same list; a revoked key is refused from its next request.
- A key keeps working if the Admin who created it leaves the team. Codes a key creates show that Admin as their creator.
- A team can have up to 10 keys at a time.
- Keep keys on your server. Never put a key in a web page, a mobile app or a public repository. If a key may have leaked, revoke it.
3. Sending your key
Send the key in the Authorization header of every request, after Bearer. A key is ek_live_ followed by 38 letters and digits.
Authorization: Bearer ek_live_...Example
export ENQODE_API_KEY="ek_live_..."
curl https://api.enqodeqr.com/v1/qr \
-H "Authorization: Bearer $ENQODE_API_KEY"4. Endpoints
These are the endpoints an API key can call. {qr_id} is a code's qr_id, as create and list return it. Analytics read the window your plan's analytics history allows; when a window reaches further back, it is shortened and range.clamped is true.
POST/v1/qr
Create a QR code for your team.
JSON body
title: Required. Up to 255 characters.content: Required. An object with a "type" (for example "LINK", "TEXT", "VCARD", "EMAIL") and that type's fields, such as "url" for LINK.is_dynamic: true for a dynamic code you can edit after printing and track. Default false.description, category: Optional text.folder_id: Optional. One of your team's folders.style: Optional. The code's colours and shapes, as the app saves them.password, scan_limit, scan_zones, custom_domain: Optional settings, on the plans that include them.scan_tracking: Default true.
Your plan's rules apply as in the app: the number of dynamic codes, the QR code types and the settings your plan includes.
Request
curl -X POST https://api.enqodeqr.com/v1/qr \
-H "Authorization: Bearer $ENQODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Spring menu",
"is_dynamic": true,
"content": { "type": "LINK", "url": "https://example.com/menu" }
}'Response: 201 Created
{
"qr_id": "qr_8f3a2c1d",
"user_id": "5c1d7e2a-9b4f-4c3e-8a6d-2f0b1e7c9a34",
"title": "Spring menu",
"description": null,
"category": null,
"short_code": "aB3dE5fG",
"qr_type": "link",
"folder_id": null,
"is_dynamic": true,
"status": "ACTIVE",
"content": {
"type": "LINK",
"url": "https://example.com/menu"
},
"style": {},
"image_url": "https://d111111abcdef8.cloudfront.net/qrs/aB3dE5fG.png?v=k3m9q2",
"image_stale": false,
"scan_tracking": true,
"scan_url": "https://enqodeqr.link/s/aB3dE5fG",
"scan_count": 0,
"created_at": "2026-10-19T09:30:00+00:00",
"updated_at": "2026-10-19T09:30:00+00:00",
"geo_mode": "off",
"geo_countries": [],
"geo_block_behavior": "block_page",
"geo_fallback_url": null,
"password_protected": false,
"expires_at": null,
"scan_limit": null,
"utm": null,
"tags": null
}GET/v1/qr
List your team's QR codes, newest first.
Query parameters
skip: Offset. Default 0.limit: Codes per page. Default 10, at most 100.status: Comma-separated: ACTIVE, PAUSED, EXPIRED. Default ACTIVE.qr_type: One content type, for example link.search: Matches the title or description.category: One category.is_dynamic: true or false.created_after, created_before: ISO 8601 dates, inclusive.sort_by: created_at (default), title or scan_count.sort_order: desc (default) or asc.
Request
curl "https://api.enqodeqr.com/v1/qr?limit=10&skip=0" \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"data": [
{
"qr_id": "qr_8f3a2c1d",
"user_id": "5c1d7e2a-9b4f-4c3e-8a6d-2f0b1e7c9a34",
"title": "Spring menu",
"description": null,
"category": null,
"short_code": "aB3dE5fG",
"qr_type": "link",
"folder_id": null,
"is_dynamic": true,
"status": "ACTIVE",
"content": {
"type": "LINK",
"url": "https://example.com/menu"
},
"style": {},
"image_url": "https://d111111abcdef8.cloudfront.net/qrs/aB3dE5fG.png?v=k3m9q2",
"image_stale": false,
"scan_tracking": true,
"scan_url": "https://enqodeqr.link/s/aB3dE5fG",
"scan_count": 42,
"created_at": "2026-10-19T09:30:00+00:00",
"updated_at": "2026-10-19T09:30:00+00:00",
"geo_mode": "off",
"geo_countries": [],
"geo_block_behavior": "block_page",
"geo_fallback_url": null,
"password_protected": false,
"expires_at": null,
"scan_limit": null,
"utm": null,
"tags": null
}
],
"pagination": {
"skip": 0,
"limit": 10,
"total": 1,
"has_more": false
}
}GET/v1/qr/{qr_id}
Get one QR code. The response adds custom_domain and password_revealable to the list fields.
Request
curl https://api.enqodeqr.com/v1/qr/qr_8f3a2c1d \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"user_id": "5c1d7e2a-9b4f-4c3e-8a6d-2f0b1e7c9a34",
"title": "Spring menu",
"description": null,
"category": null,
"short_code": "aB3dE5fG",
"qr_type": "link",
"folder_id": null,
"is_dynamic": true,
"status": "ACTIVE",
"content": {
"type": "LINK",
"url": "https://example.com/menu"
},
"style": {},
"image_url": "https://d111111abcdef8.cloudfront.net/qrs/aB3dE5fG.png?v=k3m9q2",
"image_stale": false,
"scan_tracking": true,
"scan_url": "https://enqodeqr.link/s/aB3dE5fG",
"scan_count": 42,
"created_at": "2026-10-19T09:30:00+00:00",
"updated_at": "2026-10-19T09:30:00+00:00",
"geo_mode": "off",
"geo_countries": [],
"geo_block_behavior": "block_page",
"geo_fallback_url": null,
"password_protected": false,
"expires_at": null,
"scan_limit": null,
"utm": null,
"tags": null,
"custom_domain": null,
"password_revealable": false
}PATCH/v1/qr/{qr_id}
Change a QR code. Send only the fields you want to change; for a dynamic code, sending new content changes where it goes without reprinting it.
JSON body (every field optional)
title, description, category: Text.status: ACTIVE or PAUSED.content: Replaces the whole content object.style: Merged key by key: keys you send replace, a key sent as null is removed. null clears the whole style.folder_id: Move to one of your team's folders; null takes it out of its folder.password: An empty string removes it.expires_at, scan_limit, scan_tracking, scan_zones, custom_domain, utm, tags: As in the app, on the plans that include them.
is_dynamic cannot be changed after creation (422 FIELD_NOT_WRITABLE).
Request
curl -X PATCH https://api.enqodeqr.com/v1/qr/qr_8f3a2c1d \
-H "Authorization: Bearer $ENQODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "content": { "type": "LINK", "url": "https://example.com/summer-menu" } }'Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"user_id": "5c1d7e2a-9b4f-4c3e-8a6d-2f0b1e7c9a34",
"title": "Spring menu",
"description": null,
"category": null,
"short_code": "aB3dE5fG",
"qr_type": "link",
"folder_id": null,
"is_dynamic": true,
"status": "ACTIVE",
"content": {
"type": "LINK",
"url": "https://example.com/summer-menu"
},
"style": {},
"image_url": "https://d111111abcdef8.cloudfront.net/qrs/aB3dE5fG.png?v=k3m9q2",
"image_stale": false,
"scan_tracking": true,
"scan_url": "https://enqodeqr.link/s/aB3dE5fG",
"scan_count": 42,
"created_at": "2026-10-19T09:30:00+00:00",
"updated_at": "2026-10-20T14:05:12+00:00",
"geo_mode": "off",
"geo_countries": [],
"geo_block_behavior": "block_page",
"geo_fallback_url": null,
"password_protected": false,
"expires_at": null,
"scan_limit": null,
"utm": null,
"tags": null,
"custom_domain": null,
"password_revealable": false
}DELETE/v1/qr/{qr_id}
Move a QR code to the trash. It stops scanning straight away and can be restored from the app's trash for 30 days.
Request
curl -X DELETE https://api.enqodeqr.com/v1/qr/qr_8f3a2c1d \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 204 No Content
(no body)GET/v1/analytics/qr/{qr_id}
Scan totals and the scan trend for one QR code.
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).period: daily (default), weekly, monthly, or hourly (a window of one day).tz: IANA time zone, for example Europe/London. Default UTC.compare_previous: true adds the same-length window before it. Default false.
Request
curl "https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d?start_date=2026-10-13&end_date=2026-10-19&period=daily" \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"range": {
"start_date": "2026-10-13",
"end_date": "2026-10-19",
"period": "daily",
"tz": "UTC",
"clamped": false
},
"totals": {
"total_scans": 42,
"unique_scans": 31,
"avg_per_day": 6,
"repeat_rate_pct": 26.2,
"peak": {
"weekday": 4,
"hour": 12,
"count": 7
}
},
"scan_trend": [
{
"date": "2026-10-18",
"total": 9,
"unique": 7
},
{
"date": "2026-10-19",
"total": 12,
"unique": 8
}
],
"previous_period": null,
"retention_days": 730
}GET/v1/analytics/qr/{qr_id}/locations
Where the code was scanned.
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).granularity: country (default), region or city.top_n: Rows to return, 1 to 200. Default 10.include_pins: true adds map pins. Default false.
Request
curl "https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/locations?granularity=city" \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"total_scans": 42,
"locations": [
{
"country": "GB",
"country_name": "United Kingdom",
"region": "England",
"city": "London",
"count": 25,
"pct": 59.5
},
{
"country": "IN",
"country_name": "India",
"region": "Maharashtra",
"city": "Mumbai",
"count": 15,
"pct": 35.7
}
],
"unidentified": {
"rows": [
{
"country": "",
"country_name": "",
"region": null,
"city": null,
"count": 2,
"pct": 4.8
}
],
"other": null
},
"pins": null,
"gps_stats": {
"granted": 4,
"denied": 10,
"unknown": 28
}
}GET/v1/analytics/qr/{qr_id}/devices
Device types, operating systems and browsers.
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).
Request
curl https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/devices \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"total_scans": 42,
"device_types": [
{
"name": "mobile",
"count": 38,
"pct": 90.5
},
{
"name": "desktop",
"count": 4,
"pct": 9.5
}
],
"os": [
{
"name": "iOS",
"count": 24,
"pct": 57.1
},
{
"name": "Android",
"count": 14,
"pct": 33.3
},
{
"name": "Windows",
"count": 4,
"pct": 9.5
}
],
"browsers": [
{
"name": "Safari",
"count": 24,
"pct": 57.1
},
{
"name": "Chrome",
"count": 18,
"pct": 42.9
}
]
}GET/v1/analytics/qr/{qr_id}/referrers
Where scans came from. Scans with no referrer are counted as "direct".
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).
Request
curl https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/referrers \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"total_scans": 42,
"referrers": [
{
"name": "direct",
"count": 40,
"pct": 95.2
},
{
"name": "instagram.com",
"count": 2,
"pct": 4.8
}
]
}GET/v1/analytics/qr/{qr_id}/campaigns
Scans by UTM source, medium and campaign. total_scans counts the scans that carry a utm_source.
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).
Request
curl https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/campaigns \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"total_scans": 30,
"utm_source": [
{
"name": "flyer",
"count": 30,
"pct": 100
}
],
"utm_medium": [
{
"name": "print",
"count": 30,
"pct": 100
}
],
"utm_campaign": [
{
"name": "spring-menu",
"count": 30,
"pct": 100
}
]
}GET/v1/analytics/qr/{qr_id}/events
The scan log, newest first, one page at a time.
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).limit: Scans per page, 1 to 200. Default 50.cursor: The next_cursor of the previous page.
Request
curl "https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/events?limit=50" \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"events": [
{
"scan_id": "0f6c2b9e-3d1a-4e7b-9c58-6a2d4f1e8b70",
"scan_time": "2026-10-19T12:41:07Z",
"city": "London",
"region": "England",
"country": "GB",
"country_name": "United Kingdom",
"latitude": null,
"longitude": null,
"gps_granted": false,
"device_type": "mobile",
"device_model": "iPhone",
"os": "iOS",
"os_version": "18.0",
"browser": "Safari",
"is_unique": true,
"is_blocked": false,
"block_reason": null,
"utm": {
"source": "flyer",
"medium": "print",
"campaign": "spring-menu",
"term": null,
"content": null
}
}
],
"next_cursor": "eyJxcl9pZCI6InFyXzhmM2EyYzFkIn0=",
"has_more": true
}GET/v1/analytics/qr/{qr_id}/hourly
Scans by weekday and hour: 168 cells (7 days by 24 hours). weekday 0 is Monday.
Query parameters
start_date: YYYY-MM-DD. Default: 30 days before end_date.end_date: YYYY-MM-DD. Default: today (UTC).tz: Required. IANA time zone, for example Asia/Kolkata.
The example shows 2 of the 168 cells.
Request
curl "https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/hourly?tz=Europe/London" \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"tz": "Europe/London",
"range": {
"start_date": "2026-09-19",
"end_date": "2026-10-19",
"tz": "Europe/London",
"clamped": false
},
"cells": [
{
"weekday": 0,
"hour": 0,
"count": 0
},
{
"weekday": 4,
"hour": 12,
"count": 7
}
],
"peak": {
"weekday": 4,
"hour": 12,
"count": 7
},
"max_count": 7
}GET/v1/analytics/qr/{qr_id}/total
All-time scan totals for one QR code, with no date window.
Request
curl https://api.enqodeqr.com/v1/analytics/qr/qr_8f3a2c1d/total \
-H "Authorization: Bearer $ENQODE_API_KEY"Response: 200 OK
{
"qr_id": "qr_8f3a2c1d",
"total_scans": 1280,
"unique_scans": 904,
"scope": "all_time",
"retention_days": 730
}5. Errors
An error from the API is JSON with a readable error and a code to act on; some add more fields beside them. A few refusals come from the gateway in front of the API, before your request reaches it: those bodies are {"message": "..."}.
| Status | Code or body | When |
|---|---|---|
| 401 | {"message":"Unauthorized"} | No Authorization header was sent. |
| 403 | {"message":"Forbidden"} | The key is malformed (not ek_live_ followed by 38 letters and digits, or a character is wrong or missing, for example a key cut short when it was pasted), or the endpoint is not open to API keys. Team members, billing, settings, exports and every endpoint not listed on this page refuse API keys. |
| 401 | INVALID_API_KEY | The key is well formed but unknown, or it has been revoked. "This API key is invalid or has been revoked." |
| 403 | API_ACCESS_NOT_IN_PLAN | Your team's plan does not include API access (Freemium or Starter, or a team that moved down from Professional). Its keys work again once it upgrades. |
| 403 | QUOTA_EXCEEDED, PLAN_NOT_ELIGIBLE | Your plan does not allow this, the same refusal the app gives: for example your plan's dynamic QR code limit, a QR code type or a setting your plan does not include. The body names the limit, your plan_code, the plan to upgrade_to and the upgrade_url. |
| 404 | NOT_FOUND | No such QR code in your team. Another team's codes are always "not found". |
| 400, 422 | INVALID_JSON, MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE, FIELD_NOT_WRITABLE | The request is not valid. Where a field is at fault, the body names it in "field". |
| 429 | RATE_LIMITED | More than 10 requests in one second with this key. Wait for the Retry-After header (1 second) and try again. |
| 429 | MONTHLY_LIMIT_REACHED | Your team has used this month's requests. Calls are refused until the month resets. |
| 429 | {"message":"Too Many Requests"} | The platform is busy on this endpoint. Wait a moment and retry, backing off between attempts. |
| 500 | INTERNAL_ERROR | Something went wrong on our side. Retry later; if it keeps happening, contact support. |
Revoked or unknown key: 401 Unauthorized
{
"error": "This API key is invalid or has been revoked.",
"code": "INVALID_API_KEY"
}Plan without API access: 403 Forbidden
{
"error": "Your plan does not include API access.",
"code": "API_ACCESS_NOT_IN_PLAN",
"plan_code": "starter",
"upgrade_url": "/pricing"
}A plan limit, as in the app: 403 Forbidden
{
"error": "Your Professional plan allows 200 dynamic QR codes and you have 200. Upgrade, or remove some, to create more.",
"code": "QUOTA_EXCEEDED",
"limit": "dynamic_qr",
"resource": "dynamic_qr",
"plan_code": "professional",
"used": 200,
"allowed": 200,
"upgrade_to": "business",
"upgrade_url": "/pricing"
}Too fast: 429 Too Many Requests
Retry-After: 1
{
"error": "Slow down: at most 10 requests a second per key.",
"code": "RATE_LIMITED"
}Monthly limit reached: 429 Too Many Requests
{
"error": "Monthly API limit reached.",
"code": "MONTHLY_LIMIT_REACHED",
"used": 3000,
"limit": 3000,
"period": "2026-10",
"resets_at": "2026-11-01T00:00:00Z"
}6. Limits
| Plan | API requests |
|---|---|
| Freemium | No API access |
| Starter | No API access |
| Professional | 3,000 requests a month |
| Business | 10,000 requests a month |
| Enterprise | 50,000 requests a month |
- The monthly count is per team, across all its keys. A month is a calendar month in UTC: the count resets at 00:00 UTC on the 1st.
- Once the limit is reached, requests are refused with
MONTHLY_LIMIT_REACHEDuntil the month resets. Unused requests do not roll over. - Every request that passes the key check counts toward the monthly limit, including requests the endpoint refuses (for example 404 or a plan refusal). Requests refused with 401, 403
API_ACCESS_NOT_IN_PLANor 429 are not counted. - Team Admins get an email at 80% and at 100% of the monthly limit.
- Settings > Developers shows how many requests your team has used this month.
- Each key can make up to 10 requests a second. Faster requests are refused with
RATE_LIMITED; wait forRetry-Afterand try again.
See pricing for the plans, and the API Terms of Use for the terms that apply to the API.
Questions about the API? Email support@enqodeqr.com.