Skip to content

REST API

BackTalk has a public REST API for reading and triaging signals (one at a time or in bulk), listing your keywords and watchers, pulling the report, reading and curating AI share-of-voice answers, and triggering a sync or an AI probe. It's a server-to-server surface: authenticate with a workspace API key as a bearer token. There are no cookies and no CORS headers — call it from your backend, a script, or a cron job, not a browser.

Base URL: https://backtalk.sh/api/v1

Authentication

Create a key in Settings → API keys (admin or owner only). The token is shown once, at creation — store it somewhere safe. Send it on every request:

curl https://backtalk.sh/api/v1/me \
  -H "Authorization: Bearer btk_YOUR_KEY"

A key is tied to one workspace. It stops working the moment it's revoked, expires, or the member who created it leaves the workspace. Tokens are stored only as a SHA-256 hash; BackTalk can't show you a key again or recover a lost one — revoke it and make a new one.

A key also follows its creator's current role, re-checked on every request rather than fixed at creation. Demote the creator to viewer and their keys turn read-only straight away: reads keep working, writes answer 403 forbidden. The key isn't revoked and its scopes don't change, so promoting them back to member or admin restores writing on the same token. Removing them from the workspace is the permanent version: every request, reads included, then answers 401.

Every response sends Cache-Control: no-store.

Scopes

Each key carries one or both scopes:

ScopeGrants
readGET signals, a single signal, keywords, watchers, the report, the AI share-of-voice endpoints, sync history, and probe status.
writeEverything in read, plus triaging signals (PATCH /signals/:id and bulk PATCH /signals), curating an AI answer (PATCH /ai/answers/:id), and triggering a sync (POST /sync) or an AI probe (POST /ai/probe).

A request that needs a scope the key doesn't have returns 403 forbidden. There is no config-write scope: keywords, watchers and webhooks are managed in the app, not over the API.

Endpoints

GET /me

Introspect the key — which workspace and scopes it carries. Useful for debugging.

curl https://backtalk.sh/api/v1/me -H "Authorization: Bearer btk_YOUR_KEY"
{
  "workspace": { "id": "…", "name": "Acme's workspace" },
  "key": { "id": "…", "name": "CI pipeline", "prefix": "btk_1a2b3c4d", "scopes": ["read"] }
}

GET /signals

List signals, newest first, with inbox-style filters. Requires read.

Query paramValues
statusunread, all (default), starred, replied, ignored
watchera watcher id, or none for keywords in no watcher
keywordcomma-separated keyword ids (from GET /keywords); narrows within a watcher
sourcecomma-separated source ids, e.g. hackernews,github
sentimentcomma-separated: positive, neutral, negative, unknown
prioritycomma-separated: urgent, high, normal, low
categorycomma-separated: question, bug_report, complaint, praise, comparison, recommendation_ask, other
intentcomma-separated buying-intent strength: high, medium, low
searchfree-text match over title, snippet, author and url
sortnewest (default) or oldest
since / untilISO date (2026-07-15) or datetime — bounds on when the signal was found
limit1–100 (default 50)
cursoropaque keyset cursor (see Pagination)
curl -G https://backtalk.sh/api/v1/signals \
  -H "Authorization: Bearer btk_YOUR_KEY" \
  --data-urlencode "sentiment=negative" \
  --data-urlencode "since=2026-07-15" \
  --data-urlencode "limit=20"
{
  "signals": [
    {
      "id": "…",
      "source_id": "hackernews",
      "title": "…",
      "snippet": "…",
      "url": "https://news.ycombinator.com/item?id=…",
      "state": "new",
      "starred": false,
      "sentiment": "negative",
      "found_at": "2026-07-15T09:12:00Z"
    }
  ],
  "next_cursor": "eyJmb3VuZF9hdCI6…"
}

GET /signals/:id

Full detail for one signal. Requires read. A signal id from another workspace returns 404 not_found — never another tenant's data.

PATCH /signals/:id

Triage a signal. Requires write. Body (JSON, at least one of state / starred / sentiment):

{ "state": "ignored", "ignore_reason_chip": "job_ad", "ignore_reason_text": "recruiter post" }
curl -X PATCH https://backtalk.sh/api/v1/signals/SIGNAL_ID \
  -H "Authorization: Bearer btk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"starred": true}'

Returns the updated signal.

PATCH /signals (bulk)

Apply one triage patch to many signals at once. Requires write. Body (JSON): an ids array (1–100) plus the same fields as the single-signal PATCH (at least one of state / starred / sentiment).

curl -X PATCH https://backtalk.sh/api/v1/signals \
  -H "Authorization: Bearer btk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["…", "…"], "state": "seen"}'
{ "updated": 2, "ids": ["…", "…"] }

updated and ids report exactly what changed: unknown, cross-tenant, or past-retention ids simply don't match, so a partial batch is a success, not an error.

GET /keywords and GET /watchers

List the workspace's keywords (phrase, variants, exclude terms, active state) and watchers. Both require read.

GET /report

The /app/report rollup for a window: volume, replied count, oldest-unread age, sentiment (positive/neutral/negative and net = positive − negative), per-source counts, and a zero-filled trend. Requires read.

Pass a rolling preset with period = today (default) | 7d | 30d, or a custom range with since / until (ISO date or datetime; until defaults to now). When since is given it wins and period is ignored, and period in the response reads "custom". Custom ranges are capped at 366 days; a wider or inverted range returns 400 invalid_request.

curl "https://backtalk.sh/api/v1/report?period=today" -H "Authorization: Bearer btk_YOUR_KEY"
curl -G "https://backtalk.sh/api/v1/report" -H "Authorization: Bearer btk_YOUR_KEY" \
  --data-urlencode "since=2026-07-01" --data-urlencode "until=2026-07-31"

GET /ai/topics

The workspace's AI share-of-voice topics: each is one buying decision with its prompts, your brand aliases, and the competitors to watch. Requires read.

GET /ai/share-of-voice?topic=&window=7|30

Standings for one topic: how often AI models (Claude, ChatGPT, Gemini, Perplexity) recommend you vs competitors. Requires read. Omit topic for the first topic with data; window defaults to 30. Until the first probe has run, the response has "available": false. When sampled answers mention no tracked brand at all, rank (and by_model[].your_rank) is null: no real ranking exists yet.

The blended share_pct weights every compared model equally: it is the average of each model's own share, not a pooling of all answers, so a model you probe more often than another does not skew it. Each by_model[] entry is that one model's own share. Because the blend is an average of per-model distributions, share_pct across tracked brands still sums to about 100.

curl "https://backtalk.sh/api/v1/ai/share-of-voice?window=7" -H "Authorization: Bearer btk_YOUR_KEY"
{
  "available": true,
  "topic": { "id": "…", "name": "Payment APIs" },
  "window_days": 7,
  "runs_sampled": 420,
  "previous_window_sampled": true,
  "standings": [
    { "brand": "Stripe", "is_you": false, "share_pct": 41.2, "previous_share_pct": 43.0, "rank": 1 },
    { "brand": "Acme", "is_you": true, "share_pct": 28.4, "previous_share_pct": 24.1, "rank": 2 }
  ],
  "by_model": [{ "family": "claude", "your_share_pct": 31.0, "your_rank": 2 }],
  "trend": [{ "day": "2026-07-14", "share_pct": { "Acme": 27.5, "Stripe": 42.1 } }],
  "families": ["claude", "chatgpt", "gemini", "perplexity"]
}

A family id names one compared model. It stays the short form above while a provider contributes a single model; when a workspace compares several models from the same provider, each appears as its own provider:model entry (for example "venice:llama-3.3-70b").

When previous_window_sampled is false, the previous window had no sampled answers, and every previous_share_pct is 0 by convention rather than a measurement. Suppress deltas in that case instead of showing a movement from zero.

GET /ai/answers

The sampled AI answers behind those numbers, newest first, keyset-paginated like /signals. Requires read. Each row carries the model, the prompt asked, the answer excerpt, the extracted brand mentions, and any citations the model returned.

Query paramValues
topica topic id (see GET /ai/topics)
modela model family (claude, chatgpt, gemini, perplexity), a provider id, or a provider:model pair to pin one model
starredtrue / false to filter on answers starred in the app
limit1–100 (default 25)
cursoropaque keyset cursor (see Pagination)

PATCH /ai/answers/:id

Curate one sampled answer. Requires write. Body (JSON, at least one of starred / flagged):

An answer id from another workspace returns 404 not_found.

curl -X PATCH https://backtalk.sh/api/v1/ai/answers/ANSWER_ID \
  -H "Authorization: Bearer btk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"flagged": true}'

Returns the updated answer.

GET /ai/probe

Current probe status. Requires read. Reports whether a probe is running (with live done/total progress) and today's manual-probe allowance.

curl https://backtalk.sh/api/v1/ai/probe -H "Authorization: Bearer btk_YOUR_KEY"
{ "running": false, "progress": { "done": 0, "total": 0 }, "used_today": 1, "remaining": 5, "limit": 6 }

remaining and limit are null when the plan has no per-day cap.

POST /ai/probe

Trigger a manual AI share-of-voice probe: ask the AI models each topic's prompts and record who they recommend. Requires write. No body. The probe runs in the background, so this returns a run_id right away; poll GET /ai/probe for progress, or GET /ai/share-of-voice / GET /ai/answers for results a minute or two later.

curl -X POST https://backtalk.sh/api/v1/ai/probe -H "Authorization: Bearer btk_YOUR_KEY"
{ "ok": true, "run_id": "…", "remaining": 5 }

Reuses the in-app "Probe now" gate: a per-plan daily cap (or sponsored grants) and one run at a time, so calling too often returns 429 with Retry-After. A workspace with no AI key enabled for probing returns 400 invalid_request. remaining is the manual probes left today (null when unlimited).

POST /sync

Trigger a manual sync for the workspace. Requires write. This reuses the in-app "Sync now" gate: a workspace-wide cooldown and a per-plan daily cap. When you hit either, you get 429 with a Retry-After header.

curl -X POST https://backtalk.sh/api/v1/sync -H "Authorization: Bearer btk_YOUR_KEY"

GET /syncs

Sync-run history, newest first, keyset-paginated like /signals. Requires read. Each run carries its trigger (manual / cron / backfill), a derived status (ok, error, running, aborted, partial), totals, and a per-source breakdown — the same view the in-app Sync history page renders.

Query paramValues
limit1–100 (default 25)
cursoropaque keyset cursor (see Pagination)
curl https://backtalk.sh/api/v1/syncs -H "Authorization: Bearer btk_YOUR_KEY"

Pagination

GET /signals, GET /ai/answers and GET /syncs use keyset (cursor) pagination, ordered by time then id, descending by default and ascending for GET /signals?sort=oldest. When a page is full there's a next_cursor; pass it back as ?cursor=… for the next page. When next_cursor is null, you've reached the end. Cursors are opaque, so don't construct or parse them.

Errors

Every non-2xx response is the same envelope:

{ "error": { "code": "forbidden", "message": "This API key needs the \"write\" scope." } }
CodeHTTPWhen
unauthorized401Missing/invalid/revoked/expired key, or its creator left the workspace. Sends WWW-Authenticate: Bearer.
forbidden403The key lacks the scope the endpoint needs, or its creator is now a viewer (read-only), so writes are refused.
workspace_read_only403The workspace is read-only (lapsed trial / inactive subscription). Reads still work; writes are blocked.
not_found404No such resource in this workspace.
invalid_request400Failed validation, malformed JSON, or an oversized body.
rate_limited429Too many requests (or sync cooldown/cap). Sends Retry-After.
internal500Something failed on our end. The message is generic; details are in our logs.

Branch on code, not on the message text.

Rate limits

Each key is limited to 120 requests per minute (fixed window). Every response includes:

Over the limit returns 429 rate_limited with Retry-After. Limits are per key, so give each integration its own key — a workspace's total throughput therefore scales with its number of active keys (up to 20 keys per workspace).

Repeated failed authentication attempts are also throttled (per client IP). If you're getting 429 on a key you believe is valid, check the Authorization header format first: Bearer btk_….