Skip to main content

Bulk operations

If you run an agency or a large account, you don't want to fire one HTTP request per keyword. This guide shows the two places the Ranktracker API lets you work in bulk:

It also covers the honest limits: there is no bulk-untag endpoint today, so untagging is one call per keyword.

:::tip New to the API? Start with the Quickstart and Authentication guides. This one assumes you already have an API key and can make an authenticated request. Tags and keywords are explained in Core concepts. :::

Bulk-tag keywords​

The single-keyword way to tag is one call per (keyword, tag) pair:

curl -X POST \
https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords/KEYWORD_UUID/tags/TAG_UUID \
-H "Authorization: tkn_usr_your_api_key_here"

That's fine for a keyword or two. To tag hundreds, use the bulk-tag endpoint, which applies one tag to many keywords in a single request:

POST /v1/domains/{domain_uuid}/keywords/taggings

The body takes the tag UUID and an array of keyword UUIDs — all scoped to the domain in the path:

curl -X POST \
https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords/taggings \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"tag_uuid": "b3f1c2d4-…",
"keyword_uuids": [
"1a2b3c4d-…",
"5e6f7a8b-…",
"9c0d1e2f-…"
]
}'
import requests

domain_uuid = "DOMAIN_UUID"
resp = requests.post(
f"https://api.ranktracker.com/v1/domains/{domain_uuid}/keywords/taggings",
headers={"Authorization": "tkn_usr_your_api_key_here"},
json={
"tag_uuid": "b3f1c2d4-…",
"keyword_uuids": ["1a2b3c4d-…", "5e6f7a8b-…", "9c0d1e2f-…"],
},
)
resp.raise_for_status()
created = resp.json()["data"]
print(f"Created {len(created)} taggings")
const domainUuid = "DOMAIN_UUID";
const resp = await fetch(
`https://api.ranktracker.com/v1/domains/${domainUuid}/keywords/taggings`,
{
method: "POST",
headers: {
Authorization: "tkn_usr_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_uuid: "b3f1c2d4-…",
keyword_uuids: ["1a2b3c4d-…", "5e6f7a8b-…", "9c0d1e2f-…"],
}),
},
);
const { data } = await resp.json();
console.log(`Created ${data.length} taggings`);

The response​

On success the endpoint returns 201 Created with a JSON:API list under data. Each item is a tagging — the join between one keyword and the tag:

{
"data": [
{
"id": "7d8e9f0a-…",
"type": "keywordTag",
"attributes": {
"domainUuid": "DOMAIN_UUID",
"keywordUuid": "1a2b3c4d-…",
"tagUuid": "b3f1c2d4-…",
"createdAt": "2026-07-03T09:14:22Z",
"updatedAt": "2026-07-03T09:14:22Z"
}
},
{
"id": "8e9f0a1b-…",
"type": "keywordTag",
"attributes": {
"domainUuid": "DOMAIN_UUID",
"keywordUuid": "5e6f7a8b-…",
"tagUuid": "b3f1c2d4-…",
"createdAt": "2026-07-03T09:14:22Z",
"updatedAt": "2026-07-03T09:14:22Z"
}
}
]
}

:::note Already-tagged keywords are skipped, not duplicated The call only creates taggings that don't already exist. If some of the keywords you send already carry that tag, those are left as-is and only the new taggings come back in data. That makes the request safe to retry. :::

You need the tag UUID and the keyword UUIDs before you call this. Get tag UUIDs from GET /v1/tags (or create one with POST /v1/tags), and keyword UUIDs from GET /v1/domains/{domain_uuid}/keywords — the id of each keyword in the list.

Errors​

The bulk-tag endpoint follows the standard JSON:API error envelope:

StatusWhen
422keyword_uuids is empty.
404One or more keyword UUIDs don't exist in this domain — the error detail lists the missing UUIDs.
403Your plan has API access disabled, or the tag/domain isn't yours.

:::warning 404 means nothing changed A 404 for missing keyword UUIDs is all-or-nothing — the call reports the unknown UUIDs and applies no taggings. Fix the list (or drop the bad UUIDs) and retry, rather than assuming a partial write happened. :::

Bulk-tag vs. single tag/untag​

There are three tag operations. Only tagging has a bulk form:

OperationEndpointBulk?
Tag one keywordPOST /v1/domains/{d}/keywords/{k}/tags/{t}No
Untag one keywordDELETE /v1/domains/{d}/keywords/{k}/tags/{t}No
Tag many keywordsPOST /v1/domains/{d}/keywords/taggingsYes

The single-keyword POST returns 201 when it creates the tagging, or 200 if that keyword already had the tag (so it's idempotent too). The single-keyword DELETE returns 204.

There is no bulk-untag endpoint​

Being upfront: a bulk-untag endpoint is not currently available. To remove a tag from many keywords, call the single-keyword DELETE once per keyword:

curl -X DELETE \
https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords/KEYWORD_UUID/tags/TAG_UUID \
-H "Authorization: tkn_usr_your_api_key_here"
import requests

domain_uuid = "DOMAIN_UUID"
tag_uuid = "b3f1c2d4-…"
keyword_uuids = ["1a2b3c4d-…", "5e6f7a8b-…", "9c0d1e2f-…"]

session = requests.Session()
session.headers["Authorization"] = "tkn_usr_your_api_key_here"

for keyword_uuid in keyword_uuids:
resp = session.delete(
f"https://api.ranktracker.com/v1/domains/{domain_uuid}"
f"/keywords/{keyword_uuid}/tags/{tag_uuid}"
)
# 204 = removed, 404 = the keyword didn't have that tag (safe to ignore)
if resp.status_code not in (204, 404):
resp.raise_for_status()

:::tip Loop gently When you're untagging in a loop, keep concurrency modest and honour Retry-After if you get a 429 (see rate limits). Deleting a tagging that isn't there returns 404, which you can safely treat as "already done." :::

Track many keywords efficiently​

The other place bulk really pays off is adding keywords. A single POST /v1/domains/{domain_uuid}/keywords tracks the cartesian product of words × search_engines — so N words across M engine/location/device configurations creates N × M tracked keywords in one request. Batch your list instead of looping one word at a time. See Reference values for valid name, location, language, device, and frequency values.

curl -X POST \
https://api.ranktracker.com/v1/domains/DOMAIN_UUID/keywords \
-H "Authorization: tkn_usr_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"frequency": "daily",
"words": [
"rank tracker",
"keyword tracking",
"serp checker"
],
"search_engines": [
{
"name": "google",
"location": "2840",
"language": "en",
"device": "desktop"
},
{
"name": "google",
"location": "2840",
"language": "en",
"device": "mobile"
}
]
}'

The example above is 3 words × 2 engine configs = 6 tracked keywords from one call. The response is a 201 with a JSON:API list under data — one entry per keyword that was linked.

:::warning Watch your quota Every keyword created this way counts against your plan's keyword and data-row limits. If a batch would push you over, the whole call returns 402 and nothing is linked to the domain — it's all-or-nothing, so trim the batch and retry. Check headroom first with GET /v1/account/usage and size your batches to the remaining count. :::

Practical batching tips​

  • Compose the matrix, don't loop words. Put every word in words and every engine/location/device combo in search_engines. One request beats N.
  • Check quota before a big batch. Read usage & quota and keep each batch at or under keywords.remaining so you don't trip a 402.
  • Tag right after you create. The create response gives you each new keyword's id. Collect those IDs and pass them straight into POST /keywords/taggings to label the whole batch in a second call.
  • Read results back with pagination. When you list keywords afterwards, page through with page and per_page (max 1000) and use the X-Total-Count / X-Total-Pages headers — see Pagination.
  • Honour Retry-After. Bulk writes are the most likely to be throttled. Each API key is capped at 300 requests/minute, shared across REST and GraphQL. Going over returns 429 with a Retry-After header — wait that many seconds and retry, rather than guessing with backoff — details in Errors & rate limits.

Putting it together​

A typical agency onboarding flow for a new domain:

  1. GET /v1/account/usage — confirm you have keyword headroom.
  2. POST /v1/domains/{domain_uuid}/keywords — add the whole words × search_engines matrix in one call; keep each id from the response.
  3. POST /v1/tags (once) — create the tag you'll group them under, if it doesn't exist yet.
  4. POST /v1/domains/{domain_uuid}/keywords/taggings — bulk-tag every new keyword UUID with that tag in one call.

Two or three requests, not hundreds.