Skip to content

REST API

BackTalk has a public REST API for reading and triaging signals, listing your keywords and watchers, pulling the report, and triggering a sync. 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.

Every response sends Cache-Control: no-store.

Scopes

Each key carries one or both scopes:

ScopeGrants
readGET signals, a single signal, keywords, watchers, and the report.
writeEverything in read, plus triaging a signal (PATCH) and triggering a sync (POST /sync).

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
sourcecomma-separated source ids, e.g. hackernews,github
sentimentcomma-separated: positive, neutral, negative, unknown
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):

{ "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.

GET /keywords and GET /watchers

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

GET /report?period=today|7d|30d

The /app/report rollup for a rolling 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.

curl "https://backtalk.sh/api/v1/report?period=today" -H "Authorization: Bearer btk_YOUR_KEY"

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"

Pagination

GET /signals uses keyset (cursor) pagination, ordered by found_at then id, descending. 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 — 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.
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.

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_….