InsightSocial API
Recipes

Brand mention monitoring

Run a daily brand-mention sweep across the search endpoints of six platforms in parallel, drop off-topic rows with a keyword check, dedupe against earlier runs and alert on what is new. From 60 credits per sweep.

This recipe is the daily job at the heart of social listening. Once a day it searches six platforms for your brand name, drops rows that turn out to be about something else, skips anything it has seen before, and sends what remains to Slack, email or a dashboard.

Cost per run

At least 60 credits per sweep: six search calls whose price starts at 10 each. All six are metered, so each call holds its ceiling when it starts (650 on Instagram, 370 on TikTok, 340 on Threads, 300 on Reddit, 150 on YouTube, 50 on X) and is charged what it used. Keep at least 1,860 credits available while a sweep runs. A daily sweep costs at least 1,800 credits a month.

How do you track brand mentions with an API?

Call each platform's keyword search with your brand name, in parallel, limited to the last day. Then check each row's text yourself: a brand name that is also an ordinary word ("notion", "apple") matches a lot of posts that are not about you. You also supply the dedupe layer, because "new since the last run" is your state.

What you need

EndpointPlatformDate windowCredits
GET /v1/reddit/searchRedditmax_age_days10–300
GET /v1/tiktok/searchTikTokmax_age_days10–370
GET /v1/youtube/searchYouTubemax_age_days10–150
GET /v1/twitter/search/tweetsXsince: operator in query, or max_age_days10–50
GET /v1/threads/searchThreadsstart_date / end_date10–340
GET /v1/instagram/search/reelsInstagrammax_age_days10–650

All six take query and return posts in the same shape: the text is post.content.text and the link is post.url, on every platform. max_age_days keeps only rows published inside the window. It filters after each page is fetched, so the page is billed as usual, and a day with nothing new returns an empty list.

The code

Python
# brand_mentions.py
# Daily brand-mention sweep with a keyword check and dedupe.
# Keep the known URLs somewhere durable between runs.
# Run with: INSIGHTSOCIAL_API_KEY=isk_live_... python brand_mentions.py
import json
import os
import re
from concurrent.futures import ThreadPoolExecutor
from datetime import date, timedelta
from pathlib import Path

import requests

KEY = os.environ["INSIGHTSOCIAL_API_KEY"]
BASE = "https://api.insightsocial.app/v1"
BRAND = "notion"
# The name is an ordinary word, so a row must also mention one of these.
CONTEXT = ["app", "workspace", "template", "notes", "wiki", "database", "page"]
KNOWN_FILE = Path("known-mentions.json")
YESTERDAY = (date.today() - timedelta(days=1)).isoformat()

SEARCHES = {
    "reddit":    ("reddit/search",          {"max_age_days": 1}),
    "tiktok":    ("tiktok/search",          {"max_age_days": 1}),
    "youtube":   ("youtube/search",         {"max_age_days": 1}),
    "twitter":   ("twitter/search/tweets",  {"max_age_days": 1}),
    "threads":   ("threads/search",         {"start_date": YESTERDAY}),
    "instagram": ("instagram/search/reels", {"max_age_days": 1}),
}

BRAND_RE = re.compile(rf"\b{re.escape(BRAND)}\b", re.IGNORECASE)
CONTEXT_RE = re.compile(r"\b(" + "|".join(map(re.escape, CONTEXT)) + r")\b", re.IGNORECASE)

def on_topic(text):
    # A row with no text (a video with no caption) cannot be checked: leave it out.
    if not text or not BRAND_RE.search(text):
        return False
    return not CONTEXT or bool(CONTEXT_RE.search(text))

def get(path, **params):
    res = requests.get(f"{BASE}/{path}", params=params,
                       headers={"x-api-key": KEY}, timeout=120)
    return res.json()

def search(platform):
    path, extra = SEARCHES[platform]
    return platform, get(path, query=BRAND, **extra)

# Step 1: six searches in parallel.
with ThreadPoolExecutor(max_workers=6) as pool:
    results = dict(pool.map(search, SEARCHES))

# Step 2: keep on-topic rows, dedupe against earlier runs by post URL.
known = set(json.loads(KNOWN_FILE.read_text())) if KNOWN_FILE.exists() else set()
fresh, spent, off_topic = [], 0, 0
for platform, body in results.items():
    spent += body.get("credits_used", 0)
    if not body["success"]:
        print(f"{platform}: {body['error']['type']}")
        continue
    for item in body["data"]["items"]:
        post = item["post"]
        text = post["content"]["text"]
        if not on_topic(text):
            off_topic += 1
            continue
        url = post["url"]
        if url and url not in known:
            known.add(url)
            fresh.append((platform, post))
KNOWN_FILE.write_text(json.dumps(sorted(known)))

# Step 3: alert on what is new.
print(f"{len(fresh)} new mentions of {BRAND!r}, {off_topic} off-topic rows skipped ({spent} credits)")
for platform, post in fresh:
    print(f"[{platform}] {post['content']['text'][:120]}")
    print(f"  {post['url']}")
    # Replace print with a Slack webhook, an email or a database insert.

Run it once a day. The one-day window plus the URL set means each mention surfaces once.

Threads takes dates, not max_age_days

/v1/threads/search bounds the period with start_date and end_date (YYYY-MM-DD). The code sets start_date to yesterday's date. On X, you can also put the window in the query itself, for example query=notion since:2026-09-22.

What you get back

Example output
14 new mentions of 'notion', 9 off-topic rows skipped (80 credits)
[reddit] Switched our team wiki from Confluence to Notion, here is what broke
  https://www.reddit.com/r/productivity/comments/...
[youtube] My 2026 Notion setup for university, templates included
  https://www.youtube.com/watch?v=...
[twitter] notion's new offline mode finally shipped, every page loads on the train
  https://x.com/.../status/...
...

The figures are illustrative. The keyword check runs on your side, so it costs nothing, and a post about "a notion of fairness" does not reach your alert channel. Rows skipped this way were still fetched and are part of the page you paid for.

What to change

  • BRAND and CONTEXT. Swap in a competitor, a product or a person. CONTEXT matters most when the name is also an ordinary word. Set it to [] for a name nobody uses for anything else.
  • A stricter check. Keyword rules miss sarcasm and odd phrasing. For borderline rows, send the text to a classifier or a language model you run, with a prompt that names your brand.
  • One language. Every post carries post.language, the platform's own tag (for example en). Many platforms leave it null, so keep rows where it is null rather than dropping them.
  • The window. Match max_age_days to how often the job runs, so windows neither overlap nor leave gaps.
  • Fewer platforms, lower price. Drop the searches you do not need. Each one removed saves at least 10 credits and lowers the hold. Check a call's price first with dry_run=1, which is never charged.
  • Persist the known URLs properly. A JSON file is fine on one machine. Move it to a database table before you run on more than one worker.

On this page