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
| Endpoint | Platform | Date window | Credits |
|---|---|---|---|
GET /v1/reddit/search | max_age_days | 10–300 | |
GET /v1/tiktok/search | TikTok | max_age_days | 10–370 |
GET /v1/youtube/search | YouTube | max_age_days | 10–150 |
GET /v1/twitter/search/tweets | X | since: operator in query, or max_age_days | 10–50 |
GET /v1/threads/search | Threads | start_date / end_date | 10–340 |
GET /v1/instagram/search/reels | max_age_days | 10–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
# 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
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
BRANDandCONTEXT. Swap in a competitor, a product or a person.CONTEXTmatters 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 exampleen). Many platforms leave itnull, so keep rows where it isnullrather than dropping them. - The window. Match
max_age_daysto 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.
Related
Recipes
Nine end-to-end builds on the InsightSocial API, from brand monitoring to ad audits, each with its credit cost worked out before you run it.
Sentiment analysis
Find the conversation about a topic on YouTube and Reddit, pull the comments, and score each comment's sentiment with a model you run. From 200 credits per run.