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
- Sign in at platform.usegorilla.app.
- Open the side menu, choose API Keys, click Create.
- 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
| Endpoint | What it does | Latency |
|---|---|---|
POST /v2-search-stream | Kick 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-status | Plan, 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"]
}
}
| Field | Type | Notes |
|---|---|---|
query | string | Required. Max 500 chars. Sent to each platform. |
since | string | Time window. Reddit + X apply it in the upstream query. Threads, LinkedIn, YouTube filter post-fetch. |
limit | int | 1 to 200. Default 50. Caps the returned list. Billing uses the full unsliced count. |
sources | string[] | Subset of reddit | twitter | threads | linkedin | youtube. Default fans out to all. |
channels | object | Per-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.
| Status | Code | When |
|---|---|---|
| 400 | missing_query | query empty or missing. |
| 400 | invalid_param | Malformed JSON or out-of-range value. |
| 401 | missing_auth | No x-api-key header. |
| 402 | insufficient_credits | Balance below the per-call reservation. Body includes balance and required. |
| 403 | invalid_auth | Key is well-formed but unrecognized or revoked. |
| 404 | invalid_param | Polled search_id not found or owned by another user. |
| 429 | rate_limit | Too many calls in the rolling hour window. |
| 500 | internal | Unexpected. Include the search_id in bug reports. |
| 502 | upstream_error | Upstream provider returned an error. |
| 504 | upstream_timeout | A 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:
| Signal | Weight | How |
|---|---|---|
| Relevance | 60% | LLM topic-match between the query and the post body. Each result is scored individually. |
| Recency | 25% | 1.0 for posts ≤24h old, linear decay to 0 at 90 days. Unknown dates use 0.3. |
| Engagement | 15% | Log-scaled likes + comments. Floors at 0.1. |
Buckets:
| Bucket | Range | Meaning |
|---|---|---|
| Hot | ≥ 0.7 | Direct match. First-person demand or perfect competitor mention. |
| Warm | 0.4 to 0.7 | Topical adjacency. Good for audience research. |
| Cold | < 0.4 | Loose 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.
| Bucket | Score | Credits | USD |
|---|---|---|---|
| Hot | ≥ 0.7 | 100 | $0.10 |
| Warm | 0.4 to 0.7 | 30 | $0.03 |
| Cold | < 0.4 | 3 | $0.003 |
1 credit = $0.001. A typical multi-platform query lands at $1 to $2.
Tiers
| Tier | Price | Credits | Notes |
|---|---|---|---|
| Free | $0 | 1,000 / week | Refills weekly. No card. |
| Monthly | $14.99 / month | 20,000 / month | Resets on renewal. No overage, top up with packs when you run out. |
| Top-up pack | $5 | 5,000 | One-time. Credits never expire. Stack as many as you want. |
Manage at platform.usegorilla.app/billing/.
Limits
| Limit | Value | Notes |
|---|---|---|
| Rate limit | 60 calls / hour / key | Rolling hour. 429 on exceed. |
| Query length | 500 chars | 400 with invalid_param if exceeded. |
| Channels per source | 10 | Extras silently truncated. |
| Per-source timeout | 90s | 504 with upstream_timeout, reservation released. |
| Search lifetime | 1 hour | Row 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.