Build a Social Monitor Agent (OpenAI SDK)
Build a Douyin and Weibo brand monitor in Python: requests calls SandBase public-data APIs, the OpenAI SDK runs tool-calling and sentiment alerts.

TL;DR — A working social monitoring agent in Python (a ~340-line reference implementation, not a minimal snippet). It uses
requeststo call SandBase’s public-data APIs atPOST /v1/api/<endpoint>and the OpenAI SDK for LLM tool-calling. It searches Douyin and Weibo for your keywords, asks the LLM for a sentiment assessment, and raises an alert when the LLM flags one or the reported negative share crosses a threshold. Tested on 2026-10-01 (UTC).
What we’re building
A social monitoring agent that:
- Periodically searches Douyin and Weibo for mentions of specified keywords
- Uses an LLM to read the results and assess sentiment and urgency
- Produces a structured alert when the LLM flags one, or when the negative share it reports is above
ALERT_THRESHOLD_NEGATIVE - Uses direct HTTP requests for the data APIs and the OpenAI SDK for LLM reasoning
It does not detect volume spikes: each search reads one page of results, so a per-cycle result count is not a volume measure. See Add trend comparison for what that would take.
Why this two-client pattern
SandBase exposes each platform data API as its own route, POST https://api.sandbase.ai/v1/api/<vendor>/<path>, with a JSON body of only that endpoint’s parameters. LLMs go through the OpenAI-compatible chat completions endpoint. So:
requestsfor data retrieval (each endpoint returns structured JSON synchronously)- OpenAI SDK for LLM reasoning (chat completions with tool-calling)
- One API key for both (same SandBase
Authorization: Bearerheader)
What data this uses
Everything here is public, read-only data: search results and public comments that anyone can see on Douyin and Weibo. You need a SandBase API key. SandBase is not an official partner of Douyin or Weibo, and this agent cannot read private-account data, DMs or owner-only analytics, and cannot publish or take account actions.
| This tutorial (SandBase public-data APIs) | Official developer platforms (Douyin Open Platform, Weibo Open Platform) | |
|---|---|---|
| Access | One SandBase API key | App registration and platform review |
| Scope | Public content: keyword search, public comments | Authorized, account-linked workflows for accounts that grant your app access |
| Account actions (publish, reply, DMs) | No | Where the platform grants the permission |
| Owner analytics | No | For authorized accounts |
| Choose it when | You need to watch public conversation about a keyword across platforms | You need to act on, or read private data of, accounts you manage |
Neither platform offers a general unauthenticated API for arbitrary public data, which is the gap this approach fills.
Prerequisites
pip install openai requests pydantic
You’ll need a SandBase API key and access to:
- An LLM model (we’ll use
openai/gpt-4.1-mini) - The three data endpoints below (browse the Douyin catalog and Weibo catalog)
The LLM client uses SandBase’s OpenAI-compatible POST /v1/chat/completions endpoint, described in the Chat Completions guide; model IDs keep the provider prefix (openai/gpt-4.1-mini). On 2026-10-01 (UTC) a request to that endpoint with model: "openai/gpt-4.1-mini" and one tools definition returned HTTP 200 with finish_reason: "tool_calls" and a tool_calls entry.
Endpoint references for every data call in the code:
douyin/search/general-search-v2(keyword,cursor,sort_type)weibo/web-v2/realtime-search(query,page)douyin/web/video-comments(aweme_id,cursor,count)
The Douyin search reference below is where the request body in execute_tool comes from: a POST /v1/api/douyin/search/general-search-v2 route with keyword, cursor and backtrace among its optional fields.

Caption: The douyin/search/general-search-v2 endpoint reference, showing the POST route, the backtrace, cursor and keyword fields and the outputs[0].data response shape the helper reads (captured 2026-10-02).
The complete agent
"""
Social Media Monitor Agent — OpenAI SDK + Direct HTTP Pattern
Monitors Douyin and Weibo for brand mentions, analyzes sentiment.
Uses requests for SandBase data APIs + OpenAI SDK for LLM reasoning.
"""
import json
import os
import time
from collections import deque
from datetime import datetime
import requests
from openai import OpenAI
from pydantic import BaseModel
# --- Configuration ---
SANDBASE_API_KEY = os.environ["SANDBASE_API_KEY"]
API_BASE = "https://api.sandbase.ai/v1/api"
MONITOR_KEYWORDS = ["你的品牌", "YourBrand", "品牌产品名"]
CHECK_INTERVAL_SECONDS = 900 # 15 minutes
ALERT_THRESHOLD_NEGATIVE = 30 # alert if the LLM reports >30% negative
MAX_ITERATIONS = 4 # tool-use LLM calls per keyword (upper bound)
MAX_DATA_CALLS_PER_KEYWORD = 4 # data API calls per keyword (upper bound)
MAX_RETRIES = 3
# --- Client Setup ---
HEADERS = {
"Authorization": f"Bearer {SANDBASE_API_KEY}",
"Content-Type": "application/json",
}
# OpenAI SDK client for LLM reasoning only
client = OpenAI(
base_url="https://api.sandbase.ai/v1",
api_key=SANDBASE_API_KEY,
)
# --- Data Models ---
class Alert(BaseModel):
timestamp: str
severity: str # "low", "medium", "high", "critical"
keyword: str
platform: str
reason: str
details: str
# --- Rate limiting (client-side pacing you choose, not a documented SandBase limit) ---
class RateLimiter:
def __init__(self, max_calls: int, window_seconds: float):
self.max_calls = max_calls
self.window = window_seconds
self.calls = deque()
def wait(self):
now = time.monotonic()
while self.calls and self.calls[0] <= now - self.window:
self.calls.popleft()
if len(self.calls) >= self.max_calls:
time.sleep(self.calls[0] + self.window - now)
self.calls.append(time.monotonic())
limiter = RateLimiter(max_calls=30, window_seconds=60)
# --- Tool name -> SandBase endpoint ---
TOOL_ENDPOINTS = {
"search_douyin_content": "douyin/search/general-search-v2",
"search_weibo_topics": "weibo/web-v2/realtime-search",
"get_douyin_video_comments": "douyin/web/video-comments",
}
# --- Tool Definitions ---
tools = [
{
"type": "function",
"function": {
"name": "search_douyin_content",
"description": "Search Douyin for videos matching a keyword. Returns recent videos with engagement metrics.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "Search keyword"},
"count": {"type": "integer", "description": "Number of results to return (max 20)", "default": 10}
},
"required": ["keyword"]
}
}
},
{
"type": "function",
"function": {
"name": "search_weibo_topics",
"description": "Search Weibo for recent posts matching a keyword. Returns posts with engagement data.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "Search keyword"},
"count": {"type": "integer", "description": "Number of results to return (max 20)", "default": 10}
},
"required": ["keyword"]
}
}
},
{
"type": "function",
"function": {
"name": "get_douyin_video_comments",
"description": "Get public comments on a specific Douyin video for sentiment analysis.",
"parameters": {
"type": "object",
"properties": {
"video_id": {"type": "string", "description": "Douyin video ID (video_id from search_douyin_content)"},
"count": {"type": "integer", "description": "Number of comments to return (max 20)", "default": 20}
},
"required": ["video_id"]
}
}
}
]
# --- Data API call: POST /v1/api/<endpoint>, read outputs[0].data ---
def call_data_api(endpoint: str, params: dict) -> dict:
"""Rate-limited call with retry/backoff on network errors, HTTP 429 and 5xx."""
last_error = None
for attempt in range(MAX_RETRIES):
limiter.wait()
try:
resp = requests.post(f"{API_BASE}/{endpoint}", headers=HEADERS, json=params, timeout=90)
except (requests.ConnectionError, requests.Timeout) as e:
last_error = e
else:
if resp.status_code != 429 and resp.status_code < 500:
resp.raise_for_status() # other 4xx (bad params, bad key): retrying won't help
body = resp.json()
outputs = body.get("outputs") or []
if body.get("status") != "completed" or not outputs:
raise RuntimeError(f"SandBase call did not complete: {body.get('status')} {body.get('error')}")
return outputs[0].get("data") or {}
last_error = RuntimeError(f"HTTP {resp.status_code} from {endpoint}")
if attempt < MAX_RETRIES - 1:
time.sleep(2 ** attempt) # 1s, 2s
raise RuntimeError(f"{endpoint} failed after {MAX_RETRIES} attempts: {last_error}")
def execute_tool(tool_name: str, arguments: dict) -> str:
"""Route a tool call to its SandBase endpoint.
Raw responses are large, so each tool returns a compact list for the LLM."""
count = min(int(arguments.get("count", 10)), 20)
endpoint = TOOL_ENDPOINTS.get(tool_name)
if endpoint is None:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
if tool_name == "search_douyin_content":
data = call_data_api(endpoint, {"keyword": arguments["keyword"], "cursor": 0, "sort_type": "0"})
items = []
for card in data.get("business_data") or []:
aweme = (card.get("data") or {}).get("aweme_info")
if not aweme:
continue # skip related-search cards etc.
stats = aweme.get("statistics") or {}
items.append({
"video_id": aweme.get("aweme_id"),
"desc": aweme.get("desc"),
"author": (aweme.get("author") or {}).get("nickname"),
"likes": stats.get("digg_count"),
"comments": stats.get("comment_count"),
"shares": stats.get("share_count"),
})
return json.dumps(items[:count], ensure_ascii=False)
if tool_name == "search_weibo_topics":
data = call_data_api(endpoint, {"query": arguments["keyword"], "page": 1})
posts = []
for p in (data.get("parsed_data") or {}).get("results") or []:
inter = p.get("interaction") or {}
posts.append({
"weibo_id": p.get("weibo_id"),
"user": p.get("user_name"),
"content": p.get("content"),
"publish_time": p.get("publish_time"),
"reposts": inter.get("repost_count"),
"comments": inter.get("comment_count"),
"likes": inter.get("like_count"),
})
return json.dumps(posts[:count], ensure_ascii=False)
# get_douyin_video_comments
data = call_data_api(endpoint, {"aweme_id": arguments["video_id"], "cursor": 0, "count": 20})
comments = [
{"text": c.get("text"), "likes": c.get("digg_count"), "replies": c.get("reply_comment_total")}
for c in data.get("comments") or []
]
return json.dumps(comments[:count], ensure_ascii=False)
# --- Agent Loop ---
def run_monitoring_cycle(keywords: list[str]) -> list[Alert]:
"""Run one monitoring cycle: search, analyze, alert."""
alerts = []
for keyword in keywords:
# Step 1: Ask the agent to monitor this keyword
messages = [
{
"role": "system",
"content": """You are a social media monitoring agent. For each keyword:
1. Search both Douyin and Weibo for recent mentions
2. Analyze the sentiment of the results
3. Identify any urgent items (complaints, crises, viral negative content)
4. Produce a structured assessment
Be concise. Focus on actionable signals, not summaries of neutral content.
If engagement is unusually high on negative content, flag it immediately."""
},
{
"role": "user",
"content": f"Monitor keyword: '{keyword}'. Search both platforms and provide sentiment analysis."
}
]
# Step 2: Agent reasons and calls tools (both loops are capped)
data_calls = 0
for _ in range(MAX_ITERATIONS):
response = client.chat.completions.create(
model="openai/gpt-4.1-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
messages.append(message)
# If no tool calls, agent is done reasoning
if not message.tool_calls:
break
# Every tool call needs a tool message, even when it is refused or fails
for tool_call in message.tool_calls:
if data_calls >= MAX_DATA_CALLS_PER_KEYWORD:
result = json.dumps({"error": "data call budget for this keyword is used up; answer with what you have"})
else:
data_calls += 1
try:
arguments = json.loads(tool_call.function.arguments)
result = execute_tool(tool_call.function.name, arguments)
except Exception as e:
result = json.dumps({"error": str(e), "tool": tool_call.function.name})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Step 3: Extract structured assessment
assessment_prompt = messages + [{
"role": "user",
"content": """Based on your analysis, provide a JSON response with:
{
"keyword": "the keyword",
"platforms_checked": ["douyin", "weibo"],
"total_mentions": <number of results you reviewed>,
"sentiment": {"positive": <pct>, "neutral": <pct>, "negative": <pct>},
"alert_needed": true/false,
"alert_severity": "low|medium|high|critical",
"alert_reason": "why an alert is needed (or empty)",
"urgent_items": ["list of specific concerning items"],
"summary": "one-paragraph summary"
}
Only output the JSON, no markdown."""
}]
assessment_response = client.chat.completions.create(
model="openai/gpt-4.1-mini",
messages=assessment_prompt
)
try:
assessment = json.loads(assessment_response.choices[0].message.content)
except json.JSONDecodeError:
print(f"? {keyword}: assessment was not valid JSON, skipped")
continue
# Step 4: Alert if the LLM flags it OR the reported negative share is over the threshold
try:
negative_pct = float((assessment.get("sentiment") or {}).get("negative") or 0)
except (TypeError, ValueError):
negative_pct = 0.0
over_threshold = negative_pct > ALERT_THRESHOLD_NEGATIVE
if assessment.get("alert_needed") or over_threshold:
reason = assessment.get("alert_reason") or (
f"{negative_pct:.0f}% negative (threshold {ALERT_THRESHOLD_NEGATIVE}%)")
alert = Alert(
timestamp=datetime.now().isoformat(),
severity=assessment.get("alert_severity") or "medium",
keyword=keyword,
platform=", ".join(assessment.get("platforms_checked") or ["douyin", "weibo"]),
reason=reason,
details=assessment.get("summary", "")
)
alerts.append(alert)
print(f"🚨 ALERT [{alert.severity.upper()}] — {alert.keyword}: {alert.reason}")
else:
print(f"✓ {keyword}: Normal ({assessment.get('total_mentions', 0)} results reviewed, "
f"{negative_pct:.0f}% negative, {data_calls} data calls)")
return alerts
# --- Alert Delivery ---
def deliver_alerts(alerts: list[Alert]):
"""Print alerts. To push them to Slack, POST {"text": ...} to an incoming webhook URL."""
for alert in alerts:
if alert.severity in ("high", "critical"):
print(f"\n{'='*60}")
print(f"🔴 {alert.severity.upper()} ALERT — {alert.timestamp}")
print(f" Keyword: {alert.keyword}")
print(f" Platform: {alert.platform}")
print(f" Reason: {alert.reason}")
print(f" Details: {alert.details}")
print(f"{'='*60}\n")
else:
# Low-priority: log for daily digest
print(f"📋 [{alert.severity}] {alert.keyword}: {alert.reason}")
# --- Main Loop ---
def main():
"""Run the monitoring agent on a schedule."""
print("🔍 Social Monitor Agent started")
print(f" Keywords: {MONITOR_KEYWORDS}")
print(f" Interval: {CHECK_INTERVAL_SECONDS}s")
print(f" Alert threshold: >{ALERT_THRESHOLD_NEGATIVE}% negative (LLM-reported)")
print()
while True:
print(f"--- Cycle start: {datetime.now().isoformat()} ---")
alerts = run_monitoring_cycle(MONITOR_KEYWORDS)
if alerts:
deliver_alerts(alerts)
else:
print("No alerts this cycle.")
print(f"--- Next cycle in {CHECK_INTERVAL_SECONDS}s ---\n")
time.sleep(CHECK_INTERVAL_SECONDS)
if __name__ == "__main__":
main()
How the pattern works
Data APIs use direct HTTP and LLM calls use the OpenAI SDK:
# LLM reasoning — OpenAI SDK (chat completions)
client.chat.completions.create(model="openai/gpt-4.1-mini", messages=...)
# Data retrieval — one route per endpoint, body = only that endpoint's params
requests.post("https://api.sandbase.ai/v1/api/douyin/search/general-search-v2",
headers=HEADERS,
json={"keyword": "...", "cursor": 0, "sort_type": "0"})
SandBase’s data APIs are not LLMs: they don’t take messages or return chat completions. TOOL_ENDPOINTS maps each tool to its endpoint, and call_data_api posts only that endpoint’s parameters. The endpoint reference documents the completed response as {id, status, model, outputs: [{data: ...}]}, so the helper reads outputs[0].data and raises an error with status and error when the call did not complete. These are the Model API POST routes from the endpoint reference, not the GET routes shown on the catalog pages.
Neither search endpoint takes a result count, so the tools slice locally. The agent only reads the first page. Pagination differs per endpoint: Douyin general search pages by sending back cursor, search_id and backtrace from the previous response; Weibo realtime search takes a page number; video comments take cursor.
The two other references show those parameters directly: Weibo realtime search lists query (required) and page, and video comments lists aweme_id (required), count (default 20) and cursor (default 0).

Caption: The weibo/web-v2/realtime-search reference, with query required and page defaulting to 1, matching the {"query": ..., "page": 1} body the agent sends (captured 2026-10-02).

Caption: The douyin/web/video-comments reference, showing required aweme_id plus count (default 20) and cursor (default 0), the three fields get_douyin_video_comments sends (captured 2026-10-02).
Every call_data_api call waits on the RateLimiter (30 calls per 60 seconds here; that number is your own pacing, not a SandBase quota) and retries network errors, HTTP 429 and 5xx with 1s/2s backoff. Tool errors are passed back to the LLM as {"error": ...} so one failed call doesn’t kill the cycle.
Tested on 2026-10-01 (UTC)
I ran the full agent code above with the LLM stubbed and every data call live, using the brand keyword 瑞幸咖啡 (Luckin Coffee). The business field names the tools read come from these calls. They are observed-only, not documented guarantees, which is why every read uses .get(). Creator names and post text are replaced with <redacted> below; the numbers are as returned.
POST https://api.sandbase.ai/v1/api/douyin/search/general-search-v2
{"keyword": "瑞幸咖啡", "cursor": 0, "sort_type": "0"}
{"id": "a7e4f79e-0231-4a05-821f-7db4d4799a76", "status": "completed", "model": "douyin/search/general-search-v2",
"outputs": [{"data": {"business_data": [{"data": {"aweme_info": {
"aweme_id": "<redacted>", "desc": "<redacted> #瑞幸 ...",
"author": {"nickname": "<redacted>"},
"statistics": {"digg_count": 943163, "comment_count": 12982, "share_count": 215170, ...}}}}, ...]}}]}
POST https://api.sandbase.ai/v1/api/weibo/web-v2/realtime-search
{"query": "瑞幸咖啡", "page": 1}
{"id": "0c118fd2-0dda-4b11-9cad-b2cfe1cccefe", "status": "completed", ...
"outputs": [{"data": {"parsed_data": {"results": [{
"weibo_id": "<redacted>", "user_name": "<redacted>", "content": "<redacted>",
"publish_time": "2分钟前", "interaction": {"comment_count": 0, "like_count": 0, "repost_count": 0}}, ...]}}}]}
POST https://api.sandbase.ai/v1/api/douyin/web/video-comments
{"aweme_id": "<video_id from the search above>", "cursor": 0, "count": 20}
{"id": "a0ed3f3d-0c2f-412b-9159-9bfc91569a5e", "status": "completed", ...
"outputs": [{"data": {"comments": [{"text": "<redacted>", "digg_count": 18, "reply_comment_total": 2}, ...]}}]}
In that cycle the tools got 17 videos, 9 Weibo posts and 19 comments before slicing. Across 28 data calls in my two test runs (EN and ZH code), two returned HTTP 503 and the retries succeeded. Raw Douyin search payloads in these runs were close to 1 MB, so the tools return a compact list instead of dumping the whole response into the LLM context.
Cost breakdown
Data API calls: the three endpoints are listed as Free in the SandBase catalog as of 2026-10-01, so the data side is counted in calls. LLM: openai/gpt-4.1-mini is $0.40 per million input tokens and $1.60 per million output tokens on SandBase as of 2026-10-01.
Call counts per keyword are bounded by the code: 1 to MAX_ITERATIONS (4) tool-use LLM calls, 1 assessment call, and 0 to MAX_DATA_CALLS_PER_KEYWORD (4) data calls. The typical path is: search both platforms, fetch comments for one video, answer — 3 tool-use calls, 1 assessment and 3 data calls.
Estimate with illustrative token assumptions (tool-use call ~2K input + 500 output; assessment ~1K input + 200 output):
Tool-use call: 2,000 × $0.40/M + 500 × $1.60/M = $0.0008 + $0.0008 = $0.0016
Assessment call: 1,000 × $0.40/M + 200 × $1.60/M = $0.0004 + $0.00032 = $0.00072
Per keyword (typical, 3 tool-use + 1 assessment): 3 × $0.0016 + $0.00072 = $0.00552
Per keyword (upper bound, 4 + 1): 4 × $0.0016 + $0.00072 = $0.00712
Per cycle, 3 keywords: typical $0.01656 upper bound $0.02136
Data calls per cycle: typical 9 upper bound 12
Running every 15 minutes (96 cycles a day, 30-day month):
Typical: $0.01656 × 96 = $1.59/day, × 30 ≈ $47.69/month; 864 data calls/day
Upper bound: $0.02136 × 96 = $2.05/day, × 30 ≈ $61.52/month; 1,152 data calls/day
These are estimates, not measurements. Real input tokens grow as tool results pile up in messages, and the assessment call resends the whole conversation, so log response.usage for a few days and redo the math with your numbers.
Extending the agent
Add more platforms
# Add Xiaohongshu monitoring: add to TOOL_ENDPOINTS
# "search_xiaohongshu_notes": "xiaohongshu/app-v2/search-notes"
# and route it in execute_tool with {"keyword": ..., "page": 1}
{
"type": "function",
"function": {
"name": "search_xiaohongshu_notes",
"description": "Search Xiaohongshu for notes mentioning a keyword.",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string"},
"count": {"type": "integer", "default": 10}
},
"required": ["keyword"]
}
}
}
In a 2026-10-01 call to xiaohongshu/app-v2/search-notes with {"keyword": "瑞幸咖啡", "page": 1}, 20 notes came back under data.items[] (run 1a422dd3-d11a-446b-a0ea-0ba15c3a2a9c, observed-only). Remember to raise MAX_DATA_CALLS_PER_KEYWORD when you add a tool.
Add trend comparison
The agent above is stateless per cycle. Spike detection needs two things it doesn’t have: a volume signal that isn’t fixed by page size (for example, the number of Weibo weibo_ids you haven’t seen before), and history persisted across restarts. A starting point, not wired into the loop:
# In-memory history; persist it (SQLite, JSON file) for real use
class TrendTracker:
def __init__(self):
self.history = {} # keyword -> [(timestamp, new_posts)]
def record(self, keyword: str, new_posts: int):
self.history.setdefault(keyword, []).append((time.time(), new_posts))
def detect_spike(self, keyword: str, window_hours: int = 24) -> bool:
"""True if the latest count is 3x the average over the window."""
cutoff = time.time() - (window_hours * 3600)
recent = [n for t, n in self.history.get(keyword, []) if t > cutoff]
if len(recent) < 10:
return False
avg = sum(recent[:-1]) / len(recent[:-1])
return avg > 0 and recent[-1] > avg * 3
Add automated response drafting
def draft_response(alert: Alert) -> str:
"""Use the LLM to draft a response for a human to review."""
response = client.chat.completions.create(
model="openai/gpt-4.1-mini",
messages=[
{"role": "system", "content": "You are a brand communications specialist. "
"Draft a brief, empathetic response to the following social media issue. "
"Keep it professional, acknowledge the concern, and offer next steps."},
{"role": "user", "content": f"Issue: {alert.reason}\nDetails: {alert.details}\n"
"Draft a response suitable for posting on social media."}
]
)
return response.choices[0].message.content
The data APIs are read-only, so posting the draft is up to your team through the platform’s own tools.
Related Reading
- Build a Social Listening Agent: Weibo + Douyin
- Build a Douyin Competitor Monitor Agent
- Top 5 Social Media Data APIs for AI Agents (2026)
- Best Douyin Data API Services in 2026
- Xiaohongshu KOL Screening Agent (Tutorial)
- China Social Commerce Data: The Agent Opportunity
Key takeaways
- Two clients, one API key.
requestsfor SandBase data APIs atPOST /v1/api/<endpoint>, the OpenAI SDK for LLM reasoning, both authenticated with the same key. - Tool-calling is the integration pattern. Map each tool to one endpoint, let the LLM decide when to call it, and cap both the LLM loop and the data calls.
- Read only
outputs[0].data, and fail loudly. Raise when a call doesn’t complete instead of silently returning{}. - LLM calls are the dollar cost. The data endpoints are listed as Free as of 2026-10-01; log token usage and tune the LLM layer first.
- Sync APIs keep the loop simple. Because social data APIs are sync-only, there are no callbacks or task management. The response schema also lists
pendingandrunningstates; this agent treats anything other thancompletedas an error rather than polling.
Get a SandBase API key · Browse the Douyin catalog · Browse the Weibo catalog