Analytics

Read click and scan figures for the workspace or one link, watch clicks as they arrive, and export them as CSV.

Figures are kept per day of the workspace's analytics time zone (Settings → Analytics time zone). How far back you can look depends on your plan's analytics history.

Every path below starts with https://bgum.me/api/v1/workspaces/{workspaceId}. Send your API key in the Authorization header as described in the API overview.

OperationMethod and path
Workspace reportGET /analytics
Link reportGET /links/{linkId}/analytics
Export smart routing as CSVGET /links/{linkId}/analytics/routes.csv
Latest clicksGET /analytics/live
Clicks per minuteGET /analytics/live/minutes
Stream of clicksGET /analytics/live/stream
Export as CSVGET /analytics/export.csv

Workspace report

GET /analytics

Totals, a day-by-day series, breakdowns, top links and chart notes for the whole workspace.

Full path
/api/v1/workspaces/{workspaceId}/analytics
API key scope
analytics:read
Role needed
Viewer or above

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.

Query parameters

NameTypeRequiredDescription
from date No First day, as YYYY-MM-DD, in the report's time zone. Defaults to 29 days before to.
to date No Last day, as YYYY-MM-DD. Defaults to today in the report's time zone. Future days are ignored.
top integer No How many rows to return in each breakdown and in topLinks: 1 to 100, default 10.
timeZone string No IANA time zone to count days in, such as Australia/Sydney or UTC. Defaults to the workspace's analytics time zone (Settings → Analytics time zone). Another zone is counted from raw clicks, so the period must fall within the raw clicks kept (usually 90 days) and the report is slower.

Example request

curl

curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/analytics?from=2026-10-01&to=2026-10-14&top=5" \
  -H "Authorization: Bearer $BGL_API_KEY"

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/analytics?from=2026-10-01&to=2026-10-14&top=5`, {
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
  },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);

Python

import os
import requests

res = requests.get(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/analytics?from=2026-10-01&to=2026-10-14&top=5",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · application/json

JSON

{
  "timeZone": "Australia/Sydney",
  "range": {
    "from": "2026-10-01",
    "to": "2026-10-14",
    "days": 14
  },
  "effectiveRange": {
    "from": "2026-10-01",
    "to": "2026-10-14",
    "days": 14
  },
  "clampedByRetention": false,
  "totals": {
    "clicks": 1290,
    "humanClicks": 1184,
    "qrScans": 212,
    "uniqueVisitorsEstimate": 1022,
    "automatedClicks": 106
  },
  "previousTotals": {
    "clicks": 940,
    "humanClicks": 871,
    "qrScans": 150,
    "uniqueVisitorsEstimate": 790,
    "automatedClicks": 69
  },
  "series": [
    {
      "day": "2026-10-01",
      "clicks": 88,
      "humanClicks": 80,
      "qrScans": 12,
      "uniqueVisitorsEstimate": 71
    },
    {
      "day": "2026-10-02",
      "clicks": 102,
      "humanClicks": 95,
      "qrScans": 17,
      "uniqueVisitorsEstimate": 83
    }
  ],
  "dimensions": {
    "referrer": [
      {
        "value": "(direct)",
        "clicks": 610,
        "humanClicks": 590
      },
      {
        "value": "www.linkedin.com",
        "clicks": 301,
        "humanClicks": 297
      }
    ],
    "country": [
      {
        "value": "AU",
        "clicks": 902,
        "humanClicks": 860
      }
    ],
    "device": [
      {
        "value": "mobile",
        "clicks": 700,
        "humanClicks": 688
      },
      {
        "value": "desktop",
        "clicks": 480,
        "humanClicks": 470
      }
    ],
    "traffic": [
      {
        "value": "LikelyHuman",
        "clicks": 1184,
        "humanClicks": 1184
      },
      {
        "value": "LinkPreview",
        "clicks": 74,
        "humanClicks": 0
      }
    ],
    "channel": [
      {
        "value": "link",
        "clicks": 1078,
        "humanClicks": 972
      },
      {
        "value": "qr",
        "clicks": 212,
        "humanClicks": 212
      }
    ]
  },
  "topLinks": [
    {
      "linkId": "ZiZihoWXe3as",
      "shortUrl": "https://bgum.me/spring-sale",
      "title": "Spring sale",
      "destinationUrl": "https://example.com/spring-sale",
      "humanClicks": 640,
      "clicks": 701
    }
  ],
  "conversions": {
    "conversions": 37,
    "rate": 0.03125,
    "values": [
      {
        "currency": "AUD",
        "value": 1848.15
      }
    ],
    "byEvent": [
      {
        "event": "purchase",
        "conversions": 37,
        "values": [
          {
            "currency": "AUD",
            "value": 1848.15
          }
        ]
      }
    ],
    "byCampaign": [
      {
        "campaignId": "aiQrz1XgekFP",
        "name": "Spring sale",
        "conversions": 37,
        "values": [
          {
            "currency": "AUD",
            "value": 1848.15
          }
        ]
      }
    ]
  },
  "notes": [
    {
      "id": "nT4wq0Xbe2Lk",
      "date": "2026-10-02",
      "time": "09:30:00",
      "text": "Spring newsletter sent",
      "linkId": null,
      "campaignId": "aiQrz1XgekFP",
      "createdBy": "Sam Taylor",
      "createdAt": "2026-10-02T00:14:51.4410+00:00",
      "updatedAt": null
    }
  ],
  "earlierDays": null
}

Good to know

  • Days run from midnight to midnight in timeZone, which the response repeats. A day when daylight saving starts or ends has 23 or 25 hours. If the range goes back further than your plan's analytics history, it's shortened: effectiveRange shows the days actually reported and clampedByRetention is true.
  • earlierDays is set when the workspace changed time zone and some days in the range are still counted in the earlier zone (raw clicks from back then were no longer kept): days before earlierDays.before are in earlierDays.timeZone.
  • notes lists the chart notes dated in the range (see Chart notes). On a link report: that link's notes, its campaign's and the workspace-wide ones.
  • previousTotals covers the same number of days immediately before effectiveRange, for comparisons.
  • humanClicks leaves out known bots, link previews and security scanners; clicks includes them. uniqueVisitorsEstimate is a sum of daily estimates per link, so one person on two days counts twice.
  • dimensions has a list per breakdown: referrer, country, device, browser, os, traffic, bot, utm_source, utm_medium, utm_campaign and channel (link or qr). A breakdown with no data is left out. Ignore keys you don't recognise; more may be added.
  • conversions counts conversions that happened in the window (by day in timeZone), with value totals per currency, and a breakdown by event name (and, for the workspace, by campaign). rate is conversions ÷ clicks from people in the same window, or null when there were none. See Conversions.

Errors

StatusCodeMeaning
422analytics.time_zone_invalidtimeZone isn't a time zone we know.
422analytics.time_zone_historyA report in a zone other than the workspace's is counted from raw clicks, and the period starts before the oldest raw clicks kept.

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

GET /links/{linkId}/analytics

The same report for one link. topLinks is always empty.

Full path
/api/v1/workspaces/{workspaceId}/links/{linkId}/analytics
API key scope
analytics:read
Role needed
Viewer or above

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.
linkId string Yes The link's ID, as returned when it was created or listed.

Query parameters

NameTypeRequiredDescription
from date No First day, as YYYY-MM-DD, in the report's time zone. Defaults to 29 days before to.
to date No Last day, as YYYY-MM-DD. Defaults to today in the report's time zone. Future days are ignored.
top integer No How many rows to return in each breakdown and in topLinks: 1 to 100, default 10.
timeZone string No IANA time zone to count days in, such as Australia/Sydney or UTC. Defaults to the workspace's analytics time zone (Settings → Analytics time zone). Another zone is counted from raw clicks, so the period must fall within the raw clicks kept (usually 90 days) and the report is slower.

Example request

Response

200 OK · application/json

JSON

{
  "timeZone": "Australia/Sydney",
  "range": {
    "from": "2026-10-01",
    "to": "2026-10-14",
    "days": 14
  },
  "effectiveRange": {
    "from": "2026-10-01",
    "to": "2026-10-14",
    "days": 14
  },
  "clampedByRetention": false,
  "totals": {
    "clicks": 1290,
    "humanClicks": 1184,
    "qrScans": 212,
    "uniqueVisitorsEstimate": 1022,
    "automatedClicks": 106
  },
  "previousTotals": {
    "clicks": 940,
    "humanClicks": 871,
    "qrScans": 150,
    "uniqueVisitorsEstimate": 790,
    "automatedClicks": 69
  },
  "series": [
    {
      "day": "2026-10-01",
      "clicks": 88,
      "humanClicks": 80,
      "qrScans": 12,
      "uniqueVisitorsEstimate": 71
    },
    {
      "day": "2026-10-02",
      "clicks": 102,
      "humanClicks": 95,
      "qrScans": 17,
      "uniqueVisitorsEstimate": 83
    }
  ],
  "dimensions": {
    "referrer": [
      {
        "value": "(direct)",
        "clicks": 610,
        "humanClicks": 590
      },
      {
        "value": "www.linkedin.com",
        "clicks": 301,
        "humanClicks": 297
      }
    ],
    "country": [
      {
        "value": "AU",
        "clicks": 902,
        "humanClicks": 860
      }
    ],
    "device": [
      {
        "value": "mobile",
        "clicks": 700,
        "humanClicks": 688
      },
      {
        "value": "desktop",
        "clicks": 480,
        "humanClicks": 470
      }
    ],
    "traffic": [
      {
        "value": "LikelyHuman",
        "clicks": 1184,
        "humanClicks": 1184
      },
      {
        "value": "LinkPreview",
        "clicks": 74,
        "humanClicks": 0
      }
    ],
    "channel": [
      {
        "value": "link",
        "clicks": 1078,
        "humanClicks": 972
      },
      {
        "value": "qr",
        "clicks": 212,
        "humanClicks": 212
      }
    ]
  },
  "topLinks": [],
  "conversions": {
    "conversions": 37,
    "rate": 0.0578,
    "values": [
      {
        "currency": "AUD",
        "value": 1848.15
      }
    ],
    "byEvent": [
      {
        "event": "purchase",
        "conversions": 37,
        "values": [
          {
            "currency": "AUD",
            "value": 1848.15
          }
        ]
      }
    ],
    "byCampaign": []
  },
  "notes": [
    {
      "id": "nT4wq0Xbe2Lk",
      "date": "2026-10-02",
      "time": "09:30:00",
      "text": "Spring newsletter sent",
      "linkId": null,
      "campaignId": "aiQrz1XgekFP",
      "createdBy": "Sam Taylor",
      "createdAt": "2026-10-02T00:14:51.4410+00:00",
      "updatedAt": null
    }
  ],
  "earlierDays": null
}

Good to know

  • Works for deleted links too, so you can still report on them.
  • For a link with smart routing, routes lists each rule and split destination: route is the recorded key (rule:<id>, split:<id>, or default for visitors no rule matched), kind is rule, split or default, and label and destinationUrl are the current ones. Each has clicks, humanClicks, conversions, conversionRate (conversions ÷ humanClicks) and values. Without routing, routes is null.

Errors

StatusCodeMeaning
404link.not_foundNo link with that ID in this workspace (or it was deleted).
422analytics.time_zone_invalidtimeZone isn't a time zone we know.
422analytics.time_zone_historyA report in a zone other than the workspace's is counted from raw clicks, and the period starts before the oldest raw clicks kept.

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

GET /links/{linkId}/analytics/routes.csv

One row per day (in the workspace's time zone) for each smart-routing rule and split destination of a link that had clicks or conversions (Pro and above).

Full path
/api/v1/workspaces/{workspaceId}/links/{linkId}/analytics/routes.csv
API key scope
analytics:read
Role needed
Analyst or above
Plan
Needs csv export in your plan (compare plans)

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.
linkId string Yes The link's ID, as returned when it was created or listed.

Query parameters

NameTypeRequiredDescription
from date No First day, as YYYY-MM-DD, in the report's time zone. Defaults to 29 days before to.
to date No Last day, as YYYY-MM-DD. Defaults to today in the report's time zone. Future days are ignored.

Example request

Response

200 OK · text/csv

day,route,label,destination_url,clicks,human_clicks,conversions
2026-10-01,split:a,Short page,https://example.com/landing-a,64,60,4
2026-10-01,split:b,Long page,https://example.com/landing-b,61,58,7

Good to know

  • label and destination_url are the rule's or destination's current ones; a rule removed since shows as Removed rule.

Errors

StatusCodeMeaning
404link.not_foundNo link with that ID in this workspace (or it was deleted).

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

Latest clicks

GET /analytics/live

Clicks from the last hour as they arrive, oldest first. Poll with after set to the cursor from the previous response to get only new ones.

Full path
/api/v1/workspaces/{workspaceId}/analytics/live
API key scope
analytics:read
Role needed
Viewer or above

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.

Query parameters

NameTypeRequiredDescription
after integer No The cursor from the previous response. Leave it out to get the latest clicks of the last hour.
limit integer No At most this many clicks: 1 to 200, default 50.
linkId string No Only this link.

Example request

curl

curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/analytics/live?after=918270&limit=100" \
  -H "Authorization: Bearer $BGL_API_KEY"

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/analytics/live?after=918270&limit=100`, {
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
  },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);

Python

import os
import requests

res = requests.get(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/analytics/live?after=918270&limit=100",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · application/json

JSON

{
  "clicks": [
    {
      "id": 918274,
      "at": "2026-10-15T03:12:09.5521+00:00",
      "linkId": "ZiZihoWXe3as",
      "shortUrl": "https://bgum.me/spring-sale",
      "title": "Spring sale",
      "country": "AU",
      "device": "mobile",
      "browser": "Safari",
      "os": "iOS",
      "referrer": "www.linkedin.com",
      "qr": false,
      "utmSource": "newsletter",
      "utmMedium": "email",
      "utmCampaign": "spring-2026",
      "traffic": "LikelyHuman",
      "bot": null
    }
  ],
  "cursor": 918274
}

Good to know

  • Clicks usually appear within a few seconds. Poll no more than every few seconds; the stream below sends them as they arrive instead.
  • Each click shows what the analytics breakdowns show (link, country, device, browser, operating system, referring site, QR code, UTM tags, and traffic: LikelyHuman, KnownBot, LinkPreview or SecurityScanner) and never addresses or visitor identifiers.
  • With no new clicks, clicks is empty and cursor stays the same.

Errors

StatusCodeMeaning
404link.not_foundNo link with that ID in this workspace (or it was deleted).

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

Clicks per minute

GET /analytics/live/minutes

Clicks per minute over the last hour (or fewer minutes), oldest minute first, including minutes with none.

Full path
/api/v1/workspaces/{workspaceId}/analytics/live/minutes
API key scope
analytics:read
Role needed
Viewer or above

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.

Query parameters

NameTypeRequiredDescription
minutes integer No How many minutes back: 1 to 60, default 60. The last one is the current, unfinished minute.
linkId string No Only this link.

Example request

curl

curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/analytics/live/minutes?minutes=5" \
  -H "Authorization: Bearer $BGL_API_KEY"

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/analytics/live/minutes?minutes=5`, {
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
  },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);

Python

import os
import requests

res = requests.get(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/analytics/live/minutes?minutes=5",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · application/json

JSON

{
  "items": [
    {
      "minute": "2026-10-15T03:08:00+00:00",
      "clicks": 4,
      "humanClicks": 3
    },
    {
      "minute": "2026-10-15T03:09:00+00:00",
      "clicks": 0,
      "humanClicks": 0
    },
    {
      "minute": "2026-10-15T03:10:00+00:00",
      "clicks": 7,
      "humanClicks": 7
    },
    {
      "minute": "2026-10-15T03:11:00+00:00",
      "clicks": 2,
      "humanClicks": 1
    },
    {
      "minute": "2026-10-15T03:12:00+00:00",
      "clicks": 1,
      "humanClicks": 1
    }
  ]
}

Errors

StatusCodeMeaning
404link.not_foundNo link with that ID in this workspace (or it was deleted).

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

Stream of clicks

GET /analytics/live/stream

Server-sent events: each new click as it arrives, and clicks per minute every 30 seconds. This is what the Live page in the app uses.

Full path
/api/v1/workspaces/{workspaceId}/analytics/live/stream
API key scope
analytics:read
Role needed
Viewer or above

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.

Query parameters

NameTypeRequiredDescription
after integer No Start after this cursor. A reconnecting EventSource sends Last-Event-ID instead, which wins.
linkId string No Only this link.

Example request

curl

curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/analytics/live/stream" \
  -H "Authorization: Bearer $BGL_API_KEY"

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/analytics/live/stream`, {
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
  },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);

Python

import os
import requests

res = requests.get(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/analytics/live/stream",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · text/event-stream

retry: 3000
event: ready
data: {"cursor":918274}

id: 918275
event: click
data: {"id":918275,"at":"2026-10-15T03:12:41.0712+00:00","linkId":"ZiZihoWXe3as","shortUrl":"https://bgum.me/spring-sale","title":"Spring sale","country":"AU","device":"mobile","browser":"Chrome","os":"Android","referrer":null,"qr":true,"utmSource":null,"utmMedium":null,"utmCampaign":null,"traffic":"LikelyHuman","bot":null}

event: minutes
data: [{"minute":"2026-10-15T03:12:00+00:00","clicks":2,"humanClicks":2}]

: keep-alive

Good to know

  • The stream starts with a ready event, then the latest clicks of the last hour (each a click event whose id is the cursor), then a minutes event. A comment line every 15 seconds keeps proxies from closing it.
  • The server ends the stream after about 10 minutes; EventSource reconnects by itself and carries on from Last-Event-ID, so nothing is missed or repeated.
  • API keys work too, but browsers' EventSource can't send an Authorization header: from a browser, sign in and use the session instead.

Errors

StatusCodeMeaning
404link.not_foundNo link with that ID in this workspace (or it was deleted).

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

Export as CSV

GET /analytics/export.csv

One row per link per day with any clicks or conversions (Pro and above).

Full path
/api/v1/workspaces/{workspaceId}/analytics/export.csv
API key scope
analytics:read
Role needed
Analyst or above
Plan
Needs csv export in your plan (compare plans)

Path parameters

NameTypeRequiredDescription
workspaceId string Yes Your workspace ID, from Settings → Workspace.

Query parameters

NameTypeRequiredDescription
from date No First day, as YYYY-MM-DD, in the report's time zone. Defaults to 29 days before to.
to date No Last day, as YYYY-MM-DD. Defaults to today in the report's time zone. Future days are ignored.
linkId string No Only this link.
timeZone string No IANA time zone to count days in, such as Australia/Sydney or UTC. Defaults to the workspace's analytics time zone (Settings → Analytics time zone). Another zone is counted from raw clicks, so the period must fall within the raw clicks kept (usually 90 days) and the report is slower.

Example request

curl

curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/analytics/export.csv?from=2026-10-01&to=2026-10-31" \
  -H "Authorization: Bearer $BGL_API_KEY" \
  -o clicks.csv

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/analytics/export.csv?from=2026-10-01&to=2026-10-31`, {
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
  },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const file = Buffer.from(await res.arrayBuffer()); // save as clicks.csv

Python

import os
import requests

res = requests.get(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/analytics/export.csv?from=2026-10-01&to=2026-10-31",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
open("clicks.csv", "wb").write(res.content)

Response

200 OK · text/csv

day,link_id,short_url,destination_url,clicks,human_clicks,qr_scans,unique_visitors_estimate,conversions
2026-10-01,ZiZihoWXe3as,https://bgum.me/spring-sale,https://example.com/spring-sale,88,80,12,71,3

Good to know

  • Needs the Analyst role or above. Days are in timeZone, as in the reports.

Errors

StatusCodeMeaning
422analytics.time_zone_invalidtimeZone isn't a time zone we know.
422analytics.time_zone_historyA report in a zone other than the workspace's is counted from raw clicks, and the period starts before the oldest raw clicks kept.

Any request can also return the general errors: 401 for a missing or wrong key, 403 for a missing scope or plan feature, 404 for an unknown workspace and 429 when you're sending too fast.

Something missing or unclear? Tell us and we'll improve this page.