Skip to content

Documentation · API v1

Everything you can call.

REST over HTTPS, JSON in and out. One key and one data shape for eight platforms. Start with the quickstart, then pick an endpoint; every card shows its cost, parameters and a real response.

Quickstart

Sign up and you get an API key with 100 free credits instantly. Set two environment variables, then make your first call. SW is the API base URL, https://api.vanefeed.com.

export SW="https://api.vanefeed.com"
export SW_KEY="sk_live_your_key"

curl "$SW/v1/youtube/profile?handle=@TED" \
  -H "Authorization: Bearer $SW_KEY"

# swap the platform and endpoint for anything else:
# /v1/x/search?q=(artemis OR moon) launch&limit=100

Authentication

Send your key on every request in either header:

Authorization: Bearer sk_live_…
# or
X-API-Key: sk_live_…
  • Your first key arrives when you sign up. Create up to 5 keys in the dashboard; they share one credit balance.
  • To see a key again, open the dashboard's API keys and press Show (your password is required). Store keys in a secret manager.
  • If a key may have leaked, press Roll: you get a new key and the old one stops working at once.
  • Each key has scopes. A call without the needed scope returns 403 forbidden.
ScopeGrants
mentions:readOn-demand single posts, comments and search; mention search, single mention, trends
accounts:readOn-demand profiles, posts, followers and following; account lookup and posts
webhooks:writeCreate webhooks
stream:readLive SSE stream

Credits & rate limits

Each endpoint's cost is shown on its card below. List endpoints (posts, comments, search, followers, following) cost per 20 results returned, up to 1,000 per call. Set how many you want with limit: posts, comments and search return 100 by default; followers and following return 20. Some platforms have a lower ceiling per call (Instagram posts 33, Instagram comments 50, Instagram search 100, news 100, Facebook comments 200, Reddit search 250); a larger limit is trimmed to it. Example: limit=100 costs at most 5 blocks of 20. Platforms hand out results about 20 at a time, so 100 take several seconds. For the fastest first results, ask for limit=20, show them, then fetch the rest with next_cursor: we start loading the next page while you read, and the total price is the same. Before a call runs we hold the most it could cost; when it finishes you pay only for what came back, and failed or cached calls cost nothing. Parallel calls can never spend the same credits twice.

If your balance can't cover a call, you get 402 insufficient_credits with affordable_limit: the largest limit your balance covers, so your code can retry smaller. Two token-bucket rate limits apply: one per key, then one per plan.

HeaderMeaningStatus
X-RateLimit-LimitRequests per minute for this keyLive
X-RateLimit-RemainingRequests left in the current minuteLive
Retry-AfterSeconds to wait, on every 429Live
X-Credits-CostCredits this call used. Always 0 for failed or rejected calls.Live
X-Credits-RemainingCredits left on this key after the callLive
X-Credits-Lowtrue when your prepaid balance is under 100 credits, so your code can warn you before calls start failing with 402Live
X-CacheHIT when an on-demand call was served from cache (free), MISS otherwiseLive
X-Request-IdUnique id for the call. Send your own and we echo it back. Quote it to support.Live
PlanRequests / min
Free / trial30
Starter120
Growth300
Custom600–1,200+

Free price checks

Add dry_run=true to any endpoint to see what it would cost without calling it. Price checks are free and don't touch your balance. Use them before large batch jobs.

curl "$SW/v1/youtube/posts?handle=@TED&limit=500&dry_run=true" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": {
    "dry_run": true,
    "endpoint": "GET /mentions/search",
    "credits": 5,
    "quota_units": 100000,
    "used_units": 1234,
    "remaining_units": 98766,
    "would_exceed_quota": false
  }
}

Errors

Every error uses the same shape. Read error in code and show message to people.

{ "error": "not_found", "message": "mention 12345 not found" }
HTTPerrorMeaning
400bad_requestA parameter is wrong or the JSON body is malformed. The message names the problem.
401unauthorizedMissing, invalid, revoked or expired key.
403forbiddenThe key doesn't have the scope this endpoint needs.
402insufficient_creditsYour balance can't cover the most this call could cost. Free. The body has balance, cost and affordable_limit.
404not_foundThat account, post or channel doesn't exist or isn't public.
422(detail)The request body failed validation. See the detail array.
429rate_limit_exceededToo many requests. Wait for the number of seconds in Retry-After.
429quota_exceededThe key's quota is spent. The body includes quota, used and retry_after.
500internal_errorSomething broke on our side. No stack traces, ever.
502upstream_errorThe platform sent something we couldn't read. Free; retry, and tell us if it persists.
503platform_unavailableThe platform is limiting or blocking us right now. Free; the message says when to retry.
504upstream_timeoutThe platform took longer than 25 seconds. Free; retry shortly.

Sentiment

Add sentiment=true to any posts, post, comments, replies or search call (also TikTok's hashtag, top_search, trending, playlist and song_videos) and every result gets a sentiment object: label (positive, neutral or negative), score (how sure, 0 to 1) and model.

It costs 1 extra credit per 20 results (1 for a single post). It reads the whole post, not keywords, with a multilingual model trained on social media (Cardiff NLP's XLM-T): strongest in English, Arabic, Hindi, Spanish, French, German, Italian and Portuguese, and it also handles Urdu and Roman Urdu. Very short or slang-heavy posts are the hardest; score tells you how sure it is.

curl "$SW/v1/tiktok/comments?id=https://www.tiktok.com/@nasa/video/123&sentiment=true" \
  -H "Authorization: Bearer $SW_KEY"

Pagination

List endpoints return data, next_cursor and has_more. Pass next_cursor back as cursor until has_more is false. Cursors are opaque, so don't build them yourself.

cursor = None
while True:
    params = {"q": "your-brand", "limit": 200}
    if cursor:
        params["cursor"] = cursor
    page = requests.get(f"{SW}/v1/mentions/search", params=params,
                        headers={"Authorization": f"Bearer {KEY}"}).json()
    handle(page["data"])
    if not page["has_more"]:
        break
    cursor = page["next_cursor"]
On-demand
Live on nine platforms. These endpoints fetch fresh data at call time and return it in one shape for every platform: swap youtube in the path for x, bluesky, reddit, tiktok, telegram, instagram, facebook or web. Repeat requests within a few minutes come from cache and cost 0 credits (X-Cache: HIT).

Platform support

Which resources each platform serves, and the parameters it takes. Unknown parameters return a 400 that lists the allowed ones.

PlatformprofilepostspostcommentssearchParameters
youtube●●●●●handle, id, q, tab, yt_params, hl, gl, limit, cursor
reddit●●●●●handle (subreddit), id (thread URL), q, subreddit, limit. Scores are null when Reddit only serves its RSS feed.
bluesky●●●●●handle (name.bsky.social, @handle, DID or bsky.app link), type (posts, replies, media, threads), id (bsky.app post link or at:// URI), q with sort (latest, top), author, lang, since, until, tag, domain; limit, cursor. Also /followers and /following.
telegram●●●—●handle (public channel), id (t.me/channel/123), q + handle (search inside a channel), limit, cursor
web—●●—●url (any news site, section or RSS/Atom feed), id (article URL: full text, author, date), q (news search), hl, gl, limit
x●●●●●handle, id (post id or URL), q, product (Latest, Top, Media), limit, cursor. Also /v1/x/followers, /following, /community (id = community id or link: name, members, rules, topic, creator; 1 credit), /community_posts (id, sort latest|top, limit, cursor; 2 credits per 20) and /transcript (id = a video post: its words as text plus timed lines. Captions when the video has them, 1 credit; otherwise its speech is transcribed, 1 credit + 1 per started minute. max_minutes (default 10, up to 60) caps the length and so the price; source = captions or speech forces one; lang e.g. en speeds it up)
tiktok●●●●●handle, id (tiktok.com/@user/video/… URL), q (video search), limit (profile videos and search up to 300), cursor. Profile videos and search take about 5–15 s. Video counts are null when TikTok only allows the embed view. Also /v1/tiktok/replies (id = the video URL, comment_id from /comments; 2 credits per 20), /followers and /following (handle, limit up to 1000; 2 per 20), /transcript (id = video URL: TikTok's captions, 1 credit, or its speech transcribed, 1 + 1 per started minute; lang, source, max_minutes), /live (handle: live now or not, viewers, likes, start time; 1 credit), /region (handle: the country its recent videos were posted from; 1 credit), /search_users and /top_search (q; Top search returns videos in data plus users; 3 per 20), /suggestions (q: what the search box suggests; 1 credit), /hashtag (q = one hashtag: its videos, and on the first page its views and video count; 3 per 20), /song (id = song id or tiktok.com/music/… link: title, artist, length, videos using it; 1 credit), /song_videos (id; 2 per 20), /playlist (id = a tiktok.com/@user/playlist/… link; 2 per 20) and /trending (no input: TikTok's Explore feed; 2 per 20). Lists go up to 300 except followers and following.
instagram●●●●●handle, id (instagram.com/p/… or /reel/… URL), q (one hashtag), limit
facebook●●●●—handle (public page), id (post id or URL), limit. Posts: the newest posts Facebook includes in the page.
# Telegram channel profile
curl "$SW/v1/telegram/profile?handle=durov" -H "Authorization: Bearer $SW_KEY"

# Newest posts in a subreddit
curl "$SW/v1/reddit/posts?handle=r/technology&limit=25" -H "Authorization: Bearer $SW_KEY"

# News search in German
curl "$SW/v1/web/search?q=elektroauto&hl=de-DE&gl=DE" -H "Authorization: Bearer $SW_KEY"

Fetch a profile

GET/v1/{platform}/profileLive
1 creditscope: accounts:read

Look up a channel live: followers, exact video count, total views, join date, country, banner, full bio and external links. A count the platform doesn't show is null, never 0.

ParameterTypeDefaultDescription
platformREQUIREDpath—youtube
handleREQUIREDstring—@handle, channel id (UC…) or channel URL.
hl / glstringen / USLanguage and region YouTube uses for the response.
Request
curl "$SW/v1/youtube/profile?handle=%40TED" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": {
    "id": "UCBJycsmduvYEL83R_U4JriQ",
    "platform": "youtube",
    "handle": "@mkbhd",
    "name": "Marques Brownlee",
    "url": "https://www.youtube.com/@mkbhd",
    "bio": "MKBHD: Quality Tech Videos | YouTuber | Geek | Consumer Electronics | Tech Head | Internet…",
    "avatar_url": "https://yt3.ggpht.com/qu4TmIaYUlS41-dJ9gZ7DUR3nilvmB5_11i6OKSdvNnBNiyOusZP1bMN6ICnuxtjFBb6ioKgRQ=s176-c-k-c0x00ffffff-no-rj-mo",
    "verified": true,
    "followers": 21300000,
    "following": null,
    "posts_count": 1856,
    "fetched_at": "2026-10-03T08:56:53+00:00",
    "banner_url": "https://yt3.googleusercontent.com/zi_48HwHU4s0Hnb6tZuvRzTfCPHrLEnrtIMDLzMauDEyDmURh9JZ4wrNd3lKr7m9uVnT4YjYKw=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
    "country": "United States",
    "joined_at": "2008-03-21",
    "total_views": 5730204755,
    "links": [
      {
        "title": "Twitter",
        "url": "https://twitter.com/MKBHD"
      },
      {
        "title": "Instagram",
        "url": "https://instagram.com/MKBHD"
      },
      {
        "title": "The Studio",
        "url": "https://youtube.com/c/TheStudio"
      },
      {
        "title": "Reddit",
        "url": "https://reddit.com/r/MKBHD"
      },
      {
        "title": "Discord",
        "url": "https://discord.gg/MKBHD"
      }
    ]
  }
}

List recent posts

GET/v1/{platform}/postsLive
2 credits · per 20 results returnedscope: accounts:read

A channel's latest videos or Shorts, newest first. Pages never skip or repeat items, whatever limit you use.

ParameterTypeDefaultDescription
handleREQUIREDstring—@handle, channel id or URL.
tabvideos | shortsvideos
limitinteger1001–1,000. Charged per 20 results that come back.
cursorstring—next_cursor from the previous page.
hl / glstringen / US
Request
curl "$SW/v1/youtube/posts?handle=%40TED&limit=10" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": [ {
    "id": "yt_r63spwe1LCI",
    "platform": "youtube",
    "platform_id": "r63spwe1LCI",
    "url": "https://www.youtube.com/watch?v=r63spwe1LCI",
    "author": { "handle": "@TED", "name": "TED", "id": "UCAuUUnT6oDeKwE6v1NGQxug", "url": "https://www.youtube.com/channel/UCAuUUnT6oDeKwE6v1NGQxug", "followers": null },
    "title": "The Gravity-Defying Future of Manufacturing | Schendy Kernizan | TED",
    "text": "The Gravity-Defying Future of Manufacturing | Schendy Kernizan | TED",
    "hashtags": [],
    "published_at": "2026-10-02T15:26:55Z",
    "published_at_precision": "hour",
    "metrics": { "likes": null, "comments": null, "shares": null, "views": 50000 },
    "metrics_available": ["views"]
  } ],
  "next_cursor": "eyJwIjoiNHFtRnNnTF…",
  "has_more": true
}

Fetch one post

GET/v1/{platform}/postLive
1 creditscope: mentions:read

One video by id or any YouTube URL (watch, youtu.be, Shorts, live): views, likes, comment count, exact publish time, category, tags, duration and visibility flags.

ParameterTypeDefaultDescription
idREQUIREDstring—Video id or URL.
hl / glstringen / US
Request
curl "$SW/v1/youtube/post?id=https%3A%2F%2Fyoutu.be%2FjNQXAC9IVRw" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": {
    "id": "yt_dQw4w9WgXcQ",
    "platform": "youtube",
    "platform_id": "dQw4w9WgXcQ",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "author": {
      "handle": "@RickAstleyYT",
      "name": "Rick Astley",
      "url": "https://www.youtube.com/@RickAstleyYT",
      "id": "UCuAXFkgsw1L7xaCfnd5JJOw",
      "followers": null
    },
    "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
    "text": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)\nThe officia…",
    "description": "The official video for “Never Gonna Give You Up” by Rick Astley. \n\nNever: The Au…",
    "lang": null,
    "media": [
      {
        "type": "thumbnail",
        "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDd2KtelLHaNSXrI9_5K-NvTscKNw"
      }
    ],
    "hashtags": [
      "rickastleynever",
      "rickastley",
      "nevergonnagiveyouup",
      "wheneveryouneedsomebody",
      "officialmusicvideo"
    ],
    "tags": [
      "rick astley",
      "Never Gonna Give You Up",
      "nggyu",
      "never gonna give you up lyrics",
      "rick rolled"
    ],
    "category": "Music",
    "published_at": "2009-10-25T06:57:33+00:00",
    "published_at_precision": "second",
    "uploaded_at": "2009-10-25T06:57:33+00:00",
    "duration_seconds": 213,
    "is_live": false,
    "is_shorts_eligible": false,
    "is_family_safe": true,
    "is_unlisted": false,
    "is_private": false,
    "ratings_enabled": true,
    "available_countries": 249,
    "embed_url": "https://www.youtube.com/embed/dQw4w9WgXcQ",
    "metrics": {
      "likes": 19456368,
      "comments": 2400000,
      "shares": null,
      "views": 1822679748
    },
    "metrics_available": [
      "comments",
      "likes",
      "views"
    ]
  }
}

List comments

GET/v1/{platform}/commentsLive
2 credits · per 20 results returnedscope: mentions:read

Top-level comments on a video, with author, likes and reply counts.

ParameterTypeDefaultDescription
idREQUIREDstring—Video id or URL.
limitinteger1001–1,000. Charged per 20 results that come back.
cursorstring—
Request
curl "$SW/v1/youtube/comments?id=jNQXAC9IVRw&limit=20" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": [ {
    "id": "ytc_UgzuC3zzpRZkjc5Qzsd4AaABAg",
    "platform": "youtube",
    "post_id": "yt_jNQXAC9IVRw",
    "parent_id": null,
    "author": { "handle": "@SanDiegoZoo", "name": "SanDiegoZoo", "verified": true, "id": "UCC5NfQ6Mf0dq_eEwv4P_hWA" },
    "text": "We're so honored that the first ever YouTube video was filmed here!",
    "published_at": "2020-10-04T08:26:51Z",
    "published_at_precision": "year",
    "metrics": { "likes": 4800000, "replies": 985 }
  } ],
  "next_cursor": "eyJwIjoiRWc0U…",
  "has_more": true
}

News pictures (free)

POST/v1/web/thumbsLive
0 credits · freescope: any key

News search (/v1/web/search) pages through every article Bing News and Google News have for your keywords: hundreds per query, newest first, with total and next_cursor. A search waits a few seconds for article pictures, and the rest keep loading on our side. Send the result links here to get each one's picture and the publisher's own link once ready. Only links from recent searches are looked up.

ParameterTypeDefaultDescription
urlsREQUIREDstring[], body—Up to 50 result links, as returned by /v1/web/search.
Request
curl -X POST "$SW/v1/web/thumbs" \
  -H "Authorization: Bearer $SW_KEY" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://news.google.com/rss/articles/CBMi…"]}'
Response
{
  "data": {
    "https://news.google.com/rss/articles/CBMi…": {
      "url": "https://www.example-news.com/2026/10/05/story",
      "image": "https://www.example-news.com/images/story.jpg",
      "pending": false
    }
  }
}
Collected data

Monitoring: search the posts we collect continuously for your workspace. Email us the brands, keywords and accounts to track, and we keep them up to date. Changes take one email.

Get one mention

GET/v1/mentions/{mention_id}Live
1 creditscope: mentions:read

Fetch a single mention by its id. Ids belonging to another workspace return 404.

ParameterTypeDefaultDescription
mention_idREQUIREDinteger, path—The mention id from a search result.
Request
curl "$SW/v1/mentions/42" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": {
    "id": 42,
    "platform": "youtube",
    "platform_id": "r63spwe1LCI",
    "author_handle": "@TED",
    "author_name": "TED",
    "author_followers": 27900000,
    "author_url": "https://www.youtube.com/@TED",
    "text": "The Gravity-Defying Future of Manufacturing | Schendy Kernizan | TED",
    "lang": "en",
    "url": "https://www.youtube.com/watch?v=r63spwe1LCI",
    "permalink": "https://www.youtube.com/watch?v=r63spwe1LCI",
    "published_at": "2026-10-02T15:26:55+00:00",
    "fetched_at": 1791007101,
    "likes": null, "shares": null, "comments_count": null, "views": 50000,
    "hashtags": [],
    "sentiment_label": "neutral",
    "sentiment_score": 0.0,
    "sentiment_model": "lexicon-fallback",
    "sentiment_why": "lexicon: no opinion words",
    "intent_score": 0,
    "intent_why": "no purchase language",
    "tags": [],
    "triage_state": "new"
  }
}

Look up an account

GET/v1/accounts/lookupLive
2 creditsscope: accounts:read

Profile statistics built from collected mentions: mention count, total likes and shares, last-seen follower count, first and last seen, and the 3 most recent mentions. This is derived from collected data, not a live profile fetch.

ParameterTypeDefaultDescription
platformREQUIREDstring—Platform name.
handleREQUIREDstring—Account handle. The @ is optional.
Request
curl "$SW/v1/accounts/lookup?platform=youtube&handle=%40TED" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": {
    "platform": "youtube",
    "handle": "@TED",
    "mention_count": 128,
    "total_likes": 1204331,
    "total_shares": 0,
    "followers": 27900000,
    "first_seen": 1790944180,
    "last_seen": 1791007101,
    "recent": [ { "id": 42, "text": "The Gravity-Defying Future of Manufacturing | Schendy Kernizan | TED" } ]
  }
}

List an account's posts

GET/v1/accounts/{platform}/{handle}/postsLive
2 creditsscope: accounts:read

Collected posts by one account, newest first. Paginates like search. URL-encode @ as %40 or drop it.

ParameterTypeDefaultDescription
platformREQUIREDstring, path—
handleREQUIREDstring, path—
limitinteger501–200, clamped.
cursorstring—
Request
curl "$SW/v1/accounts/youtube/%40TED/posts?limit=20" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": [ { "id": 42, "platform": "youtube", "text": "…" } ],
  "next_cursor": null,
  "has_more": false
}

Create a webhook

POST/v1/webhooksLive
3 creditsscope: webhooks:write

Get mentions and alerts pushed to your server. The URL must resolve to a public address; loopback, private and cloud-metadata addresses are rejected.

ParameterTypeDefaultDescription
urlREQUIREDstring, body—http(s) URL that receives POST deliveries.
eventsREQUIREDstring[], body—For example mention.new, alert.fired.
secretstring, bodygeneratedSigns every delivery. If omitted we generate one and return it once.
Request
curl -X POST "$SW/v1/webhooks" \
  -H "Authorization: Bearer $SW_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.your-app.com/signalforge","events":["mention.new","alert.fired"]}'
Response
{
  "data": {
    "id": 7,
    "url": "https://hooks.your-app.com/signalforge",
    "events": ["mention.new", "alert.fired"],
    "active": true,
    "secret": "k3Y…",
    "warning": "store this secret now; it signs webhook payloads"
  }
}

Live stream (SSE)

GET/v1/streamLive
60 credits · per sessionscope: stream:read

Server-sent events. You get event: connected, then one data: line per new mention, then event: done when the session ends. Use webhooks for 24/7 delivery and the stream for interactive sessions.

ParameterTypeDefaultDescription
sinceepoch or ISO-86011 hour agoStart of the window.
durationinteger, seconds301–300.
Request
curl -N "$SW/v1/stream?duration=120" \
  -H "Authorization: Bearer $SW_KEY"
Response
event: connected
data: {"ok": true}

data: {"id": 43, "platform": "reddit", "text": "…"}

event: done
data: {}

Check your usage

GET/v1/usageLive
0 creditsscope: none

Your quota and spend for the current calendar month (UTC), broken down by endpoint.

No parameters.

Request
curl "$SW/v1/usage" \
  -H "Authorization: Bearer $SW_KEY"
Response
{
  "data": {
    "period": "current calendar month (UTC)",
    "quota_units": 100000,
    "used_units": 1234,
    "remaining_units": 98766,
    "calls": 410,
    "by_endpoint": [ { "endpoint": "GET /mentions/search", "units": 1200, "calls": 240 } ]
  }
}

Mention object

Every collected post, comment, video or article is returned as a Mention, whatever platform it came from. Counts a platform doesn't expose are null, never 0.

FieldTypeDescription
idintegerVanefeed id
platformstringSource platform
platform_idstringThe platform's own id
author_handle / author_namestringWho posted it
author_followersinteger | nullFollower count as last seen
author_urlstringAuthor profile link
textstringPost text, title + description, or comment body
langstring | nullDetected language code
url / permalinkstringLink to the original
published_atISO-8601 | nullWhen it was posted. YouTube times are approximate.
fetched_atepoch secondsWhen we collected it
likes, shares, comments_count, viewsinteger | nullEngagement counts
hashtagsstring[]Hashtags in the text
sentiment_labelpositive | neutral | negativeEstimated sentiment
sentiment_scorenumber, −1 to 1Strength of the sentiment
sentiment_modelstringWhich model scored it
sentiment_whystringThe reason for the score
intent_score / intent_why0–10, stringBuying-intent estimate and reason
tags, triage_statestring[], stringYour workspace's own labels

Webhook signatures

Deliveries are POST requests with a JSON body and two headers: X-XMonitor-Event (the event name) and X-XMonitor-Signature (sha256= followed by the HMAC-SHA256 of the raw body, keyed with your webhook secret). Verify against the raw bytes, before parsing.

import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/hook", (req, res) => {
  const sig = req.get("x-xmonitor-signature") || "";
  const want = "sha256=" + crypto
    .createHmac("sha256", process.env.SW_WEBHOOK_SECRET)
    .update(req.body).digest("hex");
  if (sig.length !== want.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want)))
    return res.sendStatus(401);
  console.log(req.get("x-xmonitor-event"), JSON.parse(req.body));
  res.sendStatus(200);
});
app.listen(3000);

MCP for AI agents Live

The Vanefeed MCP server gives agents in Claude, Cursor and other MCP clients eight tools: get_profile, get_posts, get_post, get_comments, search_platform, search_tracked_mentions, get_usage and get_status. Each result states its credit cost. It's one file with no dependencies and needs Node 18 or newer.

1. Download vanefeed-mcp.mjs. 2. Add it to your client's MCP config with your key:

{
  "mcpServers": {
    "vanefeed": {
      "command": "node",
      "args": ["/path/to/vanefeed-mcp.mjs"],
      "env": { "SW": "https://api.vanefeed.com", "SW_KEY": "sk_live_your_key" }
    }
  }
}

SDKs Live

One-file clients with no dependencies. They handle authentication, paging, retries on rate limits, and expose each call's cost. Download: vanefeed.py (Python 3.9+) · vanefeed.mjs (Node 18+, Deno, Bun, browsers).

from vanefeed import Vanefeed

sw = Vanefeed("sk_live_your_key")
profile = sw.profile("youtube", "@TED")["data"]
print(profile["followers"])

for post in sw.iter_posts("x", "NASA", limit=20, max_pages=3):
    print(post["published_at"], post["text"][:80])

print("used", sw.last_cost, "credits; left:", sw.last_remaining)

Versioning & deprecation

  • The version is in the path: /v1. Within a version we only make additive changes: new endpoints, new optional parameters, new fields.
  • Your code should ignore fields it doesn't know. We never change a field's type or meaning within a version.
  • Removing or renaming anything in a released version gets at least 90 days' notice by email and in the changelog, with a Deprecation header on affected responses.
  • Platform changes we can't control (a platform hiding a count) show up as null, never as a broken shape.

Platform status Live

GET /v1/status is public and free, with no key needed. For each platform it reports a state (operational, degraded or outage), the success rate and median and 95th-percentile response time of live calls in the last hour, and the latest background health check. Check it before large batch jobs.

curl "$SW/v1/status"
Response (illustrative numbers)
{
  "data": {
    "overall": "operational",
    "window": "last 60 minutes",
    "platforms": [
      {
        "platform": "youtube",
        "state": "operational",
        "live_last_hour": { "calls": 412, "success_rate": 0.998, "p50_s": 1.41, "p95_s": 2.9 },
        "health_check": { "state": "ok", "success_rate": 1.0, "checked_at": 1791007101 }
      }
    ]
  }
}