API overview
Authentication, workspace IDs, request and response formats, errors, rate limits, retries and paging.
Quick start
The API does what the app does: create and manage links, QR codes, campaigns and custom domains, and read analytics. It's included in the Business plan.
- Create an API key. In the app, open API keys (you need the Administrator or Owner role). Give the key a name, choose when it expires and tick only the permissions it needs, then choose Create key.
-
Copy the key straight away. It starts with
bgl_and is shown only once. We keep only a scrambled copy, so we can't show it again. If you lose it, revoke it and make a new one. - Find your workspace ID. Open Settings. Under Workspace, copy the Workspace ID for the API.
-
Keep both somewhere safe, such as environment variables. The examples in these docs read them from
BGL_API_KEYandWORKSPACE_ID:Shell (macOS, Linux, WSL)
export BGL_API_KEY="bgl_…your key…" export WORKSPACE_ID="U95AmzlOd2V7"PowerShell (Windows)
$env:BGL_API_KEY = "bgl_…your key…" $env:WORKSPACE_ID = "U95AmzlOd2V7" -
Make your first request. This lists your five newest links:
curl
curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/links?pageSize=5" \ -H "Authorization: Bearer $BGL_API_KEY"JavaScript
const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/links?pageSize=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']}/links?pageSize=5", headers={ "Authorization": f"Bearer {os.environ['BGL_API_KEY']}", }, timeout=30, ) res.raise_for_status() print(res.json())You'll get back
200 OKand a JSON list. If you get401, check the key; if you get404, check the workspace ID. - Next, create a link or browse the endpoints in the menu.
Authentication
Send your API key with every request, in the Authorization header:
HTTP header
Authorization: Bearer bgl_4Kx9…
- One workspace per key. A key only works for the workspace it was created in. Use a separate key for each workspace.
- Acts as its creator. A key can do what the person who created it can do in the workspace today, limited further by its scopes. If that person's role changes, so does the key. If they leave the workspace, the key stops working (
apikey.owner_removed). - Two-factor rules apply. If the organisation requires two-factor authentication, the key's creator must have it turned on, or the key gets
organisation.mfa_required. - Expiry and revoking. Keys can expire after 30, 90 or 365 days, or never. Revoke a key any time in API keys; it stops working straight away. The page shows when each key was last used.
- Keep keys secret. Use keys only from your own servers or scripts. Never put them in web pages, mobile apps or public code repositories. If a key leaks, revoke it.
Scopes
Scopes are the permissions you tick when you create a key. Give each key only the scopes it needs.
| Scope | Allows | Endpoints |
|---|---|---|
links:read |
Read links, QR codes and campaigns, check back-halves, and see whether destinations still work. | Links, Folders, Tags, Saved views, UTM templates, QR codes, Campaigns, Link health |
links:write |
Create, change and delete links, including in bulk, and check or ignore their destinations. | Links, Folders, Tags, UTM templates, Link health |
analytics:read |
Read analytics, live clicks and chart notes, and export analytics as CSV. | Analytics, Chart notes |
analytics:write |
Add, change and delete chart notes. | Chart notes |
qr:write |
Create, restyle and archive QR codes, and set their logos. | QR codes |
campaigns:write |
Create, change and archive campaigns. | Campaigns |
domains:read |
List custom domains and see their DNS status. | Custom domains |
domains:write |
Add, check, configure and remove custom domains. | Custom domains |
conversions:write |
Record conversions (sign-ups, sales) for clicks on links with conversion tracking. | Conversions |
webhooks:read |
List webhooks and their recent deliveries. | Webhooks |
webhooks:write |
Add, change and remove webhooks, replace their signing secrets, send test events and resend deliveries. | Webhooks |
A scope never gives more than the creator's role allows. For example, a key with links:write created by an Analyst still can't create links, because Analysts can't. See what each role can do.
Base URL and workspace ID
Every endpoint is under your workspace:
https://bgum.me/api/v1/workspaces/{workspaceId}
- Replace
{workspaceId}with the ID from Settings → Workspace, such asU95AmzlOd2V7. - Links, campaigns, domains and workspaces have 12-character IDs; QR codes have 8-character IDs. IDs are case-sensitive.
- Always use HTTPS.
Requests
- JSON. Send request bodies as JSON with
Content-Type: application/json, encoded as UTF-8. Field names are in camelCase, such asdestinationUrl. Fields we don't recognise are ignored. - Optional fields. Leave out what you don't need. In a
PATCH, only the fields you send change. - Times are ISO 8601 with a time zone, such as
2026-11-01T09:00:00+11:00or2026-10-31T22:00:00Z. Analytics dates are days in the workspace's time zone (or thetimeZoneyou pass), writtenYYYY-MM-DD; reports say which zone intimeZone. - Fixed values, such as a link's
status, are sent as their names, written as shown in these docs, such as"Paused". - Size. A request body can be up to 1 MB.
- Avoid overwriting changes. When changing a link, send the
versionyou last read asexpectedVersion. If someone changed the link since, you get409 link.version_conflictinstead of overwriting their change.
Responses
- JSON in camelCase, with every field present: empty values are
null. - Times are UTC with an offset, such as
2026-10-14T22:49:34.114896+00:00. - Creating returns
201 Created, the new item and aLocationheader with its address. - Deleting and archiving return
204 No Contentwith no body. - Lists are wrapped in an object with an
itemsarray, so more fields can be added later.
Errors
Errors use the standard problem format (RFC 9457) with Content-Type: application/problem+json. Use code to decide what to do; it never changes. detail is written for people and may be reworded.
JSON
{
"type": "https://docs.blackgumlabs.com/errors/alias.taken",
"title": "Conflict",
"status": 409,
"detail": "“spring-sale” is already in use on this domain. Try another back-half.",
"code": "alias.taken",
"traceId": "00-68e8a002645c783ee9ceb0fb177c10ea-b45bb3b99037b204-00"
}
| Field | Meaning |
|---|---|
code | Stable error code, such as alias.taken. All codes are listed in Error codes. |
status | The HTTP status code again. |
title | A short summary of the kind of error. |
detail | What went wrong, in plain English, ready to show to a person. |
type | A URI that identifies the error code. |
traceId | Quote this if you contact us about the error. |
limit, resetsAt | Only on allowance errors: the monthly limit and when it resets. |
| Status | Means |
|---|---|
| 200 | Done. The body has the result. |
| 201 | Created. The body has the new item. |
| 204 | Done, nothing to return. |
| 401 | No API key, or the key is wrong, revoked or expired. |
| 403 | Not allowed: a missing scope or role, a plan without the feature, or an organisation rule. |
| 404 | The workspace or item doesn't exist, or isn't yours. We don't say which, so IDs from other workspaces can't be probed. |
| 409 | Conflicts with the current state, such as a back-half that's taken or a changed version. |
| 413 | The request body is too large. |
| 422 | Something in the request isn't valid. detail says what. |
| 429 | Too many requests, or a monthly allowance is used up (then there's a code). |
| 500 | Something went wrong on our side. Try again. |
These codes can come back from any endpoint:
| Status | Code | Meaning |
|---|---|---|
| 404 | workspace.not_found | The workspace ID in the path doesn't exist, or your key or account can't use it. |
| 403 | access.forbidden | Your role in the workspace, or the key's scopes, don't allow this. |
| 403 | apikey.owner_removed | The person who created this API key is no longer in the workspace. |
| 403 | plan.feature_required | The feature isn't included in the organisation's plan. API access itself needs the Business plan. |
| 403 | organisation.mfa_required | The organisation requires two-factor authentication and the key's creator (or you) hasn't turned it on. |
| 403 | account.email_unverified | The account that owns the key hasn't confirmed its email address, so it can't create links. |
| 403 | account.suspended | The organisation is suspended, usually after a failed payment, so new links and QR codes can't be made. |
| 403 | installation.read_only | This installation of the platform has no valid licence, so links, QR codes and other settings can't be created or changed. Reading still works. |
| 403 | csrf.header_missing | A signed-in browser request changed something without the X-BGL-Request: 1 header. |
Rate limits
- Requests. Each API key can make up to 120 requests in a burst. The allowance refills by 60 requests every 10 seconds, so you can keep up about 6 requests a second.
- Going over returns
429 Too Many Requestswith an empty body. Wait and try again, slowing down each time. - Monthly allowances for new links and QR codes are separate. When one runs out you get
429withquota.links_exhaustedorquota.qr_exhausted, andresetsAtsays when it resets. Waiting a few seconds won't help. Check what's left with GET /usage. - Many links at once? Use bulk create rather than many single requests.
A simple way to retry politely:
JavaScript
async function call(url, options, attempts = 5) {
for (let i = 0; ; i++) {
const res = await fetch(url, options);
if (res.status !== 429 || i === attempts - 1) return res;
const wait = Number(res.headers.get("Retry-After")) || 2 ** i;
await new Promise(r => setTimeout(r, (wait + Math.random()) * 1000));
}
}
Safe retries
If a request to create a link times out, you can't tell whether the link was made. Send an Idempotency-Key header with a value unique to that link, such as an order number or a UUID, and retry with the same key:
curl
curl -X POST "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/links" \
-H "Authorization: Bearer $BGL_API_KEY" \
-H "Idempotency-Key: order-1042-confirmation" \
-H "Content-Type: application/json" \
-d '{"destinationUrl": "https://example.com/orders/1042"}'
- A retry with the same key and the same body within 48 hours returns
200 OKand the link from the first attempt. It doesn't create a duplicate or use more of your allowance. - The same key with a different body returns
409 idempotency.key_reused. Use a new key for each different link. - Keys can be up to 200 characters. They work on create a link and bulk create.
- Changing a link is safe to repeat. Deleting twice returns
404the second time, which you can treat as done.
Paging
List links returns one page at a time. Ask for a page with page (from 1) and pageSize (up to 100). The response has total, the number of matching links. Keep asking for the next page until you have them all:
Python
import os
import requests
base = f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}"
headers = {"Authorization": f"Bearer {os.environ['BGL_API_KEY']}"}
links, page = [], 1
while True:
res = requests.get(f"{base}/links", headers=headers, params={"page": page, "pageSize": 100}, timeout=30)
res.raise_for_status()
body = res.json()
links += body["items"]
if not body["items"] or len(links) >= body["total"]:
break
page += 1
print(len(links), "links")
Other lists (QR codes, campaigns, domains) return everything in one response, up to 500 items, newest first.
Versions and changes
- The version is in the path:
/api/v1. - New endpoints, new optional fields, new response fields and new error codes can be added to v1 at any time. Write your code so it ignores fields and codes it doesn't recognise.
- We avoid changes that would break existing code, such as removing or renaming a field, within v1.
OpenAPI description
A machine-readable description of every operation is at /api/openapi/v1.json. Import it into tools such as Postman or Insomnia, or use it to generate a client. It lists paths, parameters and request bodies; the response formats are described on these pages.
Something missing or unclear? Tell us and we'll improve this page.