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.

1. Getting started

  • Base URL: https://api.enqodeqr.com
  • Requests and responses are JSON. Send Content-Type: application/json with 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": "..."}.

StatusCode or bodyWhen
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.
401INVALID_API_KEYThe key is well formed but unknown, or it has been revoked. "This API key is invalid or has been revoked."
403API_ACCESS_NOT_IN_PLANYour 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.
403QUOTA_EXCEEDED, PLAN_NOT_ELIGIBLEYour 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.
404NOT_FOUNDNo such QR code in your team. Another team's codes are always "not found".
400, 422INVALID_JSON, MISSING_REQUIRED_FIELD, INVALID_FIELD_VALUE, FIELD_NOT_WRITABLEThe request is not valid. Where a field is at fault, the body names it in "field".
429RATE_LIMITEDMore than 10 requests in one second with this key. Wait for the Retry-After header (1 second) and try again.
429MONTHLY_LIMIT_REACHEDYour 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.
500INTERNAL_ERRORSomething 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

PlanAPI requests
FreemiumNo API access
StarterNo API access
Professional3,000 requests a month
Business10,000 requests a month
Enterprise50,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_REACHED until 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_PLAN or 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 for Retry-After and 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.