Links

Create, list, change and delete short links, in ones or in bulk.

A link has a destination, a back-half (alias) on a domain, and optional title, tags, campaign, schedule and password. Links are identified by their id, a 12-character string.

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
Create a linkPOST /links
List linksGET /links
Get a linkGET /links/{linkId}
Change a linkPATCH /links/{linkId}
Delete a linkDELETE /links/{linkId}
Create links in bulkPOST /links/bulk
Change many linksPOST /links/bulk-actions
Check a back-halfGET /aliases/availability

Create a link

POST /links

Creates a short link. Leave out alias to get a random back-half. Each new link uses one of the organisation's monthly allowance.

Full path
/api/v1/workspaces/{workspaceId}/links
API key scope
links:write
Role needed
Manager or above

Path parameters

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

Headers

NameTypeRequiredDescription
Idempotency-Key string No Any unique value up to 200 characters, such as a UUID. Repeating a request with the same key within 48 hours returns the first result instead of creating a duplicate or using more allowance.

Request body

JSON (Content-Type: application/json). Leave out fields you don't need.

NameTypeRequiredDescription
destinationUrl string Yes Where the link goes: an http or https address, up to 4,096 characters. A bare address such as example.com/page gets https:// added.
domainId string No ID of an active custom domain from GET /domains (Business). Leave out for the default short domain.
alias string No Your own back-half (Pro and above): 3 to 64 letters, numbers, hyphens or underscores, not starting or ending with - or _. Case doesn't matter: Sale and sale are the same link.
title string No A name to recognise the link by. Longer than 500 characters is cut.
description string No Longer description, up to 2,000 characters.
notes string No Private notes for your team.
tags string[] No Up to 20 tags of up to 50 characters. New tags are created as needed; case doesn't matter.
campaignId string No ID of an active campaign (Pro and above). Its UTM tags are added when people follow the link.
folderId string No ID of a folder in the workspace, from GET /folders. Leave out for no folder.
startsAt date-time No When the link starts working, in ISO 8601 with a time zone, e.g. 2026-11-01T09:00:00+11:00 (Pro and above).
endsAt date-time No When the link stops working and shows a "link expired" page. Must be after startsAt (Pro and above).
draft boolean No true creates the link as a draft that doesn't redirect until you activate it. Default false.
redirectType string No Found (302, the default), MovedPermanently (301) or TemporaryRedirect (307). The number also works, e.g. 301.
password string No Visitors must enter this password first (Pro and above). At least 4 characters.
trackConversions boolean No true adds a click ID (bgl_click) to the destination so your site can report sign-ups and sales (Business). Default false. See Conversions.
routing object No Smart routing (Business): rules by country, device or language, a traffic split, and app links. See the notes below for its shape.
maxClicks integer No The link expires after this many clicks from people (Pro and above), from 1 to 1,000,000,000.
expiredRedirectUrl string No Where people go once the link has expired (stop time or click limit), instead of the "link expired" page (Pro and above). Same rules and safety check as destinationUrl.
idempotencyKey string No Same as the Idempotency-Key header. The header wins if both are sent.

Example request

curl

curl -X POST "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/links" \
  -H "Authorization: Bearer $BGL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "destinationUrl": "https://example.com/spring-sale",
  "alias": "spring-sale",
  "title": "Spring sale",
  "tags": [
    "email",
    "spring"
  ],
  "campaignId": "aiQrz1XgekFP"
}'

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/links`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "destinationUrl": "https://example.com/spring-sale",
    "alias": "spring-sale",
    "title": "Spring sale",
    "tags": [
      "email",
      "spring"
    ],
    "campaignId": "aiQrz1XgekFP"
  }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);

Python

import os
import requests
import uuid

res = requests.post(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/links",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "destinationUrl": "https://example.com/spring-sale",
        "alias": "spring-sale",
        "title": "Spring sale",
        "tags": ["email", "spring"],
        "campaignId": "aiQrz1XgekFP",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

201 Created · application/json

JSON

{
  "id": "ZiZihoWXe3as",
  "shortUrl": "https://bgum.me/spring-sale",
  "domain": "bgum.me",
  "alias": "spring-sale",
  "isCustomAlias": true,
  "destinationUrl": "https://example.com/spring-sale",
  "title": "Spring sale",
  "description": null,
  "notes": null,
  "status": "Active",
  "state": "Active",
  "startsAt": null,
  "endsAt": null,
  "redirectType": "Found",
  "passwordProtected": false,
  "tags": [
    "email",
    "spring"
  ],
  "campaignId": "aiQrz1XgekFP",
  "version": 1,
  "createdAt": "2026-10-14T22:49:34.114896+00:00",
  "updatedAt": "2026-10-14T22:49:34.114896+00:00",
  "totalClicks": 0,
  "blocked": false,
  "trackConversions": false,
  "folderId": "q5Tz0aLmW2xd",
  "routing": null,
  "maxClicks": null,
  "clickLimitReached": false,
  "expiredRedirectUrl": null
}

Good to know

  • Returns 201 with a Location header for a new link. A repeat with the same Idempotency-Key returns 200 and the original link.
  • When the monthly allowance is used up the response is 429 with a Retry-After header (seconds) and resetsAt in the body.
  • The account that owns the key must have confirmed its email address.
  • routing has three optional parts. rules: up to 20, checked in order; the first rule whose conditions all match sends the visitor to its destinationUrl. Each rule needs at least one of countries (two-letter codes such as AU), devices (ios, android, windows, macos, other) or languages (the browser's preferred language, such as en). split: 2 to 5 destinations with whole-number weights from 1 to 1,000, sharing everyone no rule matched; the same visitor keeps getting the same one. app: iosUrl and androidUrl, your app's own link (myapp://…), an Android intent: link or an https link; phones try the app first and fall back to the web destination. See Smart routing in the help centre.
  • Each rule and split destination has an id (up to 12 lower-case letters and digits). Leave it out and one is made up. Send the ids back when you change routing, so analytics keep counting each rule and destination as the same one.
  • Every destination in routing is checked like destinationUrl, including the safety check. Smart routing needs the Business plan; maxClicks needs Pro or above.

Errors

StatusCodeMeaning
422destination.requireddestinationUrl is missing or empty.
422destination.invalidThe destination isn't a valid web address.
422destination.schemeOnly http and https addresses can be shortened.
422destination.credentialsThe address contains a username or password (user:pass@host).
422destination.privateThe address points to a private network, localhost or an internal name.
422destination.loopThe address is itself a short link on this service, which would loop.
422destination.too_longThe destination is longer than 4,096 characters.
422link.destination_unsafeA threat-intelligence service lists the destination as phishing or malware, so it can't be used. provider and threat say which.
422alias.too_shortThe back-half is shorter than the minimum (usually 3 characters).
422alias.too_longThe back-half is longer than the maximum (usually 64 characters).
422alias.invalid_charactersThe back-half has characters other than letters, numbers, hyphens and underscores.
422alias.invalid_edgesThe back-half starts or ends with a hyphen or underscore.
409alias.reservedThe back-half is a word this service uses itself, such as admin or docs.
422alias.prohibitedThe back-half contains a word that isn't allowed.
409alias.takenAnother link on that domain already uses the back-half, including archived and deleted links.
409alias.generation_failedWe couldn't generate a unique random back-half.
404domain.not_foundNo domain with that ID in this workspace, or it isn't available to it.
409domain.not_readyThe domain isn't verified and active yet, so links can't use it.
404campaign.not_foundNo campaign with that ID in this workspace. When you add a link to a campaign, an archived one counts as missing.
404folder.not_foundNo folder with that ID in this workspace.
422link.schedule_invalidThe stop time isn't after the start time.
422link.password_too_shortLink passwords need at least 4 characters.
422link.redirect_type_invalidredirectType isn't one of the allowed values.
422tags.too_manyMore than 20 tags on one link.
422tags.too_longA tag is longer than 50 characters.
422idempotency.key_too_longThe Idempotency-Key is longer than 200 characters.
409idempotency.key_reusedThe Idempotency-Key was already used for a different request in the last 48 hours.
409idempotency.target_deletedThe link created by this Idempotency-Key has since been deleted.
429quota.links_exhaustedThe organisation has used this month's allowance of new links. The response has limit and resetsAt fields and a Retry-After header.
409platform.no_default_domainThe service has no default short domain set up yet.
422routing.too_many_rulesrouting has more than 20 rules.
422routing.rule_invalidA rule in routing.rules is null.
422routing.rule_condition_requiredA rule has no countries, devices or languages, so it would catch everyone.
422routing.too_many_countriesA rule lists more than 60 countries.
422routing.country_invalidA country isn't a two-letter ISO 3166 code.
422routing.device_invalidA device isn't one of ios, android, windows, macos or other.
422routing.too_many_languagesA rule lists more than 20 languages.
422routing.language_invalidA language isn't a two- or three-letter code.
422routing.split_sizerouting.split has one destination, or more than five.
422routing.variant_invalidA destination in routing.split is null.
422routing.weight_invalidA split weight isn't a whole number from 1 to 1,000.
422routing.name_too_longA rule or split name is longer than 100 characters.
422routing.id_invalidA rule or split id isn't up to 12 lower-case letters and digits.
422routing.id_duplicateTwo rules or split destinations have the same id.
422routing.app_url_invalidAn app link isn't an app address (myapp://…), an Android intent: link or an https address.
422link.max_clicks_invalidmaxClicks isn't a whole number from 1 to 1,000,000,000.

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

Lists the workspace's links one page at a time: newest first, or best match first when searching. Archived links are left out unless you ask for them with state=Archived.

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

Path parameters

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

Query parameters

NameTypeRequiredDescription
search string No Words to find in the back-half, title, description, notes, destination or tags. Each word matches the start of a word (camp finds Campaign) and all must match, in any order. Part of a back-half also matches. Case and punctuation don't matter.
state string No Only links in this state: Draft, Scheduled, Active, Paused, Expired or Archived.
tag string No Only links with this tag.
campaignId string No Only links in this campaign.
domainId string No Only links on this domain.
folderId string No Only links directly in this folder (not its subfolders). none gives links that aren't in a folder.
createdFrom date-time No Only links created at or after this time, in ISO 8601 with a time zone, e.g. 2026-10-01T00:00:00Z.
createdTo date-time No Only links created before this time.
sort string No relevance (best match first, the default with search), -created (newest first, the default otherwise), created, -updated, updated or alias.
page integer No Page number, from 1. Default 1.
pageSize integer No Links per page, 1 to 100. Default 25.

Example request

Response

200 OK · application/json

JSON

{
  "items": [
    {
      "id": "ZiZihoWXe3as",
      "shortUrl": "https://bgum.me/spring-sale",
      "domain": "bgum.me",
      "alias": "spring-sale",
      "isCustomAlias": true,
      "destinationUrl": "https://example.com/spring-sale",
      "title": "Spring sale",
      "description": null,
      "notes": null,
      "status": "Active",
      "state": "Active",
      "startsAt": null,
      "endsAt": null,
      "redirectType": "Found",
      "passwordProtected": false,
      "tags": [
        "email",
        "spring"
      ],
      "campaignId": "aiQrz1XgekFP",
      "version": 1,
      "createdAt": "2026-10-14T22:49:34.114896+00:00",
      "updatedAt": "2026-10-14T22:49:34.114896+00:00",
      "totalClicks": 0,
      "blocked": false,
      "trackConversions": false,
      "folderId": "q5Tz0aLmW2xd",
      "routing": null,
      "maxClicks": null,
      "clickLimitReached": false,
      "expiredRedirectUrl": null
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1
}

Good to know

  • total is the number of matching links across all pages. Keep requesting the next page until you've seen total items or a page comes back empty.
  • To count the links a filter matches (for example before a bulk action), ask for pageSize=1 and read total.

Errors

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}

Returns one link, including its total clicks.

Full path
/api/v1/workspaces/{workspaceId}/links/{linkId}
API key scope
links: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.

Example request

Response

200 OK · application/json

JSON

{
  "id": "ZiZihoWXe3as",
  "shortUrl": "https://bgum.me/spring-sale",
  "domain": "bgum.me",
  "alias": "spring-sale",
  "isCustomAlias": true,
  "destinationUrl": "https://example.com/spring-sale",
  "title": "Spring sale",
  "description": null,
  "notes": null,
  "status": "Active",
  "state": "Active",
  "startsAt": null,
  "endsAt": null,
  "redirectType": "Found",
  "passwordProtected": false,
  "tags": [
    "email",
    "spring"
  ],
  "campaignId": "aiQrz1XgekFP",
  "version": 1,
  "createdAt": "2026-10-14T22:49:34.114896+00:00",
  "updatedAt": "2026-10-14T22:49:34.114896+00:00",
  "totalClicks": 0,
  "blocked": false,
  "trackConversions": false,
  "folderId": "q5Tz0aLmW2xd",
  "routing": null,
  "maxClicks": null,
  "clickLimitReached": false,
  "expiredRedirectUrl": null
}

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.

PATCH /links/{linkId}

Changes only the fields you send. Changing a link never uses your monthly allowance. The redirect changes straight away.

Full path
/api/v1/workspaces/{workspaceId}/links/{linkId}
API key scope
links:write
Role needed
Manager 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.

Request body

JSON (Content-Type: application/json). Leave out fields you don't need.

NameTypeRequiredDescription
destinationUrl string No New destination. Same rules as when creating.
title string No New title. Send an empty string to remove it.
description string No New description. Send an empty string to remove it.
notes string No New notes.
tags string[] No Replaces all tags. Send [] to remove them.
campaignId string No Moves the link to this campaign.
clearCampaign boolean No true takes the link out of its campaign.
folderId string No Moves the link into this folder.
clearFolder boolean No true takes the link out of its folder.
startsAt date-time No New start time (Pro and above).
endsAt date-time No New stop time (Pro and above).
clearSchedule boolean No true removes both start and stop times.
status string No Active, Paused, Archived or Draft. Allowed changes: Draft → Active or Archived; Active ↔ Paused; Active or Paused → Archived; Archived → Paused.
redirectType string No Found, MovedPermanently or TemporaryRedirect.
password string No Sets or replaces the password (Pro and above).
removePassword boolean No true removes the password.
trackConversions boolean No true turns conversion tracking on (Business), false turns it off.
routing object No Replaces the link's smart routing (Business). Send {} or use clearRouting to remove it. Same shape as when creating.
clearRouting boolean No true removes smart routing. Works on any plan.
maxClicks integer No Sets or changes the click limit (Pro and above). Raising it above the clicks counted so far brings an expired link back; lowering it below them expires the link now.
clearMaxClicks boolean No true removes the click limit.
expiredRedirectUrl string No Sets or changes where people go once the link has expired (Pro and above).
clearExpiredRedirectUrl boolean No true removes it: an expired link shows the "link expired" page again, or the domain's choice.
expectedVersion integer No The version you last read. If the link has changed since, you get 409 link.version_conflict instead of overwriting someone else's change.

Example request

Response

200 OK · application/json

JSON

{
  "id": "ZiZihoWXe3as",
  "shortUrl": "https://bgum.me/spring-sale",
  "domain": "bgum.me",
  "alias": "spring-sale",
  "isCustomAlias": true,
  "destinationUrl": "https://example.com/spring-sale-2026",
  "title": "Spring sale",
  "description": null,
  "notes": null,
  "status": "Active",
  "state": "Active",
  "startsAt": null,
  "endsAt": null,
  "redirectType": "Found",
  "passwordProtected": false,
  "tags": [
    "email",
    "spring"
  ],
  "campaignId": "aiQrz1XgekFP",
  "version": 2,
  "createdAt": "2026-10-14T22:49:34.114896+00:00",
  "updatedAt": "2026-10-15T01:12:40.5862714+00:00",
  "totalClicks": 0,
  "blocked": false,
  "trackConversions": false,
  "folderId": "q5Tz0aLmW2xd",
  "routing": null,
  "maxClicks": null,
  "clickLimitReached": false,
  "expiredRedirectUrl": null
}

Good to know

  • The back-half and domain can't be changed: create a new link instead, so printed or shared copies never change meaning.
  • Each change adds a version; the link page in the app shows the history.
  • routing has three optional parts. rules: up to 20, checked in order; the first rule whose conditions all match sends the visitor to its destinationUrl. Each rule needs at least one of countries (two-letter codes such as AU), devices (ios, android, windows, macos, other) or languages (the browser's preferred language, such as en). split: 2 to 5 destinations with whole-number weights from 1 to 1,000, sharing everyone no rule matched; the same visitor keeps getting the same one. app: iosUrl and androidUrl, your app's own link (myapp://…), an Android intent: link or an https link; phones try the app first and fall back to the web destination. See Smart routing in the help centre.
  • Each rule and split destination has an id (up to 12 lower-case letters and digits). Leave it out and one is made up. Send the ids back when you change routing, so analytics keep counting each rule and destination as the same one.
  • Every destination in routing is checked like destinationUrl, including the safety check. Smart routing needs the Business plan; maxClicks needs Pro or above.

Errors

StatusCodeMeaning
404link.not_foundNo link with that ID in this workspace (or it was deleted).
403link.blockedThe link was disabled after an abuse report, so it can't be changed.
409link.version_conflictexpectedVersion doesn't match: someone changed the link since you read it.
409link.invalid_transitionThe link can't move from its current status to the one you asked for.
422link.schedule_invalidThe stop time isn't after the start time.
422link.password_too_shortLink passwords need at least 4 characters.
422link.redirect_type_invalidredirectType isn't one of the allowed values.
422destination.invalidThe destination isn't a valid web address.
422destination.schemeOnly http and https addresses can be shortened.
422destination.privateThe address points to a private network, localhost or an internal name.
422destination.loopThe address is itself a short link on this service, which would loop.
422link.destination_unsafeA threat-intelligence service lists the destination as phishing or malware, so it can't be used. provider and threat say which.
404campaign.not_foundNo campaign with that ID in this workspace. When you add a link to a campaign, an archived one counts as missing.
404folder.not_foundNo folder with that ID in this workspace.
422tags.too_manyMore than 20 tags on one link.
422tags.too_longA tag is longer than 50 characters.
422routing.too_many_rulesrouting has more than 20 rules.
422routing.rule_invalidA rule in routing.rules is null.
422routing.rule_condition_requiredA rule has no countries, devices or languages, so it would catch everyone.
422routing.too_many_countriesA rule lists more than 60 countries.
422routing.country_invalidA country isn't a two-letter ISO 3166 code.
422routing.device_invalidA device isn't one of ios, android, windows, macos or other.
422routing.too_many_languagesA rule lists more than 20 languages.
422routing.language_invalidA language isn't a two- or three-letter code.
422routing.split_sizerouting.split has one destination, or more than five.
422routing.variant_invalidA destination in routing.split is null.
422routing.weight_invalidA split weight isn't a whole number from 1 to 1,000.
422routing.name_too_longA rule or split name is longer than 100 characters.
422routing.id_invalidA rule or split id isn't up to 12 lower-case letters and digits.
422routing.id_duplicateTwo rules or split destinations have the same id.
422routing.app_url_invalidAn app link isn't an app address (myapp://…), an Android intent: link or an https address.
422link.max_clicks_invalidmaxClicks isn't a whole number from 1 to 1,000,000,000.

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.

DELETE /links/{linkId}

Deletes the link. It stops redirecting straight away and its back-half is never reused, so old copies can't be taken over.

Full path
/api/v1/workspaces/{workspaceId}/links/{linkId}
API key scope
links:write
Role needed
Manager 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.

Example request

Response

204 No Content

Good to know

  • Deleting doesn't give back the monthly allowance. To stop a link temporarily, PATCH its status to Paused 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.

Create links in bulk

POST /links/bulk

Creates many links in one request (Business). Each link is created on its own: one bad row doesn't stop the others.

Full path
/api/v1/workspaces/{workspaceId}/links/bulk
API key scope
links:write
Role needed
Manager or above
Plan
Needs bulk operations in your plan (compare plans)

Path parameters

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

Headers

NameTypeRequiredDescription
Idempotency-Key string No Recommended. Sending the same key again returns the links already made instead of making more. Without a key, an identical request body counts as the same batch.

Request body

JSON (Content-Type: application/json). Leave out fields you don't need.

NameTypeRequiredDescription
links object[] Yes The links to create, each with the same fields as Create a link (idempotencyKey is ignored). Up to 1,000 on Business.

Example request

curl

curl -X POST "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/links/bulk" \
  -H "Authorization: Bearer $BGL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "links": [
    {
      "destinationUrl": "https://example.com/a",
      "title": "Page A"
    },
    {
      "destinationUrl": "https://example.com/b",
      "alias": "page-b"
    }
  ]
}'

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/links/bulk`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "links": [
      {
        "destinationUrl": "https://example.com/a",
        "title": "Page A"
      },
      {
        "destinationUrl": "https://example.com/b",
        "alias": "page-b"
      }
    ]
  }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);

Python

import os
import requests
import uuid

res = requests.post(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/links/bulk",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "links": [
            {
                "destinationUrl": "https://example.com/a",
                "title": "Page A",
            },
            {
                "destinationUrl": "https://example.com/b",
                "alias": "page-b",
            },
        ],
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · application/json

JSON

{
  "rows": [
    {
      "line": 1,
      "destination": "https://example.com/a",
      "outcome": "created",
      "shortUrl": "https://bgum.me/a2ruepj",
      "linkId": "ZGqD6yhrCyac",
      "error": null,
      "sourceShortUrl": null,
      "note": null
    },
    {
      "line": 2,
      "destination": "https://example.com/b",
      "outcome": "failed",
      "shortUrl": null,
      "linkId": null,
      "error": "“page-b” is already in use on this domain. Try another back-half.",
      "sourceShortUrl": null,
      "note": null
    }
  ],
  "created": 1,
  "alreadyCreated": 0,
  "failed": 1,
  "notAttempted": 0
}

Good to know

  • outcome is created, already created (from an earlier attempt with the same key), failed (error says why) or not attempted.
  • If the monthly allowance runs out part-way, the remaining rows are not attempted. Retry later with the same Idempotency-Key to finish the batch without duplicates.

Errors

StatusCodeMeaning
422bulk.emptyThe bulk request had no links in it.
429bulk.too_manyThe bulk request has more links than your plan allows in one go (1,000 on Business).

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.

Change many links

POST /links/bulk-actions

Moves, tags, pauses, resumes, sets or removes stop times on, or deletes many links in one request: the links you list, or every link matching a filter. Works on every plan.

Full path
/api/v1/workspaces/{workspaceId}/links/bulk-actions
API key scope
links:write
Role needed
Manager or above

Path parameters

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

Request body

JSON (Content-Type: application/json). Leave out fields you don't need.

NameTypeRequiredDescription
action string Yes move, addTags, removeTags, pause, resume, setExpiry, clearExpiry or delete. delete needs permission to delete links; setExpiry and clearExpiry need Pro or above.
linkIds string[] No The links to change, up to 1,000. Send this or filter.
filter object No Change every link matching these, as in List links: search, state, tag, folderId, domainId, campaignId, createdFrom and createdTo. {} means every link that isn't archived. Up to 10,000 links.
folderId string No For move: the folder to move to. Leave out or send null to take the links out of their folders.
tags string[] No For addTags and removeTags: the tags. New tags are created as needed.
endsAt date-time No For setExpiry: when the links stop working.
expectedCount integer No With filter: how many links you expect it to match, such as the total you showed someone to confirm. If it matches a different number now, nothing changes.

Example request

curl

curl -X POST "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/links/bulk-actions" \
  -H "Authorization: Bearer $BGL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "pause",
  "filter": {
    "search": "spring",
    "state": "Active"
  },
  "expectedCount": 23
}'

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/links/bulk-actions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BGL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "action": "pause",
    "filter": {
      "search": "spring",
      "state": "Active"
    },
    "expectedCount": 23
  }),
});
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.post(
    f"https://bgum.me/api/v1/workspaces/{os.environ['WORKSPACE_ID']}/links/bulk-actions",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    json={
        "action": "pause",
        "filter": {
            "search": "spring",
            "state": "Active",
        },
        "expectedCount": 23,
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · application/json

JSON

{
  "action": "pause",
  "matched": 23,
  "changed": 22,
  "unchanged": 0,
  "failed": [
    {
      "linkId": "ZiZihoWXe3as",
      "code": "link.blocked",
      "message": "This link was disabled after an abuse report or a safety check, so it can't be changed. Contact support if you think this is a mistake."
    }
  ]
}

Good to know

  • unchanged counts links already as asked, such as pausing a paused link. failed lists links that weren't changed and why: listed IDs not found in this workspace (link.not_found), links disabled by a safety check (link.blocked), a link that would get more than 20 tags, or a stop time before its start time.
  • Links are changed in batches of 200. If someone changes one of a batch at the same moment, that batch is left as it was and its links are listed in failed with link.version_conflict: send them again.
  • Pausing, resuming, stop times and deleting reach the short links straight away and are added to each link's history.

Errors

StatusCodeMeaning
422bulk.action_invalidaction isn't one of move, addTags, removeTags, pause, resume, setExpiry, clearExpiry or delete.
422bulk.selection_invalidThe request sent both linkIds and filter, or neither, or an empty linkIds.
422bulk.too_many_idslinkIds has more than 1,000 IDs.
409bulk.too_many_matchesThe filter matches more than 10,000 links.
409bulk.count_changedThe filter now matches a different number of links than expectedCount, so nothing was changed. count says how many it matches now.
422bulk.tags_requiredaddTags or removeTags was sent without any tags.
422bulk.ends_at_requiredsetExpiry was sent without endsAt.
404folder.not_foundNo folder with that ID in this workspace.
422tags.too_manyMore than 20 tags on one link.
422tags.too_longA tag is longer than 50 characters.

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.

Check a back-half

GET /aliases/availability

Says whether a back-half is free on a domain, without reserving it.

Full path
/api/v1/workspaces/{workspaceId}/aliases/availability
API key scope
links:write
Role needed
Manager or above

Path parameters

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

Query parameters

NameTypeRequiredDescription
alias string Yes The back-half to check.
domainId string No The custom domain to check on. Leave out for the default short domain.

Example request

curl

curl "https://bgum.me/api/v1/workspaces/$WORKSPACE_ID/aliases/availability?alias=summer-sale" \
  -H "Authorization: Bearer $BGL_API_KEY"

JavaScript

const res = await fetch(`https://bgum.me/api/v1/workspaces/${process.env.WORKSPACE_ID}/aliases/availability?alias=summer-sale`, {
  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']}/aliases/availability?alias=summer-sale",
    headers={
        "Authorization": f"Bearer {os.environ['BGL_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())

Response

200 OK · application/json

JSON

{
  "alias": "summer-sale",
  "available": true
}

Good to know

  • When the back-half isn't allowed, available is false and reason and code explain why, e.g. {"alias":"admin","available":false,"reason":"That back-half is reserved. Try another.","code":"alias.reserved"}. Someone else can still take a free back-half before you create the link.

Errors

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.