> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nephia.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK (Node.js)

> Official TypeScript client for the Nephia API.

Install the official SDK and call `/v1/*` with typed namespaces. Requires Node ≥ 20.

```bash theme={null}
npm install nephia
```

```ts theme={null}
import { Nephia } from "nephia";

const client = new Nephia({ apiKey: process.env.NEPHIA_API_KEY });

// Everything your keywords caught in the last 24 hours, newest first.
const { mentions } = await client.mentions.list({ limit: 50 });

for (const mention of mentions) {
  console.log(mention.source, mention.sentiment, mention.title, mention.url);
}
```

Create the keyword that does the catching:

```ts theme={null}
const keyword = await client.keywords.create({
  name: "figma",
  refreshIntervalSeconds: 900,
  globalCriteria: { market: "fr", terms: ["figma", "figma alternative"] },
  sources: [
    { source: "reddit", enabled: true },
    { source: "x", enabled: true },
  ],
  aiEnabled: true,
  sentimentEnabled: true,
});
```

## Authentication

Pass `apiKey` or set `NEPHIA_API_KEY`. Every request sends the `x-api-key` header.

```ts theme={null}
const client = new Nephia({ apiKey: "YOUR_API_KEY" });
```

## Namespaces

| Namespace | Examples |
| - | - |
| `client.account` | `credits()` |
| `client.mentions` | `list()`, `stats()`, `similar()` |
| `client.keywords` | `list()`, `get()`, `listResults()`, `events()`, `activity()`, `aiAnswerRuns()`, `listBuckets()`, `create()`, `update()`, `pause()`, `resume()`, `delete()` |
| `client.explore` | `estimate()`, `run()` |
| `client.analyses` | `run()`, `estimate()` |
| `client.vinted` | `items.getPriceHistory()`, `market.stats()` |

Params use **camelCase** in TypeScript. The SDK converts them to **snake\_case** on the wire,
and responses come back as-is (camelCase JSON).

`client.keywords.create()`, `client.keywords.update()` and both `client.explore` calls are the
exception: their bodies travel **camelCase**, unconverted. A keyword is the one object you round-trip (read it, change a field,
send it back), and that only works if the body's keys are spelled the way the response spells them.

## Mentions

```ts theme={null}
// Free: a substring scan over your own stored mentions.
const page = await client.mentions.list({
  since: "2026-08-01T00:00:00.000Z",
  source: "reddit",
  intent: ["purchase_intent", "comparison"],
});

// Charged: searches by meaning instead of by substring.
const semantic = await client.mentions.list({ q: "pricing complaints", mode: "semantic" });
```

Count instead of paging. Free, and the same numbers Insights shows:

```ts theme={null}
// One row per Source and sentiment over the last seven days.
const { total, rows } = await client.mentions.stats({ group_by: "source,sentiment" });

// The ten authors who said the most in a closed week.
const authors = await client.mentions.stats({
  group_by: "author",
  since: "2026-09-01T00:00:00.000Z",
  until: "2026-09-08T00:00:00.000Z",
});

// Charged: the mentions closest in meaning to one you already have.
const similar = await client.mentions.similar(page.mentions[0].id);
```

Page with the cursor until it stops coming back:

```ts theme={null}
let cursor: string | undefined;
do {
  const page = await client.mentions.list({ cursor, limit: 100 });
  handle(page.mentions);
  cursor = page.nextCursor;
} while (cursor);
```

## Explore

Ask once, without creating a keyword. Price it, show the figure, then run the body you priced:

```ts theme={null}
const body = {
  globalCriteria: { market: "global", query: "alternative to stalkr" },
  sources: [
    { source: "reddit", enabled: true },
    { source: "hackernews", enabled: true },
  ],
};

// Free: the run's own validation, the credits per search and your balance.
const { valid, estimate, estimateToken, refusal } = await client.explore.estimate(body);

// Charged per search, when it runs. The token refuses any other body.
const { run, mentions, outcomes } = await client.explore.run(body, { estimateToken });

// The run is kept: read it again for free rather than running it twice.
const again = await client.mentions.list({ run: run.id });
```

## Keywords

```ts theme={null}
const { keywords } = await client.keywords.list();
const detail = await client.keywords.get(keywords[0].id);

// Mentions scoped to one keyword, carrying the bucket each was sorted into.
const { mentions } = await client.keywords.listResults(detail.id, { limit: 50 });

// A patch: a key you leave out is left alone; `null` clears it.
const { keyword, resetSearches } = await client.keywords.update(detail.id, {
  refreshIntervalSeconds: 1800,
});
```

`resetSearches` is worth reading. Changing what a search looks for (its terms, filters or
market) starts that search over: its first poll records what currently matches without
emitting it, and new results arrive from then on. The mentions caught before the change
stay on the keyword. A new interval or webhook applies in place and resets nothing. The list names the searches that were started over; an empty list means none
were.

<h2 id="events">
  Events, activity and AI answer runs
</h2>

Three free reads sit beside `listResults()`, for code that speaks the webhook or has no
endpoint to receive one:

```ts theme={null}
// The raw replay: each event is the body its webhook delivered, newest first.
const { events } = await client.keywords.events(detail.id, {
  since: "2026-07-01T00:00:00.000Z",
  source: "reddit",
});

// What happened underneath: every check, and every webhook delivery with its status code.
const { activity } = await client.keywords.activity(detail.id, { since: "2026-07-01T00:00:00.000Z" });
const failed = activity.filter((entry) => entry.type === "webhook.failed");

// Every stored run of the keyword's AI answers Source, the unchanged ones included.
const { runs } = await client.keywords.aiAnswerRuns(detail.id, { engine: "chatgpt" });
```

In Python the same three are `client.keywords.events()`, `client.keywords.activity()` and
`client.keywords.ai_answer_runs()`. See [Webhooks: Replay events](/webhooks#replay) and
[AI answers](/sources/ai-answers#reading-the-history).

## Command line

The same package ships a `nephia` binary: one command per `/v1` operation, for a shell, a cron job or a coding agent. No install needed with `npx`, no configuration file: the key comes from `NEPHIA_API_KEY` or `--api-key`.

```bash theme={null}
npx nephia mentions list --since 24h --sentiment negative --intent question
npx nephia mentions stats --group-by day,source --since 7d
npx nephia keywords list --json | jq '.keywords[] | select(.staleSources > 0)'
```

The grammar is `nephia <group> <command> [positionals] [--flag value]...`. Path parameters are positionals in order, query parameters are flags (`group_by` is `--group-by`), and a body is `-f body.json`, `-f -` for stdin, or `--body '{...}'`, sent as written. `--since` also takes a duration: `30m`, `24h`, `7d`. `nephia --help` and `nephia <group> --help` list every command.

Without a terminal on stdout, the output is the `/v1` response body as JSON, unchanged; on a terminal, a short table (`--json` or `--table` to choose). Errors are one line on stderr, and the exit code says what happened:

| Code | Meaning |
| - | - |
| `0` | Success |
| `1` | The API refused or failed |
| `2` | Usage: unknown command, wrong arguments, no API key |
| `3` | Spending refused |

A command that spends credits (`keywords create`, `keywords update`, `explore run`, `analyses run`, `mentions similar`, the Vinted reads, and `--mode semantic`) asks before it calls. Without a terminal it exits `3` and calls nothing, unless `--yes` is passed. `keywords create`, `keywords update` and `explore run` price themselves first with the free estimate and write with its token:

```bash theme={null}
npx nephia keywords estimate -f keyword.json
npx nephia keywords create -f keyword.json --yes
```

The package also carries an `AGENTS.md` for coding agents such as Claude Code or Codex: `npx nephia agents` prints it.

## Webhook signatures

```ts theme={null}
import { Nephia } from "nephia";

const ok = await Nephia.webhooks.verify(rawBody, req.headers["x-signature"], secret);
```

The `X-Signature` header is `t=<unix>,v1=<hmac>`; `verify` recomputes the HMAC over `"<t>.<body>"` with your account's signing secret (dashboard → API Keys) and rejects timestamps older than 5 minutes (configurable via `{ toleranceSeconds }`).

## Errors & retries

Typed errors: `AuthenticationError` (401), `InsufficientCreditsError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, or 503 with code `RATE_LIMITED`), `ServiceUnavailableError` (other 503s), `NephiaError` (fallback). All expose `status`, `code`, `body`, and `retryAfter`.

The client retries **429 / 502 / 503 / 504** and network failures (respects `Retry-After`, capped at 60s). It does **not** retry other 4xx or timeouts. Every POST carries an auto-generated `Idempotency-Key` reused across attempts, so retried creates can never duplicate a keyword or double-charge credits. Override it per request with `{ idempotencyKey }`. Per-request options also accept `signal`, `timeoutMs`, and `headers`.

## Next steps

* [Quickstart](/quickstart)
* [Credits](/credits)
* [Keywords](/keywords)
* [Webhooks](/webhooks)
* [API Reference](/api-reference/overview)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.