API v1

TrendWatch API

Read your TrendWatch workspace programmatically: connected Instagram and TikTok accounts, collected videos with their metrics, transcripts and AI script formulas, and start AI analysis from your own scripts.

Use the REST API to work with the same data available in the TrendWatch application. Requests authenticate with an API key.

API and MCP access is included in every paid plan. On the free plan keys cannot be created, and existing keys answer 403 PLAN_REQUIRED.

Base URL
https://server.trndwtch.com/api/v1

Beta limitations

Except for starting AI analysis, the published API v1 endpoints are read-only. There are currently no API endpoints for:

  • Adding, editing, pausing or removing tracked Instagram and TikTok accounts.
  • Submitting a new video by URL.
  • Changing a video's production status or its position on the Kanban board.
Need one of these operations or another API capability? Email hello@trndwtch.com and tell us about your use case.

Quickstart

Create a key on your Account page, then check that it works. /me costs nothing and returns your plan and credit balance.

export TWA_KEY="twa_..."

curl -s "https://server.trndwtch.com/api/v1/me" \
  -H "Authorization: Bearer $TWA_KEY"

Then list your ten most recently collected videos:

curl -s "https://server.trndwtch.com/api/v1/videos?sort_by=created_at&order=desc&limit=10" \
  -H "Authorization: Bearer $TWA_KEY"

The same call in JavaScript and Python:

const res = await fetch(
  "https://server.trndwtch.com/api/v1/videos?sort_by=created_at&order=desc&limit=10",
  { headers: { Authorization: `Bearer ${process.env.TWA_KEY}` } },
);
if (!res.ok) throw new Error((await res.json()).message);
const { data, total_count, has_more } = await res.json();
import os, requests

r = requests.get(
    "https://server.trndwtch.com/api/v1/videos",
    params={"sort_by": "created_at", "order": "desc", "limit": 10},
    headers={"Authorization": f"Bearer {os.environ['TWA_KEY']}"},
    timeout=30,
)
r.raise_for_status()
payload = r.json()

Starter prompt for Claude Code or Codex

After saving your API key in the TWA_KEY environment variable, paste this prompt into Claude Code or Codex:

Use the TrendWatch REST API at https://server.trndwtch.com/api/v1.

Read the API key from the TWA_KEY environment variable. Never print or expose the key.
1. Call GET /me with the Authorization: Bearer $TWA_KEY header to verify authentication.
2. Call GET /videos?sort_by=created_at&order=desc&limit=5 with the same header.
3. Show the five most recently collected videos in a compact table with account name, caption, platform_url, views, likes, comments, and posted_at.
If a request fails, show the HTTP status and API error message without exposing the key.

Authentication

Every request carries a bearer token: an API key created on the Account page. Keys start with twa_ and are shown exactly once at creation; only a SHA-256 hash is stored, so a lost key cannot be recovered, only replaced.

Authorization: Bearer twa_<your key>

You can hold up to five active keys and revoke any of them instantly. A key acts strictly as its owner: it reads that account's data and spends that account's credits, and it can never reach another user's workspace.

The key's owner must be on an active paid plan. The check runs on every request, so a key created while subscribed stops working — with 403 and data.code: PLAN_REQUIRED — if the subscription lapses, and starts working again after an upgrade. No key is revoked in the process.

Conventions

  • Every successful response wraps its payload in data.
  • Listing endpoints add page, limit, total_count and has_more next to data. /accounts is unpaginated and returns everything, so its total_count equals the array length.
  • Timestamps are ISO 8601 in UTC. Date filters take YYYY-MM-DD.
  • Fields the platform never reported come back as null rather than being omitted, except the long text fields on the video detail endpoint, which are absent unless requested with include.
  • Videos come from two sources that share one id space: accounts (collected from a connected account) and submitted (added by URL). Detail and analysis endpoints accept an id from either and report which one it was in source.

Metrics

Two of the numbers on every collected video compare it against its own account's norm rather than against the platform. That is what makes a 5k-follower account and a 5M-follower account directly comparable.

Name Type Description
outlier_score number Reach against the account's own norm. 1 = a typical video for that account, 3 = about three times its usual reach.
er_outlier_score number, percent Engagement against the account's own norm. 0 = as usual, +100 = about twice the usual engagement, -50 = about half of it.
er number, percent Engagement rate: likes and comments relative to views. An absolute figure, unlike er_outlier_score.
engagement low | average | best Absolute engagement band.
virality low | average | good | best | virus Coarse reach band against the account's norm, assigned when the video is collected. no_data until the account has a norm.
engagement is absolute while er_outlier_score is relative, so engagement: "best" next to a negative er_outlier_score is not a contradiction: the video is strong for the platform yet below what that particular account usually gets.

The breakout videos of every account you track, best first:

curl -s "https://server.trndwtch.com/api/v1/videos?min_outlier_score=3&sort_by=outlier_score&order=desc" \
  -H "Authorization: Bearer $TWA_KEY"

All scores are null until the account has enough collected videos to have a norm.

Errors

Errors use standard HTTP status codes. The enumerated API failures below carry a stable data.code; match on that code because the human-readable message may be reworded. Validation failures return data.errors instead.

{
  "message": "This API key has been revoked.",
  "data": { "code": "REVOKED_API_KEY" }
}

The same envelope carries every other failure. Running out of credits while starting an analysis, for example, answers 402 with:

{
  "message": "No credits left for this billing period.",
  "data": { "code": "INSUFFICIENT_CREDITS" }
}

Status codes

Name Type Description
400 Bad Request A query parameter failed validation; data.errors lists the offending fields.
401 Unauthorized MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY or REVOKED_API_KEY.
402 Payment Required INSUFFICIENT_CREDITS — no credits left this billing period.
403 Forbidden PLAN_REQUIRED — the key's owner is not on an active paid plan.
404 Not Found VIDEO_NOT_FOUND — no such video in your workspace.
429 Too Many Requests RATE_LIMITED — see rate limits below.
500 Server Error Unexpected failure; retry, then contact support.
502 Bad Gateway UPSTREAM_QUERY_FAILED — the video store was unreachable. Safe to retry.

Rate limits

200 requests per minute per API key, plus a 1000 per minute ceiling per IP address applied before the key is looked up. Both windows are one minute and report their state in the standard RateLimit response headers. Exceeding either returns 429.

Production runs several server instances and each enforces its own counters, so treat these numbers as approximate floors rather than an exact contract. Back off on 429 and retry.
GET/api/v1/me

Your plan and credit balance. The cheapest way to verify a key works.

{
  "data": {
    "email": "you@example.com",
    "username": "you",
    "plan": "pro",
    "credits": {
      "available": 812,
      "used_this_period": 188,
      "plan_limit": 1000,
      "package_credits": 0,
      "total_monthly_limit": 1000,
      "period_started_at": "2026-07-18T00:00:00.000Z",
      "resets_at": "2026-08-18T00:00:00.000Z"
    }
  }
}
Credits are a monthly allowance, not a running balance: available = plan_limit + package_credits − used_this_period, floored at zero, and a new period resets usage by itself.
GET/api/v1/accounts

Every social account connected to your workspace, with follower counts, collection status and aggregate engagement. Takes no parameters.

{
  "data": [
    {
      "id": "a1b2c3d4",
      "name": "Nike",
      "platform": "instagram",
      "username": "nike",
      "url": "https://instagram.com/nike",
      "followers": 302000000,
      "collection_status": "done",
      "is_active": true,
      "video_count": 143,
      "posting_frequency_per_week": 4.5,
      "average_er": 2.1,
      "created_at": "2026-05-02T10:14:00.000Z"
    }
  ],
  "total_count": 1
}

collection_status is the scrape lifecycle (new, pending, processing, done, error). is_active is false while an account is paused and collecting nothing. Use id as account_id when listing videos.

GET/api/v1/videos

Paginated video search across your workspace.

Query parameters

Name Type Description
source accounts | submitted Which set to read. Default accounts. submitted supports pagination only in v1; search, sort and date filters do not apply to it.
page integer 1-based page number. Default 1.
limit integer Page size, 1–100. Default 20.
sort_by string created_at (default), posted_at, views, likes, comments, shares, duration, followers, er, outlier_score, er_outlier_score. Combine the last two with order=desc to put the strongest videos first.
order asc | desc Default desc.
search string Text search over captions, scripts, topics and account names. Max 200 characters.
account_id string Restrict to one connected account.
date_from YYYY-MM-DD Publication date on or after this day.
date_to YYYY-MM-DD Publication date on or before this day.
min_outlier_score number Only videos whose views are at least this many times the account's own norm. min_outlier_score=3 is the standard way to ask for "just the breakout videos". source=accounts only.
max_outlier_score number Upper bound on the same scale — e.g. max_outlier_score=1 for underperformers. source=accounts only.
min_er_outlier_score number Only videos whose engagement beat the account's own norm by at least this many percent. min_er_outlier_score=50 = at least 50% above usual. source=accounts only.
max_er_outlier_score number Upper bound on the same scale. source=accounts only.
engagement string Comma-separated absolute engagement buckets: low, average, best. source=accounts only.
virality string Comma-separated virality buckets: low, average, good, best, virus. A coarse pre-computed label — prefer min_outlier_score when the threshold has to be exact. source=accounts only.
date_from and date_to filter by publication date. For what TrendWatch collected this week, sort by created_at instead; collection date is sortable, not filterable.
{
  "data": [
    {
      "id": "v_9f8e7d",
      "source": "account",
      "account": {
        "id": "a1b2c3d4",
        "name": "Nike",
        "platform": "instagram",
        "username": "nike",
        "followers": 302000000
      },
      "platform_url": "https://instagram.com/reel/XYZ",
      "caption": "Just do it.",
      "topic": "motivation",
      "summary": "Athlete montage with a voiceover hook.",
      "metrics": {
        "views": 1840000,
        "likes": 96000,
        "comments": 1200,
        "shares": 4300,
        "duration_sec": 28,
        "outlier_score": 4.2,
        "er_outlier_score": 38.5,
        "er": 5.28,
        "engagement": "best",
        "virality": "good"
      },
      "posted_at": "2026-07-30T09:12:00.000Z",
      "created_at": "2026-07-30T11:40:00.000Z",
      "analysis_status": "done",
      "has_transcript": true,
      "has_formula": true,
      "has_script": false
    }
  ],
  "page": 1,
  "limit": 20,
  "total_count": 143,
  "has_more": true
}

Videos with source: "submitted" carry url, platform, owner_username, owner_followers and collection_status instead of the account object, and a smaller metrics set.

GET/api/v1/videos/{id}

One video in full, from either source.

Query parameters

Name Type Description
id path, string Video id from the listing endpoint.
include string Comma-separated long fields to fetch: transcript, formula, script. Unknown values are ignored.

Long text fields are omitted from the payload entirely unless requested, so a listing stays small. The has_transcript, has_formula and has_script booleans are always present, letting you see what exists before fetching it.

curl -s "https://server.trndwtch.com/api/v1/videos/v_9f8e7d?include=transcript,formula" \
  -H "Authorization: Bearer $TWA_KEY"
POST/api/v1/videos/{id}/analysis

Start AI analysis: transcription plus script-formula extraction. Spends one credit.

Analysis is asynchronous and takes a few minutes. A fresh start answers 202 with already_running: false and charges one credit. If a run is already in flight the call answers 200 with already_running: true and charges nothing, so retrying is safe. Re-analysing a video that already finished starts a new run and charges again.

{ "data": { "source": "account", "status": "to_transcribe", "already_running": false } }
GET/api/v1/videos/{id}/analysis

Poll for the result. Free to call. Terminal states are done (the formula is ready) and error (the error field says why); anything else means the pipeline is still working.

{
  "data": {
    "source": "account",
    "status": "done",
    "formula": {
      "caption": "Three habits that changed my mornings",
      "formula": "Question hook → quick proof → three-step payoff",
      "text_hook": "Still checking your phone first thing?",
      "audio_hook": "Direct question over a quiet intro",
      "visual_hook": "Phone placed face-down beside an alarm clock"
    },
    "error": null
  }
}
# start, then poll until the status is terminal
curl -s -X POST "https://server.trndwtch.com/api/v1/videos/$VIDEO_ID/analysis" \
  -H "Authorization: Bearer $TWA_KEY"

until curl -s "https://server.trndwtch.com/api/v1/videos/$VIDEO_ID/analysis" \
  -H "Authorization: Bearer $TWA_KEY" | grep -qE '"status":"(done|error)"'; do
  sleep 30
done
A run stuck for more than 15 minutes is treated as dead and can be restarted; that restart charges a credit again.

OpenAPI spec

The full contract is published as OpenAPI 3.1 at https://server.trndwtch.com/api/v1/openapi.json. It needs no authentication, so you can import it into Postman, Insomnia or Bruno, or generate a typed client before you have a key.

npx @openapitools/openapi-generator-cli generate \
  -i https://server.trndwtch.com/api/v1/openapi.json \
  -g typescript-fetch -o ./trendwatch-client

Security

  • An API key grants read access to your whole workspace and the ability to spend your credits on analysis. Treat it like a password: keep it in an environment variable or a secret manager, never in client-side code or a public repository.
  • Revoke a key the moment you suspect it leaked; revocation takes effect immediately. Each key shows a last-used timestamp on the Account page, which is the only leak signal available.
  • Captions, transcripts and scripts returned by the API are third-party or user-generated content. If you pass them to an LLM, treat them as data, never as instructions.
  • No endpoint deletes anything, and no endpoint can reach another user's data; every query is scoped to the key's owner in the database.

MCP server

The same data is available to AI agents over MCP at POST https://server.trndwtch.com/mcp (streamable HTTP, stateless), using the same bearer token. Five tools: trendwatch_list_accounts, trendwatch_search_videos, trendwatch_get_video, trendwatch_run_ai_analysis (spends one credit, flagged destructive) and trendwatch_get_usage.

claude mcp add --transport http trendwatch https://server.trndwtch.com/mcp \
  --header "Authorization: Bearer twa_..."

ChatGPT developer mode: Settings → Connectors → Advanced → Developer mode, add https://server.trndwtch.com/mcp with auth type API key and the twa_… value.

Claude.ai web and desktop connectors are not supported yet — their connector UI only accepts OAuth, which v1 does not implement. Use Claude Code, the Claude API MCP connector, or ChatGPT developer mode instead.