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:
- Bulk-tag many keywords in one call with
POST /v1/domains/{domain_uuid}/keywords/taggings. - Track many keywords in one call by posting a
words×search_enginesmatrix toPOST /v1/domains/{domain_uuid}/keywords.
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:
| Status | When |
|---|---|
422 | keyword_uuids is empty. |
404 | One or more keyword UUIDs don't exist in this domain — the error detail lists the missing UUIDs. |
403 | Your 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:
| Operation | Endpoint | Bulk? |
|---|---|---|
| Tag one keyword | POST /v1/domains/{d}/keywords/{k}/tags/{t} | No |
| Untag one keyword | DELETE /v1/domains/{d}/keywords/{k}/tags/{t} | No |
| Tag many keywords | POST /v1/domains/{d}/keywords/taggings | Yes |
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 back off on 503
(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.
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": "United States",
"language": "en",
"device": "desktop"
},
{
"name": "google",
"location": "United States",
"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
wordsand every engine/location/device combo insearch_engines. One request beats N. - Check quota before a big batch. Read
usage & quota and keep each batch at or under
keywords.remainingso you don't trip a402. - Tag right after you create. The create response gives you each new
keyword's
id. Collect those IDs and pass them straight intoPOST /keywords/taggingsto label the whole batch in a second call. - Read results back with pagination. When you list keywords afterwards, page
through with
pageandper_page(max1000) and use theX-Total-Count/X-Total-Pagesheaders — see Pagination. - Back off on
503. Bulk writes are the most likely to be throttled. Throttled requests return503(there are noRateLimit-*headers yet), so retry with exponential backoff — details in Errors & rate limits.
Putting it together
A typical agency onboarding flow for a new domain:
GET /v1/account/usage— confirm you have keyword headroom.POST /v1/domains/{domain_uuid}/keywords— add the wholewords×search_enginesmatrix in one call; keep eachidfrom the response.POST /v1/tags(once) — create the tag you'll group them under, if it doesn't exist yet.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.
Related guides
- Core concepts — the domain → keyword → tag model.
- Usage & quota — check headroom before a bulk write.
- Pagination — read large result sets back.
- Errors & rate limits —
402,404,422, and503backoff. - API reference: Keyword Tags, Keywords, Tags, Account.