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.

  1. 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.
  2. 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.
  3. Find your workspace ID. Open Settings. Under Workspace, copy the Workspace ID for the API.
  4. Keep both somewhere safe, such as environment variables. The examples in these docs read them from BGL_API_KEY and WORKSPACE_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"
  5. 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 OK and a JSON list. If you get 401, check the key; if you get 404, check the workspace ID.

  6. 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.

ScopeAllowsEndpoints
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 as U95AmzlOd2V7.
  • 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 as destinationUrl. 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:00 or 2026-10-31T22:00:00Z. Analytics dates are days in the workspace's time zone (or the timeZone you pass), written YYYY-MM-DD; reports say which zone in timeZone.
  • 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 version you last read as expectedVersion. If someone changed the link since, you get 409 link.version_conflict instead 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 a Location header with its address.
  • Deleting and archiving return 204 No Content with no body.
  • Lists are wrapped in an object with an items array, 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"
}
FieldMeaning
codeStable error code, such as alias.taken. All codes are listed in Error codes.
statusThe HTTP status code again.
titleA short summary of the kind of error.
detailWhat went wrong, in plain English, ready to show to a person.
typeA URI that identifies the error code.
traceIdQuote this if you contact us about the error.
limit, resetsAtOnly on allowance errors: the monthly limit and when it resets.
StatusMeans
200Done. The body has the result.
201Created. The body has the new item.
204Done, nothing to return.
401No API key, or the key is wrong, revoked or expired.
403Not allowed: a missing scope or role, a plan without the feature, or an organisation rule.
404The workspace or item doesn't exist, or isn't yours. We don't say which, so IDs from other workspaces can't be probed.
409Conflicts with the current state, such as a back-half that's taken or a changed version.
413The request body is too large.
422Something in the request isn't valid. detail says what.
429Too many requests, or a monthly allowance is used up (then there's a code).
500Something went wrong on our side. Try again.

These codes can come back from any endpoint:

StatusCodeMeaning
404workspace.not_foundThe workspace ID in the path doesn't exist, or your key or account can't use it.
403access.forbiddenYour role in the workspace, or the key's scopes, don't allow this.
403apikey.owner_removedThe person who created this API key is no longer in the workspace.
403plan.feature_requiredThe feature isn't included in the organisation's plan. API access itself needs the Business plan.
403organisation.mfa_requiredThe organisation requires two-factor authentication and the key's creator (or you) hasn't turned it on.
403account.email_unverifiedThe account that owns the key hasn't confirmed its email address, so it can't create links.
403account.suspendedThe organisation is suspended, usually after a failed payment, so new links and QR codes can't be made.
403installation.read_onlyThis installation of the platform has no valid licence, so links, QR codes and other settings can't be created or changed. Reading still works.
403csrf.header_missingA 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 Requests with 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 429 with quota.links_exhausted or quota.qr_exhausted, and resetsAt says 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 OK and 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 404 the 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.