Skip to content
API key

Guide

Scan analytics

Read how often a card or a QR code was scanned. The API returns totals and breakdowns for a period; it does not return individual scans.

What data is available

What is it?
Scan statistics for one card, one QR code, or the whole account, over a chosen period.
Why would I use it?
To show scan counts in your own product, or to report on a print campaign.
What do I need first?
A key with analytics:read.
Which endpoint do I use?
GET /analytics/:id for one item, GET /analytics/overview for the account.
What result should I expect?
A scan_stats object: totals, a comparison with the previous period, and a day-by-day timeline.
Fields of the scan_stats object
FieldWhat it isAlways present?
totalScansScans in the period.Yes
uniqueVisitorsDistinct visitors in the period.Yes
previousPeriodScansScans in the period of the same length just before.Yes
trendThe change between the two, as text such as "+20%".Yes
timelineOne entry per day: { date, scans }.Yes
periodThe start, end and number of days actually reported.Yes
clamped, historyDaysWhether the plan shortened the period you asked for. See plan limits.Yes
topCountriesUp to ten countries: { country, code, scans }.Only with advanced analytics
devicesDevice types: { device, scans, percent }.Only with advanced analytics
topQrCodesThe five busiest items: { id, name, type, scans }.Overview only

Statistics for one card or code

GET/analytics/:id

:id is a card id or a QR code id; the endpoint accepts either. This asks for the last seven days of the example card:

curl "https://api.qrbold.com/api/public/v1/analytics/cmf3k2a9x0001?range=7d" \
  -H "Authorization: Bearer $QRBOLD_API_KEY"
200 response
{
  "object": "scan_stats",
  "qrCodeId": "cmf3k2a9x0001",
  "range": "7d",
  "period": {
    "start": "2026-10-02T00:00:00.000Z",
    "end": "2026-10-09T09:40:00.000Z",
    "days": 7
  },
  "clamped": false,
  "historyDays": -1,
  "totalScans": 120,
  "uniqueVisitors": 80,
  "previousPeriodScans": 100,
  "trend": "+20%",
  "timeline": [
    {
      "date": "2026-10-07",
      "scans": 6
    },
    {
      "date": "2026-10-08",
      "scans": 9
    }
  ],
  "topCountries": [
    {
      "country": "United Kingdom",
      "code": "GB",
      "scans": 74
    }
  ],
  "devices": [
    {
      "device": "Mobile",
      "scans": 108,
      "percent": 90
    }
  ]
}

The field is called qrCodeId even when the id is a card’s. An id that does not exist in the account answers 404.

Account overview

GET/analytics/overview
curl "https://api.qrbold.com/api/public/v1/analytics/overview?range=30d&timeZone=Europe%2FLondon" \
  -H "Authorization: Bearer $QRBOLD_API_KEY"
200 response
{
  "object": "scan_stats",
  "qrCodeId": null,
  "range": "30d",
  "period": {
    "start": "2026-09-09T00:00:00.000Z",
    "end": "2026-10-09T09:40:00.000Z",
    "days": 30
  },
  "clamped": false,
  "historyDays": -1,
  "totalScans": 120,
  "uniqueVisitors": 80,
  "previousPeriodScans": 100,
  "trend": "+20%",
  "timeline": [
    {
      "date": "2026-10-07",
      "scans": 6
    },
    {
      "date": "2026-10-08",
      "scans": 9
    }
  ],
  "topCountries": [
    {
      "country": "United Kingdom",
      "code": "GB",
      "scans": 74
    }
  ],
  "devices": [
    {
      "device": "Mobile",
      "scans": 108,
      "percent": 90
    }
  ],
  "topQrCodes": [
    {
      "id": "cmf3k2a9x0001",
      "name": "Ada Lovelace",
      "type": "card",
      "scans": 74
    },
    {
      "id": "cmf3k4c2e0003",
      "name": "Spring menu",
      "type": "url",
      "scans": 46
    }
  ]
}

The same object, for every card and code in the account together. qrCodeId is null, and topQrCodes lists the five busiest items, where type tells a card from a url code.

Ranges and time zones

Analytics query parameters
NameTypeRequiredDescription
rangestringOptionalThe period to report. One of 7d, 30d, 90d, 12m. Default: 30d.
timeZonestringOptionalIANA time zone the daily buckets are cut in. Default: UTC.
  • The period is one of four fixed ranges. Custom start and end dates are not supported.
  • timeZone decides where one day ends and the next begins in timeline. Without it, days are cut in UTC, so a scan at 11pm in New York lands on the next day’s row. Send the IANA name of your audience’s zone, such as America/New_York.
  • A range outside those four answers 422. A time zone name is not checked in the same way, so make sure you send a real IANA name.

Plan limits on analytics

Two things depend on the account’s plan, and the response tells you about both.

How a plan affects analytics responses
LimitHow it shows in the response
How far back history goeshistoryDays is the number of days the plan keeps, or -1 for no limit. If you ask for a longer range, you get the shorter one and clamped is true. period shows what was really covered.
Country and device breakdownstopCountries and devices are absent, not empty, on a plan without advanced analytics.

Treat a missing topCountries as “not available on this plan”, and an empty array as “no scans with a known country yet”. They are different.

Privacy and what is not returned

The analytics endpoints return aggregates only. They never return:

  • IP addresses.
  • Precise locations. Country is the finest level of geography.
  • Individual scan records, visitor identifiers or timestamps of single scans.
  • Contact details that visitors submitted on a card. Those are leads, and are not in the API.

There are also no webhooks for scans. To keep your own numbers current, call these endpoints on a schedule, for example once an hour. See webhooks.