How do keyword sources work?
The core unit of tracking — phrases, locales, devices, and schedules in one configurable object.
A keyword source is the core unit of tracking in Nozzle. It bundles together everything Nozzle needs to monitor a set of keywords on an ongoing basis: the phrases themselves, the locales (country + city + language combinations) to track them in, the devices (desktop or mobile), and the schedule for how often to re-check the SERPs.
One keyword source equals one continuous tracking pipeline. The keywords inside it get re-pulled on the schedule you've defined, the SERPs get parsed and stored, and the metrics flow into reports, dashboards, and BigQuery.
This guide covers how keyword sources are structured, how to read them via the API, how to update them safely, and the common patterns for managing them programmatically.
Anatomy of a keyword source
Every keyword source has the following top-level fields:
| Field | Type | Description |
|---|---|---|
id |
integer | Unique identifier for this keyword source |
versionId |
integer | Timestamp-style version (YYYYMMDDHHMMSS) — see The versionId mechanic |
workspaceId |
integer | The workspace this belongs to |
teamId |
integer | The project this belongs to |
name |
string | Display name (e.g., "Branded Terms — US/UK") |
description |
string | Optional notes |
type |
enum | basic, json, or advanced — see below |
schedules |
array | One or more {rrule, isPriority} objects defining when to pull |
keywordCount |
integer | Total tracked keyword combinations |
uniquePhraseCount |
integer | Distinct phrases |
uniqueLocaleCount |
integer | Distinct locales |
uniqueDeviceCount |
integer | Distinct devices |
groups |
array | Optional top-level keyword groupings |
config |
object | Type-specific configuration — shape varies by type |
kind |
string | Always "keywordSource" |
createdAt / updatedAt / deletedAt |
timestamp | Standard lifecycle timestamps |
The shape of config is what differs between the three types.
The keyword source types
Nozzle supports three keyword source types. Each has a different config shape and a different sweet spot. For full detail, see Keyword source types.
basic
You provide a list of phrases, a list of localeIds, and a list of devices. Nozzle multiplies them. If you have 10 phrases, 4 locales, and 2 devices, you get 80 tracked combinations.
{
"type": "basic",
"config": {
"phraseGroups": [
{ "phrase": "running shoes", "groups": null },
{ "phrase": "trail running shoes", "groups": null }
],
"localeIds": [44249, 14964],
"devices": ["d", "m"]
}
}
Use basic whenever it fits. It's the least error-prone — Nozzle handles the multiplication, so you can't accidentally end up with phrases tracked in the wrong locales or vice versa.
json
Each phrase carries its own list of localeIds and devices. Useful when different phrases need different targeting — for example, POI-specific keywords that only make sense in their respective cities.
{
"type": "json",
"config": {
"jsonKeywords": [
{
"phrase": "tokyo station luggage storage",
"localeIds": [18995],
"devices": ["m"],
"groups": ["POI tracking"]
},
{
"phrase": "gare du nord luggage storage",
"localeIds": [168406],
"devices": ["m"],
"groups": ["POI tracking"]
}
]
}
}
More flexible than basic, but easier to get wrong. Use it when you genuinely need per-phrase locale control.
advanced
A more flexible variant for complex tracking scenarios. Most customers don't need this — see the types reference for specifics.
Reading a keyword source
Fetch a keyword source by its ID:
curl 'https://api.nozzle.app/keywordSources/<keyword_source_id>?workspaceId=<your_workspace_id>' \
-H 'accept: application/json' \
-H 'authorization: Token <your_api_key>'
The response includes everything: metadata, the full phrase list, locales, devices, schedules, and the all-important versionId. If you intend to update this keyword source later, save the entire response — you'll need most of it for the PUT request.
Example response (basic type):
{
"success": true,
"data": {
"id": 823965476044669,
"versionId": 251007225055,
"workspaceId": 893121228039810,
"teamId": 608677144962928,
"name": "Industry Verticals",
"type": "basic",
"schedules": [
{
"rrule": "FREQ=DAILY;BYHOUR=0;BYMINUTE=0;BYSECOND=0",
"isPriority": false
}
],
"keywordCount": 42,
"uniquePhraseCount": 42,
"uniqueLocaleCount": 1,
"uniqueDeviceCount": 1,
"groups": [],
"config": {
"phraseGroups": [
{ "phrase": "how seo services drive success across key industries", "groups": null }
],
"localeIds": [44249],
"devices": ["d"]
},
"kind": "keywordSource",
"createdAt": "2025-10-07T22:50:55Z",
"updatedAt": "2025-10-07T22:50:55Z"
}
}
To find a keyword source's ID, look in the URL of its page in app.nozzle.io, or list the keyword sources for a project. See the Admin API reference for the listing endpoint.
The versionId mechanic
The versionId is Nozzle's optimistic concurrency control. Every time a keyword source is saved, a new versionId is generated based on the save timestamp.
When you PUT an update, the server compares the versionId in your request body against the current versionId in the database. If they don't match, it means someone (or something) else modified the keyword source between your GET and your PUT — and your update may be rejected to prevent silently overwriting the other change.
What this means in practice:
- Always GET immediately before PUT. The shorter the gap between fetching and updating, the less likely you are to hit a
versionIdmismatch. - Don't hold onto in-memory copies for long periods. A keyword source you GET'd an hour ago is likely stale. Re-fetch before you write.
- If you get a 409 Conflict on PUT, re-GET and retry. Don't blindly bump the
versionId— that defeats the purpose. Re-fetch the current state, re-apply your intended change against that fresh copy, then PUT again. - Treat updates as deterministic functions of inputs, not as edits to a long-lived object. "Add these 5 phrases" is robust. "Modify this in-memory keyword source I've been holding for 3 minutes" is not.
Updating a keyword source
Updates use PUT /keywordSources/{id}, and the request body must contain the full keyword source object — not just the fields you want to change. This is the most common stumbling block for new API integrations.
The pattern is:
- GET the keyword source
- Modify the response object in memory (add/remove phrases, change devices, etc.)
- PUT the entire modified object back, including the
versionIdfrom the GET
Example: adding a new phrase to an existing basic keyword source.
curl 'https://api.nozzle.app/keywordSources/<id>?workspaceId=<ws>' \
-X 'PUT' \
-H 'accept: application/json' \
-H 'authorization: Token <your_api_key>' \
-H 'content-type: application/json' \
--data-raw '{
"id": 823965476044669,
"versionId": 251007225055,
"workspaceId": 893121228039810,
"teamId": 608677144962928,
"name": "Industry Verticals",
"type": "basic",
"schedules": [
{ "rrule": "FREQ=DAILY;BYHOUR=0;BYMINUTE=0;BYSECOND=0", "isPriority": false }
],
"config": {
"phraseGroups": [
{ "phrase": "how seo services drive success across key industries", "groups": null },
{ "phrase": "new phrase being added", "groups": null }
],
"localeIds": [44249],
"devices": ["d"]
},
"kind": "keywordSource"
}'
The response includes the new versionId. If you plan to make another update, use that fresh value in your next PUT.
Common patterns
Adding phrases to a basic keyword source
GET the keyword source, append entries to config.phraseGroups, PUT the whole object back. Each entry is { "phrase": "...", "groups": null } (or with group names if you're tagging it).
Removing phrases
GET, filter config.phraseGroups to exclude the phrases you want gone, PUT.
Adding phrases to a json keyword source
GET, append entries to config.jsonKeywords with the localeIds and devices each new phrase needs, PUT. Each phrase carries its own targeting.
Changing the tracking schedule
Modify the schedules array. Each entry is an rrule string plus an isPriority flag. Multiple schedule entries are allowed — Nozzle pulls on whichever rule fires first. See How do I create a custom schedule using an RRULE? for the full RRULE format.
Adding a new locale
If your keyword source needs a locale that's not already in use, you'll need its localeId. See How do Nozzle locales work? for finding existing locale IDs and requesting new ones if the combination you need isn't in the catalog yet.
Tagging phrases with groups
You can tag individual phrases with one or more group names by populating the per-phrase groups field. Groups let you slice and filter keywords in reports — think of them as labels.
{
"phrase": "flight club tokyo",
"groups": ["fc brand", "fc tokyo"],
"devices": ["i"],
"localeIds": [271011]
}
A phrase can belong to multiple groups, and groups can be added or removed by updating the groups array on each phrase and PUTting the keyword source.
Creating new keyword sources
Most customers create keyword sources through the Nozzle UI in app.nozzle.io rather than via the API — the UI handles the locale and device pickers, schedule builder, and validation for you.
If you have a use case for programmatic creation (e.g., onboarding many tenants of a SaaS product, or syncing from an external content management system), reach out and we'll walk you through the create payload for the type you need.
Common gotchas
- PUT requires the full object, not a patch. If you send only the fields you want to change, the server treats the missing fields as removals.
- Stale
versionIdcan cause unexpected behavior. Always GET immediately before PUT, and never reuse aversionIdfrom a long-cached copy. - Mixing types isn't supported. A basic keyword source can't have a
config.jsonKeywordsfield, and vice versa. If you need per-phrase targeting, the keyword source needs to be created as typejsonfrom the start. - Locale IDs aren't always available. Not every
(criteria_id, language)combination has alocaleIdyet. If you hit a missing locale, see the locales guide for how to request additions. - Schedule changes only affect future pulls. Updating
schedulesdoesn't retroactively re-pull historical SERPs. To backfill, talk to support.
Need help?
If you're working through a tricky keyword source scenario — large bulk updates, complex per-phrase targeting, schedule changes, or anything that's behaving unexpectedly — reach out. We've helped customers run keyword sources of every shape, and we'd rather work through it with you than have you stuck.