Skip to content
  • There are no suggestions because the search field is empty.

How do keyword sources work?

The core unit of tracking — phrases, locales, devices, and schedules in one configurable object.

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 basicjson, 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 versionId mismatch.
  • 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:

  1. GET the keyword source
  2. Modify the response object in memory (add/remove phrases, change devices, etc.)
  3. PUT the entire modified object back, including the versionId from 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 versionId can cause unexpected behavior. Always GET immediately before PUT, and never reuse a versionId from a long-cached copy.
  • Mixing types isn't supported. A basic keyword source can't have a config.jsonKeywords field, and vice versa. If you need per-phrase targeting, the keyword source needs to be created as type json from the start.
  • Locale IDs aren't always available. Not every (criteria_id, language) combination has a localeId yet. If you hit a missing locale, see the locales guide for how to request additions.
  • Schedule changes only affect future pulls. Updating schedules doesn'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.