Watchlists.
Track competitor pages on the Meta Ad Library and read what they launch, kill and keep running. Everything here is the same data the dashboard shows, scoped to the workspace your API key belongs to (or the one named in X-Workspace-Id).
The Watchlist object
id: watchlist id. Matchesdata.watchlist.idon the webhook.brand: Facebook page name.page_url: canonical Facebook page URL.workspace_id,workspace_name: owning workspace.nullfor legacy watchlists created before workspaces shipped.created_at: when the watchlist was added.last_alerted_at: when we last sent an alert for this watchlist. Useful for reconciling against your own event log.alerts.email_enabled: whether email digests are on.alerts.webhook_url: the webhook receiver configured for this watchlist, ornull.
List watchlists
/api/v1/watchlists/Every watchlist on your account, across all workspaces. No pagination: a typical account has a few hundred at most, so sort client-side.
curl https://api.brandmov.com/api/v1/watchlists/ \
-H "Authorization: Bearer bm_live_your_key_here"[
{
"id": "wl_abc123",
"brand": "Example Brand",
"page_url": "https://www.facebook.com/examplebrand",
"workspace_id": "1c9d2f4a-7b6e-4a2f-9c3d-8e5f1a2b3c4d",
"workspace_name": "Acme Growth",
"created_at": "2026-08-15T10:30:00+00:00",
"last_alerted_at": "2026-09-02T09:00:00+00:00",
"alerts": {
"email_enabled": true,
"webhook_url": "https://ops.example.com/webhooks/brandmov"
}
}
]Get a watchlist
/api/v1/watchlists/{watchlist_id}curl https://api.brandmov.com/api/v1/watchlists/wl_abc123 \
-H "Authorization: Bearer bm_live_your_key_here"{
"id": "wl_abc123",
"brand": "Example Brand",
"page_url": "https://www.facebook.com/examplebrand",
"workspace_id": "1c9d2f4a-7b6e-4a2f-9c3d-8e5f1a2b3c4d",
"workspace_name": "Acme Growth",
"created_at": "2026-08-15T10:30:00+00:00",
"last_alerted_at": "2026-09-02T09:00:00+00:00",
"alerts": {
"email_enabled": true,
"webhook_url": "https://ops.example.com/webhooks/brandmov"
}
}404 when the id does not exist or belongs to another account. The two cases look the same on purpose.
{
"detail": "Watchlist not found"
}Add a competitor
/api/v1/watchlists/page_url | string, required | Best: an Ad Library URL with view_all_page_id. Also accepts facebook.com/profile.php?id= and facebook.com/<slug>. |
page_name | string | Display name. Taken from the URL when left out. |
auto_refresh | bool | Default true: queue the first scrape now. Ads show up in the feed within a few minutes. |
curl -X POST https://api.brandmov.com/api/v1/watchlists/ \
-H "Authorization: Bearer bm_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"page_url": "https://www.facebook.com/ads/library/?view_all_page_id=104000000000000"}'Response: 201 Created
{
"watchlist": { "id": "wl_abc123", "brand": "Example Brand", "...": "..." },
"background_scrape": {
"status": "queued",
"initial_job_id": "8f2c...",
"target_ads": "all_available"
}
}400 when you are at your plan's watchlist limit, 409 when the page is already on your watchlist.
Remove a competitor
/api/v1/watchlists/{watchlist_id}{ "removed": "wl_abc123" }Ads feed
/api/v1/watchlists/{watchlist_id}/feedThe brand's ads as of the last scrape. Served from cache, so it is fast and free.
max_results | int | 1 to 500. Default 50. |
ad_active_status | string | active or inactive. |
media_type | string | IMAGE or VIDEO. |
sort_by | string | days_run or total_impressions. Default newest first. |
curl "https://api.brandmov.com/api/v1/watchlists/wl_abc123/feed?max_results=20&ad_active_status=active&sort_by=days_run" \
-H "Authorization: Bearer bm_live_your_key_here"Returns {watchlist_id, count, results[]}. Each result carries the copy, media URLs, start and end dates, platforms and the Ad Library link.
Refresh
/api/v1/watchlists/{watchlist_id}/refreshQueues a fresh scrape of the brand. Read the feed again a few minutes later. Once per watchlist per 24 hours. Each month includes one free refresh; after that each one costs one image credit.
max_results | int, query | 1 to 500. Default 100. |
scrape_all | bool, query | Default true: active and inactive ads. |
Response: 202 Accepted
{
"watchlist_id": "wl_abc123",
"queued": true,
"job_id": "8f2c...",
"queued_at": "2026-09-30T09:00:00+00:00",
"charged_credits": 0,
"free_refreshes_remaining": 0
}Response: 429 Too Many Requests
{
"detail": {
"error": "Watchlists refresh at most once every 24 hours.",
"last_refreshed_at": "2026-09-30T08:00:00+00:00",
"next_refresh_available_at": "2026-10-01T08:00:00+00:00",
"retry_after_seconds": 82800
}
}What changed
/api/v1/watchlists/diffNew and killed ads across every watchlist since a cutoff, grouped by brand, busiest brand first. The call to poll from a daily job.
since | string | last_24h (default), last_48h, last_7d, last_14d, last_30d, or an ISO 8601 timestamp. |
watchlist_id | string | Limit to one brand. |
max_ads_per_brand | int | 1 to 50. Default 10. Counts are always the full count. |
curl "https://api.brandmov.com/api/v1/watchlists/diff?since=last_7d" \
-H "Authorization: Bearer bm_live_your_key_here"{
"since": "last_7d",
"cutoff_utc": "2026-09-23T09:00:00+00:00",
"total_new_ads": 14,
"total_killed_ads": 6,
"brands_with_changes": 3,
"brands": [
{
"watchlist_id": "wl_abc123",
"page_name": "Example Brand",
"new_ad_count": 9,
"killed_ad_count": 2,
"new_ads": [
{
"ad_id": "1234567890",
"headline": "The 3am wake-up fix",
"media_type": "IMAGE",
"media_url": "https://...",
"video_url": null,
"ad_library_url": "https://www.facebook.com/ads/library/?id=1234567890",
"start_date": "2026-09-28"
}
],
"killed_ads": [{ "...": "same shape, plus end_date" }]
}
]
}An ad counts as killed when it is inactive and its end date falls inside the window.
Winners
/api/v1/watchlists/winners/api/v1/watchlists/{watchlist_id}/winnersAds ranked by how long they have been running, across every brand or for one. Running time is the strongest public signal that an ad is profitable.
top_n | int | Per brand, 1 to 50. Default 10. |
min_running_days | int | Floor. 30 means it survived learning, 90 means proven. |
active_only | bool | Default true. Set false for what worked, not just what is working now. |
{
"watchlist_id": "all",
"filters": { "top_n": 10, "min_running_days": 30, "active_only": true },
"total_winners": 12,
"brands": [
{
"watchlist_id": "wl_abc123",
"page_name": "Example Brand",
"winner_count": 7,
"max_running_days": 214,
"median_running_days": 61,
"winners": [
{
"ad_id": "1234567890",
"headline": "The 3am wake-up fix",
"media_type": "IMAGE",
"start_date": "2026-02-28",
"end_date": null,
"is_active": true,
"running_days": 214
}
]
}
]
}Hooks
/api/v1/watchlists/{watchlist_id}/hooksReads the text on each image and the opening line of the copy, tags each hook by type (question, bold claim, social proof, curiosity, offer, problem agitation) and returns the ranked set with a short playbook.
max_ads | int | Ads to sample, 1 to 20. Default 8. Longest runners first. |
include_ocr | bool | Default true. false reads copy only and is faster. |
active_only | bool | Default true. |
Returns {per_ad[], top_hooks[], commentary}.
Angle shifts
/api/v1/watchlists/{watchlist_id}/anglesCompares the brand's last window_days (7 to 90, default 30) with the window before it and returns newly_ramped, retired and still_running angles. Needs at least three ads in each window; below that you get empty lists and a note instead of a guess.
Strategy teardown
/api/v1/watchlists/{watchlist_id}/strategyA written breakdown of the brand's ads: hooks, offers, audience persona, strengths and weaknesses, and content gaps. Cached after the first call.
Errors
404: watchlist not found on this workspace.400: bad input, or no ads cached yet. Call refresh first.429: refresh within 24 hours of the last one. Honourretry_after_seconds.