- its terms: the search text, either a single
queryor a list oftermsOR’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
webhookUrlthat receives each match as it is found, see Webhooks; - its exclusions:
muteRules, the words, authors and communities you never want to see.
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:
monthlyCreditsandchecksPerMonth, in total, per Source (bySource) and per search (bySearch);firstScanCreditswhenbackfillis true: the most the first scan can cost, since on X it is charged per tweet returned;planAllowance,creditsRemaining, andsearchSlots(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. Passbackfill: 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.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
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.
{ "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
…/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}/bucketslists them with acounteach, plusunclassifiedCountandpendingCount(mentions no sorting pass has read yet). It never triggers a sorting pass, so listing buckets cannot bill.POST /v1/keywords/{id}/bucketsadds one:label,rule, and an optionalcolor. 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, andunclassifiedin the answer says how many.
/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.
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 ofterms 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.