Skip to main content
A keyword is the one object you create to monitor something: a brand, a competitor, a topic. It carries everything the monitoring needs:
  • its terms: the search text, either a single query or a list of terms OR’d into one request (a brand plus its variants), see Limits: Terms per search;
  • its Sources: the platforms you switch on, each one polled on its own schedule;
  • its interval: refreshIntervalSeconds, with an optional override per Source;
  • its webhook: an optional webhookUrl that receives each match as it is found, see Webhooks;
  • its exclusions: muteRules, the words, authors and communities you never want to see.
Nephia polls every enabled Source on the interval, keeps what matches, reads each mention for sentiment and intent, and sorts it into your buckets. You read the result back with GET /v1/keywords/{id}/results or GET /v1/mentions?keyword={id}, both free.
Keyword bodies are camelCase, unlike the rest of this API. A keyword is the one object you round-trip (read it, change a field, send it back), so its body is spelled the way its response is. See Overview.

Lifecycle

Every read, estimate, pause and resume is free. What costs credits is the polling a keyword does, see Billing.

Create a keyword

name, refreshIntervalSeconds, globalCriteria and sources are required. The answer is 201 with the keyword. The body, the plan limits, the interval floors and the multi-term rules are exactly the ones the dashboard applies: a keyword this refuses is one the Configure screen would refuse too, with the same error code.

Estimate before you write

POST /v1/keywords/estimate takes the create body and answers what the create would do, without creating anything or charging anything. It runs the create’s own validation, so a refused body comes back as valid: false with a refusal (status, code, message) rather than as an error. A valid body comes back priced under estimate:
  • monthlyCredits and checksPerMonth, in total, per Source (bySource) and per search (bySearch);
  • firstScanCredits when backfill is true: the most the first scan can cost, since on X it is charged per tweet returned;
  • planAllowance, creditsRemaining, and searchSlots (inUse, requested, limit);
  • normalized: the keyword as it would poll, after defaults and overrides are merged.
POST /v1/keywords/{id}/estimate does the same for a patch, and adds previousMonthlyCredits, monthlyCreditsDelta and resetSearches. Both answers carry an estimateToken, valid 15 minutes. Send it back in the Nephia-Estimate-Token header of the write, with the same body, and the write refuses a body that is not the one priced. The header is optional for an API key and required from the MCP server.

Update a keyword

PATCH /v1/keywords/{id} is a real patch: a key you do not send is left alone, and a key sent as null clears it. The answer is { "keyword": …, "resetSearches": [...] }. The difference matters. A search whose terms, filters or market change is started over: its first poll records what currently matches without emitting it, and resetSearches names it (source, searchId, label). The mentions caught before the change stay on the keyword. A new interval or webhook applies in place and starts nothing over.

Day-one backfill

A new keyword starts empty: it reports what appears after it exists. Pass backfill: true on create and it runs one immediate search per enabled Source and stores the results as its starting history, so GET /v1/keywords/{id}/results answers with content straight away. Three things are worth knowing before you use it. It costs one tick per Source, at that Source’s rate (see Billing): it is a real search, charged like any other poll. On X it is charged like an X search instead, 5 credits plus 3 per tweet returned, at most 65 for its page of 20, so the figure is a ceiling and an empty page costs 5. It notifies nobody. Seeded mentions fire no webhook and no channel. A backfill is context, not news, and a create that immediately POSTed fifty old items to your endpoint is how an integration gets switched off. Read them instead. It runs just after the create returns. POST answers 201 as soon as the backfill is queued, so the mentions appear a moment later, not in the response body. Mentions that came from a backfill carry "seeded": true. Everything the keyword catches on its own ticks from then on is delivered normally, and nothing is emitted twice. See History & backfill for what a keyword starts with.

Match mode

Search platforms do not all match the way you would expect. Some tolerate typos and prefixes, some are fuzzy, some rank by relevance rather than text, so a keyword on a distinctive brand name can return items that do not carry it anywhere. match is an optional field of globalCriteria (and of a search’s overrides). It says how strictly a result must carry your terms: Filtering happens before a mention is stored, so it applies to every surface at once: webhook deliveries, /results, /v1/mentions, and everything derived from them.
Under word and contains, an item whose text misses is still kept when one of its identities (the author’s handle, the subreddit, the channel) is exactly your text. A post by @nephia is not noise, whatever it happens to say.
Two things match does not touch. A search with no text (a Youtube channel, a Bluesky author, an RSS feed_url, a Reddit subreddit on its own) has no term to be held to, so nothing is filtered. And Vinted does not take the field at all: catalog search is not a text match, and a listing titled “501” is a correct hit for “jean levis 501”. One case runs the other way. Reddit comments are read from a listing, not a search, so the text is a filter we apply rather than a search the platform ran, and it applies under every match mode, platform included. Setting word or contains there tightens our filter; leaving match unset does not turn it off.

Read what it caught

The answer carries since, the keyword header (id, name, kind, aiEnabled, isActive), enabledSources, the mentions, and a nextCursor when there is another page. Each mention carries the bucket it was sorted into. Filter with since, until, source, q, bucket, intent, sentiment, engagement_min or engagement. GET /v1/mentions?keyword=KEYWORD_ID is the same read across the whole Account, narrowed to one keyword. Each mention there names the keyword it came from and the matchedTerms that caught it. Both are free in the default text mode. mode=semantic retrieves by meaning and is charged, see Credits. Before you trust a quiet page, look at the keyword itself. GET /v1/keywords and GET /v1/keywords/{id} give each Source a health: lastCheckedAt, lastOutcome, consecutiveFailures and stale. A stale Source should have been checked and was not, so an empty page from it means nothing. The list also counts them per keyword in staleSources.

Events

/results answers mentions: each catch projected to a title, a body and a reading, the same on every Source. GET /v1/keywords/{id}/events answers the raw events under them, each one exactly the body its webhook delivered: id, type, keywordId, source, the entity itself (tweet, post, listing and the rest), previousPrice, occurredAt, publishedAt, ai, and analysis when something read the item.
The answer is { "events": [...] }, newest first, across every Source and every search of the keyword, including what a search caught before its criteria last changed. since defaults to the last 24 hours, limit to 200 (500 at most), and source narrows to one Source. It is free. Reach for it in two cases: your code already handles the webhook and you want to catch up or poll instead (Webhooks: Replay events), or you need what a mention does not carry, such as Vinted’s listing.price_changed with its previousPrice, and listing.delisted.

Activity

GET /v1/keywords/{id}/activity is the diagnostic read: what Nephia did for this keyword, check by check and delivery by delivery, newest first. It is free, takes the same since (the last 7 days by default), limit and source, and answers { "activity": [...] }. Every entry also carries id, source and occurredAt; the fields that do not apply to its type are null. eventId is an id of /events. Where a Source’s health on the keyword says that something is wrong (stale, consecutiveFailures), activity says what: a run of poll.failed names the upstream error, and a run of webhook.failed with a statusCode of 500 is your endpoint, not the polling.

Pause and resume

POST /v1/keywords/{id}/pause stops every Source. Nothing is polled, nothing is charged and nothing is delivered until you resume; the mentions already caught stay readable. POST /v1/keywords/{id}/resume starts every Source that can follow. A Source that support stopped stays stopped and is named in blockedSources (source, reason) rather than failing the call. Both answer { "keyword": …, "blockedSources": [...] }. pausedReason on the keyword tells you who stopped it: user_paused when you called pause, schedule when it is outside its activation window, insufficient_credits when a tick could not be debited, plan_entitlement when a downgrade left it outside your plan, admin_paused when support paused it. Polling does not resume on its own after a top-up or an upgrade: call resume.

One Source at a time

Stops one Source and leaves the others polling. Its searches and the mentions it caught stay. It is refused when the Source is not enabled on the keyword, when the keyword itself is paused, and when it is the last Source still polling: pause the keyword instead. …/sources/{source}/resume starts it again.

Buckets

A keyword sorts its mentions into buckets: a name and one plain-language sentence saying what belongs in it. The model reads the sentence, not a word list.
  • GET /v1/keywords/{id}/buckets lists them with a count each, plus unclassifiedCount and pendingCount (mentions no sorting pass has read yet). It never triggers a sorting pass, so listing buckets cannot bill.
  • POST /v1/keywords/{id}/buckets adds one: label, rule, and an optional color. Mentions already sorted stay where they are; the next pass reads the new bucket. 409 when a bucket of that name exists.
  • DELETE /v1/keywords/{id}/buckets/{bucketId} retires one. Its mentions are not deleted: they become uncategorised, and unclassified in the answer says how many.
Filter /results to one bucket with bucket=<id>, or to the unsorted ones with bucket=uncategorised. A keyword of kind: catalog (Vinted listings) has no buckets.

Brand

Reply drafts speak for one of the brands on your Account. GET /v1/brands lists them (id, name, website, isDefault). GET /v1/keywords/{id}/brand answers what the keyword stores (brandProfileId, null for the Account’s default) and the brand that resolves to. PUT /v1/keywords/{id}/brand with { "brandProfileId": "<id>" } chooses one, and null goes back to the default. Nothing is polled differently and no search starts over. A brand id that is not on your Account is a 404. The brand card itself is edited in the dashboard and is not part of this API.

Delete a keyword

DELETE /v1/keywords/{id} stops the keyword for good and frees the plan slot. It answers 204. Without purge, it is deliberately not a hard delete: the keyword leaves GET /v1/keywords but stays readable by id with isActive: false, and the mentions it caught stay readable until retention ages them out. You retired a monitor, not the record of what it found. With ?purge=true, every mention the keyword collected is deleted as well, with its readings and marks. The keyword itself still stays readable by id. Credits are never refunded either way.

Per-Source limits

Each Source also caps its active searches per Account, see Limits.
These are the Source limits, not what your Account can actually do. Your plan applies its own floor on top, and the stricter of the two wins: the effective minimum is max(plan minimum, source minimum). A Free account’s floor is 3600s on every Source. Your plan also caps how many keywords you run and how many terms a search carries.See Limits for the per-plan figures.

Billing

A keyword is billed for the polling it does. Each check of one search on one Source is a tick: 4 credits for X, Reddit and Vinted; 5 credits for Youtube; 1 credit for Bluesky, Hacker News and RSS, charged before the poll runs. AI answers charges 100 credits per engine per run, after each engine answers, and an engine that errors is not charged. A list of terms is one request and one tick, the same as a single term. See Credits: Polling, and use the estimate to see a keyword’s monthly figure before you create it. When credits are insufficient, polling pauses with pausedReason: insufficient_credits. It does not resume automatically: call POST /v1/keywords/{id}/resume after topping up. backfill: true charges one extra tick per Source, for the one immediate search it runs. On X that search is charged per tweet returned, 5 credits plus 3 each, at most 65. An agent step (aiStep) is charged on top of the tick, 1 credit per 25 events read, and a delivery waits for the reading, up to 120 seconds. The event is delivered either way: see Webhooks: Agent step.