logo
AnalyticsAPIAutomations

Read analytics from the API, MCP, and webhooks

Read clicks and scans with GET /analytics and GET /events, ask for them through the CodeQR MCP server, and get link.clicked and qrcode.scanned webhooks.

Avatar for undefined
CodeQR Team
Content Team

By the end of this guide you can read the same numbers you see in Analytics from your own code — totals, time series, breakdowns, top links, UTM — list individual events, ask an AI assistant for them through the MCP server, and get a webhook on every click or scan.

Availability

  • Plan: GET /analytics on all plans, with the plan's date window (Free 30 days, Starter 90, Pro 1 year, Business+ unlimited). GET /events from Pro. link.clicked and qrcode.scanned webhooks from Business.
  • Where: https://api.codeqr.io/analytics and https://api.codeqr.io/events with a key from SettingsAPI Keys (scope analytics.read); the MCP server at https://mcp.codeqr.io/mcp; webhooks in SettingsWebhooks. Full field reference: Retrieve analytics and Retrieve a list of events on docs.codeqr.io.

Before you start

  • Authenticate with Authorization: Bearer <api key>. Keys are per workspace; the workspace is implied.
  • Aggregates are cached for two to ten minutes, the same as the dashboard. Poll no faster than that.
  • IDs: linkId and qrcodeId are the id fields returned by /links and /qrcodes; you can also address a link by domain + key + type=link (or type=qrcode).
  • The examples below were run against a real workspace on 2026-08-17; only the domain was replaced by go.example.com.

Steps

  1. Count. GET https://api.codeqr.io/analytics?event=composite&groupBy=count&interval=24h&linkId=cmsxdw1ww0001ulo2n1new0ne
{"clicks":9,"scans":0,"views":0,"leads":0,"sales":0,"saleAmount":0}

event is clicks, scans, views, leads, sales, or composite (all fields filled). Without linkId the same call returns the workspace totals.

  1. Time series. GET https://api.codeqr.io/analytics?event=clicks&groupBy=timeseries&interval=24h&timezone=America/Sao_Paulo&linkId=cmsxdw1ww0001ulo2n1new0ne returns one object per bucket (hourly for 24h, daily for 7d and 30d, monthly from 90d), each with start in the given timezone:
[{"start":"2026-08-17T15:00:00.000+0000","clicks":9,"scans":0,"views":0,"leads":0,"sales":0,"saleAmount":0}]
  1. Breakdowns. Change groupBy to countries, cities, continents, devices, browsers, os, referers, referer_urls, top_links, top_qrcodes, top_urls (needs linkId), or utm_sources, utm_mediums, utm_campaigns, utm_contents, utm_terms:
GET /analytics?event=clicks&groupBy=referers&interval=24h&linkId=cmsxdw1ww0001ulo2n1new0ne
[{"referer":"(direct)","clicks":7,"leads":0,"sales":0,"saleAmount":0},{"referer":"mail.google.com","clicks":1,"leads":0,"sales":0,"saleAmount":0},{"referer":"instagram.com","clicks":1,"leads":0,"sales":0,"saleAmount":0}]

GET /analytics?event=clicks&groupBy=top_links&interval=24h
[{"link":"cmsxdw1ww0001ulo2n1new0ne","id":"cmsxdw1ww0001ulo2n1new0ne","domain":"go.example.com","key":"docs-analytics-utm","shortLink":"https://go.example.com/docs-analytics-utm","url":"https://example.com/newsletter?utm_source=newsletter&utm_medium=email&utm_campaign=august-2026","createdAt":"2026-08-17T15:23:49.808Z","clicks":9,"leads":0},{"link":"cmsxdw3qk0003bhs7k6msb5og","id":"cmsxdw3qk0003bhs7k6msb5og","domain":"go.example.com","key":"docs-analytics-plain","shortLink":"https://go.example.com/docs-analytics-plain","url":"https://example.com/promo","createdAt":"2026-08-17T15:23:52.172Z","clicks":2,"leads":0}]

GET /analytics?event=clicks&groupBy=utm_sources&interval=24h&linkId=cmsxdw1ww0001ulo2n1new0ne
[{"utm_source":"newsletter","clicks":9}]

Breakdowns count clicks, scans, and page views together; add types=link, types=qrcode, or types=page to keep one kind. Breakdowns are returned on every plan (the app blurs some cards, the API does not).

  1. Filters and periods. Any of domain, key, linkId, qrcodeId, tagIds, folderId, country, city, continent, device, browser, referer, url, utm_sourceutm_term narrows the result; interval is 24h, 7d, 30d, 90d, ytd, 1y, or all, or send start and end (ISO 8601; end cannot be in the future) with timezone. Example: GET /analytics?event=clicks&groupBy=count&interval=24h&country=BR&device=Mobile.
  2. Events. GET https://api.codeqr.io/events?event=clicks&interval=24h&linkId=cmsxdw1ww0001ulo2n1new0ne&limit=3 (Pro and above) returns one object per click, newest first; page and limit (default 100) paginate, order=asc reverses:
[{"event":"click","timestamp":"Mon Aug 17 2026 15:27:57 GMT+0000 (Coordinated Universal Time)","click":{"id":"iPjJ1eT6yjCB1mGw","url":"https://example.com/newsletter?utm_source=newsletter&utm_medium=email&utm_campaign=august-2026","continent":"SA","country":"BR","city":"Carnaíba","device":"Mobile","browser":"Mobile Safari","os":"iOS","referer":"mail.google.com","refererUrl":"","type":"link","ip":""},"link":{"id":"cmsxdw1ww0001ulo2n1new0ne","domain":"go.example.com","key":"docs-analytics-utm","url":"https://example.com/newsletter?utm_source=newsletter&utm_medium=email&utm_campaign=august-2026","shortLink":"https://go.example.com/docs-analytics-utm","clicks":9,"leads":0,"sales":0,"saleAmount":0,"lastClicked":"2026-08-17T15:27:57.000Z","createdAt":"2026-08-17T15:23:49.808Z","utm_source":"newsletter","utm_medium":"email","utm_campaign":"august-2026","publicStats":true,"tags":[]},"click_id":"iPjJ1eT6yjCB1mGw","link_id":"cmsxdw1ww0001ulo2n1new0ne","domain":"go.example.com","key":"docs-analytics-utm","url":"https://example.com/newsletter?utm_source=newsletter&utm_medium=email&utm_campaign=august-2026","continent":"SA","country":"BR","city":"Carnaíba","device":"Mobile","browser":"Mobile Safari","os":"iOS","type":"link","ip":""}]

click.type is link, qrcode, or page; click.ip is always empty. Use types=qrcode to list scans only. event=leads and event=sales list conversion events when conversion tracking is on.

  1. Errors you will meet. 400 invalid_enum_value: interval: Invalid enum value. Expected '1h' | '24h' | '7d' | '30d' | '90d' | 'ytd' | '1y' | 'all' | 'all_unfiltered', received '2d'; 400 custom: end: The end date cannot be in future.; 403 You can only get analytics for up to 30 days on a free plan. Upgrade to Starter, Pro, or Business to get analytics for longer periods. (and the Starter/Pro equivalents); 403 No permission: You need a higher plan. on /events below Pro; 404 Link not found.

The same through the MCP server

Connect your AI client to https://mcp.codeqr.io/mcp (OAuth 2.0). Ask, for example:

How many scans did my QR codes get in the last 7 days, by country?

The client calls get_analytics with { "event": "scans", "groupBy": "countries", "interval": "7d" } and reads the JSON above. get_analytics takes event (clicks, scans, leads, sales, composite), groupBy (count, timeseries, countries, cities, devices, browsers, os, referers, top_links, top_qrcodes, top_urls), and optionally linkId, qrcodeId, domain + key, and interval (defaults to 24h). Custom start/end, timezone, and the tag/country/device filters are API-only. The plan's date window applies: a Free workspace asking for 1y gets the 403 above.

Webhooks and automations

  • Webhooks: in SettingsWebhooks, create an endpoint and choose the trigger. link.clicked and qrcode.scanned (and page.visited) are picked per link, QR code, or page and are available from Business; the other triggers (link.created, lead.created, sale.created, …) are workspace-wide from Pro. A link.clicked delivery is { "id": "evt_…", "event": "link.clicked", "createdAt": "…", "data": { "eventName": "Link clicked", "interaction": { "timestamp", "click_id", "link_id", "url", "country", "city", "device", "browser", "os", "referer", "referer_url", "type", … }, "link": { … } } } — read the click from data.interaction. The Send test event button still sends an older sample with data.click; real deliveries use data.interaction. Signature and retries: docs.codeqr.io/concepts/webhooks/introduction.
  • Google Sheets: the Pluga recipe in Log data to Google Sheets automatically writes one row per event from a webhook.
  • Make and Zapier: Zapier lists link.clicked as a trigger (no qrcode.scanned); Make has no click trigger — schedule an HTTP module that calls GET /analytics?…&groupBy=count&interval=24h daily and writes the result where you need it.
  • SDK: npm install @codeqr/ts, then const client = new CodeQR({ token: process.env.CODEQR_API_KEY }); const totals = await client.analytics.retrieve({ event: 'composite', groupBy: 'count', interval: '30d' });

Verify it works

curl -s "https://api.codeqr.io/analytics?event=composite&groupBy=count&interval=24h" \
  -H "Authorization: Bearer $CODEQR_API_KEY"
# {"clicks":68,"scans":16,"views":3,"leads":1,"sales":0,"saleAmount":0}

Open one of your links on your phone, wait two to three minutes, and run the call again with &linkId=<id>: clicks is 1.

Troubleshooting

groupBy=top_urls returns an empty list

top_urls needs a specific link: add linkId or domain + key + type=link.

qrcodeId on /events returns link clicks

/events filters by link only. Use types=qrcode to list scans, then read link_id / key on each row.

The response is 403 with an upgrade message

The interval (or start) is older than your plan's window. Use a shorter range or upgrade; the message names the plan that opens the range.

The count is lower than the dashboard

Check event: clicks alone excludes scans and views; the dashboard's Total is event=composite summed. Also compare the same timezone and the same window.

Related articles