Antechamber API

Search Reddit evidence, hydrate the records a user opens, and create live streams for terms or fields that should keep matching over time.

Get started

Make the first useful request

The Antechamber API is organized around three actions: search for evidence, hydrate details by ID or username, and keep watching with streams.

Base URL https://api.think-pol.com
Auth Authorization: Bearer $THINKPOL_API_KEY
Format JSON responses
Contract /antechamber/swagger
  1. Call GET /v3/search with one or more terms. The response contains hydrated submissions and comments arrays that are ready to render in a results view.
  2. Use has_more and after to fetch the next search page. Do not build a client that tries to pull the complete archive in one run.
  3. When a user opens a result, use the v2 content routes to fetch the author, post, comment, replies, or subreddit context needed for that screen.
  4. When the same matcher should keep running, create a POST /v1/streams stream. Poll the stream inbox first; add a public URL only if you need push delivery.
Search for a term Use mode=word for token search and mode=phrase for exact contiguous text.
curl -sS "https://api.think-pol.com/v3/search?terms=acme&mode=word&type=comment&order=desc" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

API basics

Resources and response shapes

The API is resource-oriented, but the best entry point depends on what the user is doing.

Search results

/v3/search returns two arrays: submissions and comments. Each item includes Reddit IDs, author, subreddit, text/title fields, timestamps, and URLs where available.

Hydration routes

Use v2 routes after search gives you a username, submission ID, or comment ID. These routes are for opening a record, not for broad discovery.

Streams

Streams are persistent matchers. They push signed webhook deliveries by default when a URL is configured and write matches to a 30-day inbox only when inbox_enabled:true is set.

TaskUseNotes
Find evidenceGET /v3/searchSupports multiple terms, word or phrase mode, optional time window, and cursor pagination.
Chart volumeGET /v3/search/popularityAccepts exactly one single-token terms value. It returns counts, not evidence records.
Open a resultv2 user, post, comment, and subreddit routesUse IDs and usernames returned from search. Most list endpoints are timestamp-paginated.
Monitor future matches/v1/streamsAdd url for signed push delivery; set inbox_enabled:true for inbox polling or inbox-only streams.

Authentication

Send a bearer token on every request

All documented Antechamber routes require a valid API key in the Authorization header.

Authenticated request Keep API keys on your server. Do not ship customer API keys in browser JavaScript.
curl -sS "https://api.think-pol.com/quota" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

401 vs 403

401 means the bearer token is missing or invalid. 403 means the token is valid but is not allowed to use that endpoint, for example stream endpoints without stream access.

Quota

GET /quota returns the remaining API quota for the authenticated key as a JSON integer.

Popularity

Measure volume before retrieving evidence

Popularity returns a total or time histogram for one token. Use it to pick a time window, then call /v3/search for the matching posts and comments.

GET/v3/search/popularity
ParameterRequiredDescription
termsYesExactly one non-empty single-token term.
resolutionNoall, year, month, day, or hour. Default is month.
fromNoUnix timestamp start. Must be provided together with to.
toNoUnix timestamp end. Must be provided together with from.
bucketsNoPositive bucket count when from and to are omitted.
Do not use popularity as a result endpoint. It returns points and optional total, not submission or comment records.
Get monthly buckets The parameter is terms, matching the OpenAPI contract.
curl -sS "https://api.think-pol.com/v3/search/popularity?terms=acme&resolution=month&buckets=24" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

Pagination

Use the cursor style for the endpoint you called

Search, content lists, prefix searches, and stream inboxes all paginate, but they do not use the same cursor field.

Endpoint familyCursor requestCursor responseOrder
/v3/searchafterafter, has_morecreated_utc, default newest first
User and content listslast or sincelast, has_moreNewest first by created_utc
User/subreddit prefix searchafterNext cursor is the last returned nameLexicographic name order
Stream inboxafternext_after, has_moreAscending event_time,event_id
Totals are optional on content lists. Use include_total=true only when the UI needs a count. Otherwise rely on has_more.

Users and content

Open only the records the user needs

Use these endpoints after search returns a username, submission ID, or comment ID.

GET/v2/user/search

Autocomplete usernames by prefix. Use no_metadata=true for a fast list of names.

GET/v2/user/{username}

Fetch Reddit profile metadata. Add fetch_socials=true only when external socials are needed.

GET/v2/user/{username}/comments
GET/v2/user/{username}/subcomments
GET/v2/user/{username}/posts

List a user's top-level comments, replies, or submissions. These use limit, last, since, and optional include_total.

GET/v2/posts

Hydrate up to 100 comma-separated submission IDs with bounded comment previews.

GET/v2/posts/{id}

Fetch one post. By default, hydrate_comments=true attaches up to 100 recent comments.

GET/v2/posts/{id}/comments
GET/v2/comments/{id}
GET/v2/subcomments/{id}

Fetch a post's comments, one comment, or replies to a comment. Comment/reply lists use timestamp pagination.

Paginate comments for a post Use the returned last timestamp to fetch older comments.
curl -sS "https://api.think-pol.com/v2/posts/abc123/comments?limit=100" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

curl -sS "https://api.think-pol.com/v2/posts/abc123/comments?limit=100&last=1779107696" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

Subreddits

Add community context

Use subreddit endpoints to autocomplete communities, display metadata and activity, and page through observed subreddit authors.

GET/v2/subreddit/search

Search subreddit names by prefix with subreddit, optional after, limit, and no_metadata=true.

GET/v2/subreddit/{subreddit_name}

Fetch a subreddit overview. Short-window activity is included by default. Set fetch_activity=true for monthly activity buckets and top authors.

GET/v2/subreddit/{subreddit_name}/users

Page through observed subreddit authors with limit and string after. The response uses { items, limit, has_more, after }.

Streams

Keep matching after the first search

Streams are customer-owned live matchers. One stream has one matcher type, one optional field, one delivery identity, and many selectors. URL-backed streams push signed webhook deliveries by default. Set inbox_enabled:true when you also want the inbox: a 30-day polling feed of matched comments and posts.

EndpointUseResponse
GET /v1/streamsList active streams for the API key.{ streams: [...] }. Includes selector and selector_count, but not signing secrets.
POST /v1/streamsCreate a stream matcher with one or more selectors.201. URL-backed creates return signing_secret once; inbox-only creates require inbox_enabled:true.
GET /v1/streams/{streamID}/selectorsPage through a stream's active selectors.{ selectors, next_after, has_more }.
POST /v1/streams/{streamID}/selectorsAdd selectors to an existing stream.Updated stream with selector_count.
DELETE /v1/streams/{streamID}/selectorsRemove selectors from an existing stream.Updated stream. Removing the last selector returns 400.
GET /v1/streams/{streamID}/eventsPoll the 30-day inbox.{ events, next_after, has_more }.
POST /v1/streams/{streamID}/rotate-secretRotate a URL-backed stream secret.New signing_secret. Old secret stops working immediately.
DELETE /v1/streams/{streamID}Soft-delete an active stream.204.

What the inbox is

The inbox is an append-only polling feed for one stream. A match is written there only when the stream was created with inbox_enabled:true.

Each events[] item is one selector hit. If one Reddit row matches two selectors in the same stream, the feed returns two events with the same reddit_id and different match.selector.

Reading the inbox does not acknowledge, lock, or delete events. Events disappear only when the 30-day TTL removes old rows.

What the cursor means

next_after is the cursor after the last event in the response. It is not a page number and should be stored exactly as returned.

The cursor is opaque base64url data over event_time and event_id. Do not parse, edit, or synthesize it on the client.

  1. First poll: call GET /v1/streams/{streamID}/events?limit=100 without after.
  2. Process events in the returned order. Inbox pages are oldest-to-newest by event_time,event_id.
  3. When the response contains events, store next_after after the events are processed.
  4. If has_more=true, request the next page immediately with after=<next_after>.
  5. If has_more=false, keep the stored cursor and poll again later with the same after.
  6. If events is empty, next_after is omitted. Keep your previous stored cursor.
Limits and retention. limit defaults to 100 and caps at 500. A malformed cursor returns 400. Inbox events are retained for 30 days, so poll often enough for your integration's recovery window.

Selectors

Create streams with selectors. Inputs are trimmed, de-duplicated case-insensitively, capped at 100,000 active selectors per stream, and limited to 256 characters each.

text_contains

Matches selectors as case-insensitive substrings in normalized comment text and submission title/text. Omit field.

field_equals

Matches selectors as case-insensitive exact field values. field must be author, subreddit, or kind.

Delivery mode

Add a public HTTPS url for signed webhook push delivery. Add inbox_enabled:true for inbox recovery, or omit url with inbox_enabled:true for inbox-only polling.

Push batching

batch_max_size is the maximum number of matched events sent in one webhook request. A Reddit row that matches three selectors contributes three events. It defaults to 1 and caps at 100.

batch_max_delay_seconds flushes a non-empty batch after that many seconds. It defaults to 0, caps at 60, and 0 disables the timer.

Batching only affects URL push delivery. Inbox polling still uses the limit parameter on /events.

Create inbox and push streams Use batch_max_size and batch_max_delay_seconds for URL push; use next_after for inbox polling.
curl -sS -X POST "https://api.think-pol.com/v1/streams" \
  -H "Authorization: Bearer $THINKPOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"text_contains","selectors":["acme breach","acme leak"],"inbox_enabled":true}'

curl -sS "https://api.think-pol.com/v1/streams" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

curl -sS -X POST "https://api.think-pol.com/v1/streams" \
  -H "Authorization: Bearer $THINKPOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"text_contains","selectors":["acme breach","acme leak"],"url":"https://customer.example/webhooks/thinkpol","batch_max_size":10,"batch_max_delay_seconds":5}'

STREAM_ID="7b98c8c8-1111-4222-8333-123456789abc"

curl -sS -X POST "https://api.think-pol.com/v1/streams/$STREAM_ID/selectors" \
  -H "Authorization: Bearer $THINKPOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"selectors":["acme lawsuit","acme investigation"]}'

curl -sS "https://api.think-pol.com/v1/streams/$STREAM_ID/selectors?limit=100" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"

curl -sS -X DELETE "https://api.think-pol.com/v1/streams/$STREAM_ID/selectors" \
  -H "Authorization: Bearer $THINKPOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"selectors":["acme leak"]}'

PAGE=$(curl -sS "https://api.think-pol.com/v1/streams/$STREAM_ID/events?limit=100" \
  -H "Authorization: Bearer $THINKPOL_API_KEY")

printf "%s" "$PAGE" | jq '.events[] | {event_id, kind, reddit_id, match}'
AFTER=$(printf "%s" "$PAGE" | jq -r '.next_after // empty')

if [ "$(printf "%s" "$PAGE" | jq -r '.has_more')" = "true" ]; then
  curl -sS "https://api.think-pol.com/v1/streams/$STREAM_ID/events?limit=100&after=$AFTER" \
    -H "Authorization: Bearer $THINKPOL_API_KEY"
fi

curl -sS -X DELETE "https://api.think-pol.com/v1/streams/$STREAM_ID" \
  -H "Authorization: Bearer $THINKPOL_API_KEY"
Matched event shape Inbox polling and webhook push use the same per-event model. Webhook deliveries wrap matched events in one stream-level request.
{
  "events": [
    {
      "event_id": "evt_bitcoin",
      "stream_id": "stream_123",
      "webhook_id": "stream_123",
      "event_time": "2026-05-20T12:00:00Z",
      "source_created_utc": 1779278400,
      "kind": "comments",
      "reddit_id": "c1",
      "author": "alice",
      "subreddit": "CryptoCurrency",
      "permalink": "/r/CryptoCurrency/comments/post/c1",
      "match": { "type": "text_contains", "field": "", "selector": "bitcoin" },
      "payload": { "id": "c1", "author": "alice", "subreddit": "CryptoCurrency", "text": "bitcoin and ethereum are moving" }
    },
    {
      "event_id": "evt_ethereum",
      "stream_id": "stream_123",
      "webhook_id": "stream_123",
      "event_time": "2026-05-20T12:00:00.000000001Z",
      "source_created_utc": 1779278400,
      "kind": "comments",
      "reddit_id": "c1",
      "author": "alice",
      "subreddit": "CryptoCurrency",
      "permalink": "/r/CryptoCurrency/comments/post/c1",
      "match": { "type": "text_contains", "field": "", "selector": "ethereum" },
      "payload": { "id": "c1", "author": "alice", "subreddit": "CryptoCurrency", "text": "bitcoin and ethereum are moving" }
    }
  ],
  "next_after": "opaque-cursor",
  "has_more": false
}

Webhook signatures

Verify push deliveries before parsing them

URL-backed streams send signed batched deliveries. Verify the exact raw request body with the stream signing secret returned at create or rotate time.

  1. Read the raw request body bytes before JSON parsing. Do not verify re-serialized JSON.
  2. Use X-Thinkpol-Webhook-Id to look up the stream's saved whsec_... signing secret.
  3. Reject missing, malformed, or stale X-Thinkpol-Timestamp values. The recommended freshness tolerance is 5 minutes.
  4. Compute HMAC-SHA256 over v1.<timestamp>.<raw_json_body> using the full secret string as the key.
  5. Accept the request if one comma-separated v1=... value in X-Thinkpol-Signature matches in constant time.
  6. After verification, parse JSON and confirm body webhook_id equals the header stream id.
HeaderDescription
X-Thinkpol-Webhook-IdStream id associated with the delivery.
X-Thinkpol-TimestampUnix timestamp in seconds. Use the exact string in the signed payload prefix.
X-Thinkpol-SignatureOne or more comma-separated signatures, formatted as v1=<hex_hmac_sha256>.
Verify a callback The HMAC key is the full whsec_... string, including the prefix.
import crypto from "node:crypto";

export function verifyThinkpolWebhook(headers, rawBody, secret) {
  const timestamp = String(headers["x-thinkpol-timestamp"] || "");
  const signature = String(headers["x-thinkpol-signature"] || "");
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
  const signed = Buffer.concat([Buffer.from(`v1.${timestamp}.`), body]);
  const expected = Buffer.from(crypto.createHmac("sha256", secret).update(signed).digest("hex"), "hex");

  return signature.split(",").some((part) => {
    const value = part.trim();
    if (!/^v1=[0-9a-f]{64}$/i.test(value)) return false;
    const candidate = Buffer.from(value.slice(3), "hex");
    return candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected);
  });
}

Analyze and quota

Check account state and generate profiles

Use quota for account state. Use analysis when the product needs a generated user profile with optional sources.

GET/quota

Returns the remaining API quota for the authenticated key as an integer.

GET/analyze/{username}

Returns a generated profile. Optional query parameters include model, latest, refresh, sources, and use_case=law_enforcement.

Errors

Handle the common failure modes

Swagger is the final source for response schemas and endpoint-specific status codes. These are the statuses integrations should handle globally.

StatusMeaningClient behavior
400Invalid parameter, malformed cursor, invalid matcher, or invalid webhook URL.Fix the request. Do not retry unchanged.
401Missing or invalid bearer token.Ask for a valid API key or refresh server configuration.
403The key is valid but not allowed to use the endpoint.Show an access message or contact support for stream access.
404Requested stream or resource was not found for this customer.Stop polling that ID and refresh local state.
503Backing store or stream inbox is unavailable.Retry with backoff.
For exact request bodies, enums, examples, and try-it-out calls, use /antechamber/swagger.