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.

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 /analyticson all plans, with the plan's date window (Free 30 days, Starter 90, Pro 1 year, Business+ unlimited).GET /eventsfrom Pro.link.clickedandqrcode.scannedwebhooks from Business. - Where:
https://api.codeqr.io/analyticsandhttps://api.codeqr.io/eventswith a key from Settings → API Keys (scopeanalytics.read); the MCP server athttps://mcp.codeqr.io/mcp; webhooks in Settings → Webhooks. 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:
linkIdandqrcodeIdare theidfields returned by/linksand/qrcodes; you can also address a link bydomain+key+type=link(ortype=qrcode). - The examples below were run against a real workspace on 2026-08-17; only the domain was replaced by
go.example.com.
Steps
- 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.
- Time series.
GET https://api.codeqr.io/analytics?event=clicks&groupBy=timeseries&interval=24h&timezone=America/Sao_Paulo&linkId=cmsxdw1ww0001ulo2n1new0nereturns one object per bucket (hourly for24h, daily for7dand30d, monthly from90d), each withstartin the giventimezone:
[{"start":"2026-08-17T15:00:00.000+0000","clicks":9,"scans":0,"views":0,"leads":0,"sales":0,"saleAmount":0}]- Breakdowns. Change
groupBytocountries,cities,continents,devices,browsers,os,referers,referer_urls,top_links,top_qrcodes,top_urls(needslinkId), orutm_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).
- Filters and periods. Any of
domain,key,linkId,qrcodeId,tagIds,folderId,country,city,continent,device,browser,referer,url,utm_source…utm_termnarrows the result;intervalis24h,7d,30d,90d,ytd,1y, orall, or sendstartandend(ISO 8601;endcannot be in the future) withtimezone. Example:GET /analytics?event=clicks&groupBy=count&interval=24h&country=BR&device=Mobile. - 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;pageandlimit(default 100) paginate,order=ascreverses:
[{"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.
- Errors you will meet.
400invalid_enum_value: interval: Invalid enum value. Expected '1h' | '24h' | '7d' | '30d' | '90d' | 'ytd' | '1y' | 'all' | 'all_unfiltered', received '2d';400custom: end: The end date cannot be in future.;403You 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);403No permission: You need a higher plan.on/eventsbelow Pro;404Link 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 Settings → Webhooks, create an endpoint and choose the trigger.
link.clickedandqrcode.scanned(andpage.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. Alink.clickeddelivery 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 fromdata.interaction. The Send test event button still sends an older sample withdata.click; real deliveries usedata.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.clickedas a trigger (noqrcode.scanned); Make has no click trigger — schedule an HTTP module that callsGET /analytics?…&groupBy=count&interval=24hdaily and writes the result where you need it. - SDK:
npm install @codeqr/ts, thenconst 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.