API reference

One key. Five sources. Ranked results, billed per result.

Overview

A search API for Reddit, X, Threads, LinkedIn, and YouTube. POST a query, GET-poll for results.

JSON in, JSON out. Each platform returns as it finishes, so the first results land in a few seconds and the slowest source takes 30 to 90 seconds.

Base URL:

https://platform.usegorilla.app/v1/

Get an API key

  1. Sign in at platform.usegorilla.app.
  2. Open the side menu, choose API Keys, click Create.
  3. Copy the key. It's shown once, prefixed grla_.

New accounts get 1,000 credits a week on the free tier. No card required.

Your first call

Two requests. POST kicks off the search. GET polls for results.

curl -X POST https://platform.usegorilla.app/v1/v2-search-stream \
  -H "x-api-key: grla_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "people looking for an AI meal planner", "since": "180d"}'

Response (immediate):

{ "search_id": "7a91-...", "status": "running" }

Then poll every 1.5s until status is no longer "running". See Streaming for the loop.

Authentication

Send your API key on every request:

-H "x-api-key: grla_..."

Keys are scoped to your user account and revocable. They don't expire. Manage them at platform.usegorilla.app/api-keys/.

Keys are secrets. Don't ship them in a public frontend, proxy through your backend instead.

Endpoints

EndpointWhat it doesLatency
POST /v2-search-streamKick off a multi-source search. Returns a search_id + initial state.<1s
GET /v2-search-stream?id=…Poll a running search for the latest per-source state. Free, no credits charged.<1s
PATCH /v2-search-stream?id=…
body: {shared: true}
Mark a search publicly shareable. Owner-only. Lets anyone load it by id without spending credits.<1s
GET /billing-statusPlan, credit balance, API-key state. Authenticated users only.<1s

Request

{
  "query": "string",                       // required, max 500 chars
  "since": "180d",                         // 24h | 7d | 30d | 180d | 6mo | all | ISO date
  "limit": 50,                             // 1–200, default 50
  "sources": ["reddit", "twitter", "threads"],  // optional, default = all 5
  "channels": {                              // optional, see Channel filtering
    "reddit": ["cooking", "MealPrepSunday"],
    "twitter": ["sarahbuilds"]
  }
}
FieldTypeNotes
querystringRequired. Max 500 chars. Sent to each platform.
sincestringTime window. Reddit + X apply it in the upstream query. Threads, LinkedIn, YouTube filter post-fetch.
limitint1 to 200. Default 50. Caps the returned list. Billing uses the full unsliced count.
sourcesstring[]Subset of reddit | twitter | threads | linkedin | youtube. Default fans out to all.
channelsobjectPer-source narrow scope. See Channel filtering.

Response

200 OK response shape:

{
  "search_id": "7a91-...",
  "status": "completed",
  "results": [...],                  // sorted by result_score desc, sliced to limit
  "total": 47,
  "buckets": { "hot": 12, "warm": 18, "cold": 17 },
  "done_sources": ["reddit", "twitter", "threads", "linkedin", "youtube"],
  "errors": {},                      // per-source error map, if any
  "credits_charged": 1418,
  "credits_remaining": 18582
}

Result row

{
  "id": "abc123",
  "source": "reddit",
  "channel": "MealPrepSunday",                // subreddit / X handle / channel name
  "title": "Need an app that plans meals + builds the grocery list",
  "url": "https://reddit.com/r/MealPrepSunday/...",
  "body_snippet": "My partner and I waste 30 min every Sunday...",
  "score": 142,
  "num_comments": 28,
  "created_utc": 1716480000,
  "result_score": 0.94
}

score is the upstream engagement metric (Reddit upvotes, X likes). result_score is the 0-1 relevance score, see Scoring. search_id is your polling key and your billing audit reference.

Errors

HTTP status code + a machine-readable code field. Branch on code, show error to humans.

StatusCodeWhen
400missing_queryquery empty or missing.
400invalid_paramMalformed JSON or out-of-range value.
401missing_authNo x-api-key header.
402insufficient_creditsBalance below the per-call reservation. Body includes balance and required.
403invalid_authKey is well-formed but unrecognized or revoked.
404invalid_paramPolled search_id not found or owned by another user.
429rate_limitToo many calls in the rolling hour window.
500internalUnexpected. Include the search_id in bug reports.
502upstream_errorUpstream provider returned an error.
504upstream_timeoutA platform took too long. Per-source cap is 90s.

Example body:

{
  "error": "Insufficient credits. Have 124, need up to 500.",
  "code": "insufficient_credits",
  "balance": 124,
  "required": 500
}

Streaming

POST kicks off the search. GET-poll until status flips. Each poll returns every source that has finished so far, so the client can render results progressively.

1. Kick off

curl -X POST https://platform.usegorilla.app/v1/v2-search-stream \
  -H "x-api-key: grla_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "people looking for an AI meal planner", "since": "180d"}'

Returns 202 immediately:

{
  "search_id": "",
  "status": "running",
  "requested_sources": ["reddit", "twitter", ...],
  "suggested_interval_ms": 1500
}

2. Poll

curl "https://platform.usegorilla.app/v1/v2-search-stream?id=" \
  -H "x-api-key: grla_..."
{
  "search_id": "",
  "status": "running",                // running | completed | failed
  "done_sources": ["reddit", "twitter"],
  "pending_sources": ["threads", "linkedin", "youtube"],
  "results": [...],
  "total": 23,                       // grows each poll until status=completed
  "buckets": { "hot": 6, "warm": 9, "cold": 8 },
  "errors": {},
  "credits_charged": null,            // populated when status=completed
  "credits_remaining": null
}

Poll every 1.5s. State persists for 1 hour, so a client that crashes mid-poll can resume by id.

Billing: one charge per search. Reservation on POST, settled on completion. Failed searches and zero-result searches refund automatically.

Channel filtering

Scope the search to specific subreddits or X handles:

{
  "query": "meal planner alternative",
  "channels": {
    "reddit":  ["cooking", "MealPrepSunday", "EatCheapAndHealthy"],
    "twitter": ["sarahbuilds", "nytfood"]
  }
}
  • Reddit: each query runs against each subreddit. Fan-out is subs × queries, capped at 10 subs.
  • X: from:<handle> gets prepended to the query for each handle. Capped at 10.
  • Threads, LinkedIn, YouTube: ignored. Passing the field is always safe.

Setting channels.reddit without listing reddit in sources still searches Reddit. The channel scope itself counts as opting in.

Scoring

Every result gets a result_score from 0 to 1, weighted across three signals:

SignalWeightHow
Relevance60%LLM topic-match between the query and the post body. Each result is scored individually.
Recency25%1.0 for posts ≤24h old, linear decay to 0 at 90 days. Unknown dates use 0.3.
Engagement15%Log-scaled likes + comments. Floors at 0.1.

Buckets:

BucketRangeMeaning
Hot≥ 0.7Direct match. First-person demand or perfect competitor mention.
Warm0.4 to 0.7Topical adjacency. Good for audience research.
Cold< 0.4Loose match. Filter client-side if you only want signal.

Retries

POST is not idempotent. Each call kicks off a new search and reserves new credits.

Safe to retry

  • 429: back off and retry after the suggested window.
  • 402: after the user tops up credits.
  • GET /v2-search-stream?id=…: pure read, retry as often as needed.

Don't retry blindly

  • POST after a 5xx. If the failed response returned a search_id, poll it. Otherwise wait 90s and check the ledger before issuing a new POST.

Failed searches refund automatically. When status flips to failed or every source returns zero results, the reservation is released with no charge recorded.

MCP server

Drop the config in, restart your agent. Three tools become available: search, get_search, billing_status.

Claude Desktop / Claude Code

Edit ~/.claude.json:

{
  "mcpServers": {
    "gorilla-mcp": {
      "command": "npx",
      "args": ["@gorilla/mcp"],
      "env": { "GORILLA_API_KEY": "grla_..." }
    }
  }
}

Cursor

Edit .cursor/mcp.json (same shape):

{
  "mcpServers": {
    "gorilla-mcp": {
      "command": "npx",
      "args": ["@gorilla/mcp"],
      "env": { "GORILLA_API_KEY": "grla_..." }
    }
  }
}

Codex / OpenAI agents

Edit ~/.codex/config.toml:

[mcp_servers.gorilla]
command = "npx"
args = ["@gorilla/mcp"]
env = { GORILLA_API_KEY = "grla_..." }

Any MCP-compatible client

GORILLA_API_KEY=grla_... npx @gorilla/mcp

The search tool POST-and-polls /v2-search-stream internally. If polling exceeds 5 minutes, it returns the search_id so the agent can recover with get_search later. Source at github.com/opusforge/gorilla-mcp.

Code samples

TypeScript

const res = await fetch("https://platform.usegorilla.app/v1/v2-search-stream", {
  method: "POST",
  headers: {
    "x-api-key": process.env.GORILLA_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: "people looking for an AI meal planner",
    since: "7d",
    limit: 50,
  }),
});

if (!res.ok) {
  const err = await res.json();
  throw new Error(`Gorilla ${res.status} (${err.code}): ${err.error}`);
}

const { results, credits_remaining } = await res.json();
console.log(`${results.length} results, ${credits_remaining} credits left`);

Streaming (TypeScript)

async function streamSearch(query: string) {
  const headers = {
    "x-api-key": process.env.GORILLA_API_KEY!,
    "Content-Type": "application/json",
  };

  const start = await fetch(
    "https://platform.usegorilla.app/v1/v2-search-stream",
    { method: "POST", headers, body: JSON.stringify({ query, since: "7d" }) },
  ).then(r => r.json());

  while (true) {
    await new Promise(r => setTimeout(r, start.suggested_interval_ms));
    const poll = await fetch(
      `https://platform.usegorilla.app/v1/v2-search-stream?id=${start.search_id}`,
      { headers },
    ).then(r => r.json());
    if (poll.status !== "running") return poll;
  }
}

Python

import os, requests

resp = requests.post(
    "https://platform.usegorilla.app/v1/v2-search-stream",
    headers={
        "x-api-key": os.environ["GORILLA_API_KEY"],
        "Content-Type": "application/json",
    },
    json={"query": "people looking for an AI meal planner", "since": "7d", "limit": 50},
)
if not resp.ok:
    err = resp.json()
    raise RuntimeError(f"Gorilla {resp.status_code} ({err['code']}): {err['error']}")

data = resp.json()
print(f"{len(data['results'])} results, {data['credits_remaining']} credits left")

Pricing

Per result, by quality. No caps. Failed searches refund.

BucketScoreCreditsUSD
Hot≥ 0.7100$0.10
Warm0.4 to 0.730$0.03
Cold< 0.43$0.003

1 credit = $0.001. A typical multi-platform query lands at $1 to $2.

Tiers

TierPriceCreditsNotes
Free$01,000 / weekRefills weekly. No card.
Monthly$14.99 / month20,000 / monthResets on renewal. No overage, top up with packs when you run out.
Top-up pack$55,000One-time. Credits never expire. Stack as many as you want.

Manage at platform.usegorilla.app/billing/.

Limits

LimitValueNotes
Rate limit60 calls / hour / keyRolling hour. 429 on exceed.
Query length500 chars400 with invalid_param if exceeded.
Channels per source10Extras silently truncated.
Per-source timeout90s504 with upstream_timeout, reservation released.
Search lifetime1 hourRow cleaned up after an hour. POST again to start a new one. Shared searches (set via PATCH) are exempt.

Include the search_id from the response in bug reports. Every search is queryable in your dashboard.