curl --request POST \
--url https://api.scrapebento.com/v1/public/scrape/ads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"domain": "<string>",
"wait": true,
"waitTimeoutMs": 123,
"webhookUrl": "<string>",
"idempotencyKey": "<string>",
"cacheTtlSec": 302400,
"fresh": true,
"op": "creatives",
"companyName": "<string>",
"advertiserId": "<string>",
"facebookUrl": "<string>",
"country": "ALL",
"activeStatus": "all",
"cursor": "<string>",
"limit": 30,
"maxLoads": 40,
"floorMonths": 24
}
'import requests
url = "https://api.scrapebento.com/v1/public/scrape/ads"
payload = {
"domain": "<string>",
"wait": True,
"waitTimeoutMs": 123,
"webhookUrl": "<string>",
"idempotencyKey": "<string>",
"cacheTtlSec": 302400,
"fresh": True,
"op": "creatives",
"companyName": "<string>",
"advertiserId": "<string>",
"facebookUrl": "<string>",
"country": "ALL",
"activeStatus": "all",
"cursor": "<string>",
"limit": 30,
"maxLoads": 40,
"floorMonths": 24
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
domain: '<string>',
wait: true,
waitTimeoutMs: 123,
webhookUrl: '<string>',
idempotencyKey: '<string>',
cacheTtlSec: 302400,
fresh: true,
op: 'creatives',
companyName: '<string>',
advertiserId: '<string>',
facebookUrl: '<string>',
country: 'ALL',
activeStatus: 'all',
cursor: '<string>',
limit: 30,
maxLoads: 40,
floorMonths: 24
})
};
fetch('https://api.scrapebento.com/v1/public/scrape/ads', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.scrapebento.com/v1/public/scrape/ads",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'domain' => '<string>',
'wait' => true,
'waitTimeoutMs' => 123,
'webhookUrl' => '<string>',
'idempotencyKey' => '<string>',
'cacheTtlSec' => 302400,
'fresh' => true,
'op' => 'creatives',
'companyName' => '<string>',
'advertiserId' => '<string>',
'facebookUrl' => '<string>',
'country' => 'ALL',
'activeStatus' => 'all',
'cursor' => '<string>',
'limit' => 30,
'maxLoads' => 40,
'floorMonths' => 24
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.scrapebento.com/v1/public/scrape/ads"
payload := strings.NewReader("{\n \"domain\": \"<string>\",\n \"wait\": true,\n \"waitTimeoutMs\": 123,\n \"webhookUrl\": \"<string>\",\n \"idempotencyKey\": \"<string>\",\n \"cacheTtlSec\": 302400,\n \"fresh\": true,\n \"op\": \"creatives\",\n \"companyName\": \"<string>\",\n \"advertiserId\": \"<string>\",\n \"facebookUrl\": \"<string>\",\n \"country\": \"ALL\",\n \"activeStatus\": \"all\",\n \"cursor\": \"<string>\",\n \"limit\": 30,\n \"maxLoads\": 40,\n \"floorMonths\": 24\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.scrapebento.com/v1/public/scrape/ads")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"domain\": \"<string>\",\n \"wait\": true,\n \"waitTimeoutMs\": 123,\n \"webhookUrl\": \"<string>\",\n \"idempotencyKey\": \"<string>\",\n \"cacheTtlSec\": 302400,\n \"fresh\": true,\n \"op\": \"creatives\",\n \"companyName\": \"<string>\",\n \"advertiserId\": \"<string>\",\n \"facebookUrl\": \"<string>\",\n \"country\": \"ALL\",\n \"activeStatus\": \"all\",\n \"cursor\": \"<string>\",\n \"limit\": 30,\n \"maxLoads\": 40,\n \"floorMonths\": 24\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.scrapebento.com/v1/public/scrape/ads")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"domain\": \"<string>\",\n \"wait\": true,\n \"waitTimeoutMs\": 123,\n \"webhookUrl\": \"<string>\",\n \"idempotencyKey\": \"<string>\",\n \"cacheTtlSec\": 302400,\n \"fresh\": true,\n \"op\": \"creatives\",\n \"companyName\": \"<string>\",\n \"advertiserId\": \"<string>\",\n \"facebookUrl\": \"<string>\",\n \"country\": \"ALL\",\n \"activeStatus\": \"all\",\n \"cursor\": \"<string>\",\n \"limit\": 30,\n \"maxLoads\": 40,\n \"floorMonths\": 24\n}"
response = http.request(request)
puts response.read_body{
"data": {
"jobId": "<string>",
"status": "succeeded",
"creditsCharged": 123,
"result": {},
"kind": "url",
"cached": true,
"completedAt": "2023-11-07T05:31:56Z",
"resultTruncated": true,
"pricingDecision": {
"outcome": "complete",
"stage": "admission",
"reason": "<string>",
"unitCount": 24
}
}
}Fetch ad creatives, walk an ad archive, or resolve an advertiser
Requires api.write.
op=creatives (default) returns an advertiser’s current ad creatives
with their media assets, landing URL, delivery dates, and publisher
platforms. op=archive returns their whole back-catalogue instead,
spending many upstream page loads inside the single job. op=resolve
maps a company to platform advertiser ids, returning not_found as a
real, cacheable answer.
The two archives are reached differently. Meta caps every page load at 30 results and exposes no usable cursor, so its walk queries narrower date windows to reach the rest. Google paginates properly, so its walk follows the cursor — the same operation at a fraction of the cost.
Note that Meta discloses spend, impressions and audience breakdowns
only for ads declared political or social-issue (categories other
than UNKNOWN), and reach only for EU-delivered ads. For ordinary
commercial ads those fields are absent on every operation. Google
discloses none of them, and no publisher-platform breakdown either.
Neither platform needs credentials: Meta is served from the public Ad
Library, Google from the Ads Transparency Center’s own endpoint. An
empty creatives list is a real answer — that advertiser is running no
ads.
curl --request POST \
--url https://api.scrapebento.com/v1/public/scrape/ads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"domain": "<string>",
"wait": true,
"waitTimeoutMs": 123,
"webhookUrl": "<string>",
"idempotencyKey": "<string>",
"cacheTtlSec": 302400,
"fresh": true,
"op": "creatives",
"companyName": "<string>",
"advertiserId": "<string>",
"facebookUrl": "<string>",
"country": "ALL",
"activeStatus": "all",
"cursor": "<string>",
"limit": 30,
"maxLoads": 40,
"floorMonths": 24
}
'import requests
url = "https://api.scrapebento.com/v1/public/scrape/ads"
payload = {
"domain": "<string>",
"wait": True,
"waitTimeoutMs": 123,
"webhookUrl": "<string>",
"idempotencyKey": "<string>",
"cacheTtlSec": 302400,
"fresh": True,
"op": "creatives",
"companyName": "<string>",
"advertiserId": "<string>",
"facebookUrl": "<string>",
"country": "ALL",
"activeStatus": "all",
"cursor": "<string>",
"limit": 30,
"maxLoads": 40,
"floorMonths": 24
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
domain: '<string>',
wait: true,
waitTimeoutMs: 123,
webhookUrl: '<string>',
idempotencyKey: '<string>',
cacheTtlSec: 302400,
fresh: true,
op: 'creatives',
companyName: '<string>',
advertiserId: '<string>',
facebookUrl: '<string>',
country: 'ALL',
activeStatus: 'all',
cursor: '<string>',
limit: 30,
maxLoads: 40,
floorMonths: 24
})
};
fetch('https://api.scrapebento.com/v1/public/scrape/ads', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.scrapebento.com/v1/public/scrape/ads",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'domain' => '<string>',
'wait' => true,
'waitTimeoutMs' => 123,
'webhookUrl' => '<string>',
'idempotencyKey' => '<string>',
'cacheTtlSec' => 302400,
'fresh' => true,
'op' => 'creatives',
'companyName' => '<string>',
'advertiserId' => '<string>',
'facebookUrl' => '<string>',
'country' => 'ALL',
'activeStatus' => 'all',
'cursor' => '<string>',
'limit' => 30,
'maxLoads' => 40,
'floorMonths' => 24
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.scrapebento.com/v1/public/scrape/ads"
payload := strings.NewReader("{\n \"domain\": \"<string>\",\n \"wait\": true,\n \"waitTimeoutMs\": 123,\n \"webhookUrl\": \"<string>\",\n \"idempotencyKey\": \"<string>\",\n \"cacheTtlSec\": 302400,\n \"fresh\": true,\n \"op\": \"creatives\",\n \"companyName\": \"<string>\",\n \"advertiserId\": \"<string>\",\n \"facebookUrl\": \"<string>\",\n \"country\": \"ALL\",\n \"activeStatus\": \"all\",\n \"cursor\": \"<string>\",\n \"limit\": 30,\n \"maxLoads\": 40,\n \"floorMonths\": 24\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.scrapebento.com/v1/public/scrape/ads")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"domain\": \"<string>\",\n \"wait\": true,\n \"waitTimeoutMs\": 123,\n \"webhookUrl\": \"<string>\",\n \"idempotencyKey\": \"<string>\",\n \"cacheTtlSec\": 302400,\n \"fresh\": true,\n \"op\": \"creatives\",\n \"companyName\": \"<string>\",\n \"advertiserId\": \"<string>\",\n \"facebookUrl\": \"<string>\",\n \"country\": \"ALL\",\n \"activeStatus\": \"all\",\n \"cursor\": \"<string>\",\n \"limit\": 30,\n \"maxLoads\": 40,\n \"floorMonths\": 24\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.scrapebento.com/v1/public/scrape/ads")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"domain\": \"<string>\",\n \"wait\": true,\n \"waitTimeoutMs\": 123,\n \"webhookUrl\": \"<string>\",\n \"idempotencyKey\": \"<string>\",\n \"cacheTtlSec\": 302400,\n \"fresh\": true,\n \"op\": \"creatives\",\n \"companyName\": \"<string>\",\n \"advertiserId\": \"<string>\",\n \"facebookUrl\": \"<string>\",\n \"country\": \"ALL\",\n \"activeStatus\": \"all\",\n \"cursor\": \"<string>\",\n \"limit\": 30,\n \"maxLoads\": 40,\n \"floorMonths\": 24\n}"
response = http.request(request)
puts response.read_body{
"data": {
"jobId": "<string>",
"status": "succeeded",
"creditsCharged": 123,
"result": {},
"kind": "url",
"cached": true,
"completedAt": "2023-11-07T05:31:56Z",
"resultTruncated": true,
"pricingDecision": {
"outcome": "complete",
"stage": "admission",
"reason": "<string>",
"unitCount": 24
}
}
}Authorizations
Send a workspace API token in the Authorization: Bearer <token> header.
Keys carry the satk_ prefix and spend your organization's credit
balance. GET /v1/public/balance reports what is left.
Test-mode keys (satk_test_), which drew on a separate self-refilling
allowance, have been retired and no longer authenticate.
Headers
Optional deduplication key, scoped to your organization. Re-sending a request with a key that was already used returns the original job instead of creating and charging for a second one.
Supersedes the idempotencyKey body field, which remains supported as
a deprecated alias. If both are present, this header wins.
255Body
- Option 1
- Option 2
- Option 3
At least one of domain, companyName, or advertiserId is
required. An advertiserId is the most precise input and is not
filtered by landing domain.
Company domain. On Meta it attributes keyword-search results to the right advertiser; on Google it IS the query — the Transparency Center is searchable by the domain an ad lands on.
Wait for a terminal response before falling back to async job polling.
Optional client-requested sync wait timeout, bounded by the server maximum.
Optional public callback URL for terminal job delivery. Available to every organization.
Naming a URL here registers it as a webhook endpoint if it is not
registered already, so the signing secret exists before the first
delivery does. Fetch it from
GET /v1/public/webhooks/endpoints/{endpointId}/secret, or register
the endpoint up front with POST /v1/public/webhooks/endpoints to
have the secret in hand before you run anything.
Deliveries carry X-Scrapebento-Signature, an HMAC-SHA256 over
"<timestamp>.<body>" under that endpoint's own secret, so the
secret you need to verify your deliveries cannot forge anyone
else's. Every attempt is recorded in
GET /v1/public/webhooks/deliveries and can be re-sent from there.
Deprecated alias for the Idempotency-Key header, which is the
conventional transport and takes precedence when both are sent.
Still honoured so existing integrations keep working.
255How long this result may be reused, in seconds. Capped at 7 days (604800); larger values are clamped.
Omit it to accept the per-kind server default. Slow-moving kinds
cache by default — infra and brand for 6h, seo, sitemap,
site, and producthunt for 1h, and hn_search, hn_post, and
rss for 5m. Everything else defaults to no caching.
A cached result is charged at the same rate as a fresh scrape, so
the TTL is how stale an answer you can be billed for. Send 0 to
prevent this result being cached, and fresh: true to bypass an
existing cache entry on read.
0 <= x <= 604800Bypass cache and force a new scrape or search execution.
creatives returns an advertiser's current ad creatives.
archive walks their whole back-catalogue, spending many
upstream page loads inside the single job. It has no keyword
fallback, so it needs the identifier that platform's archive is
keyed on: advertiserId for Meta, domain for Google.
resolve maps a company to platform advertiser ids, returning
not_found as a real, cacheable answer.
creatives, archive, resolve Omit on creatives to query every platform and merge the results. archive and resolve each answer about a single platform and default to meta.
meta, google Meta Page id (numeric) or Google advertiser id (AR…). On Meta it skips search entirely and returns that Page's ads; on Google it filters the domain's ads down to that advertiser, which is what keeps affiliates out of the result.
^[A-Za-z0-9_-]{1,64}$Official Facebook Page URL, used as a resolution hint. Must be a facebook.com or fb.com URL.
ISO-3166 alpha-2 code, or ALL for every country.
all, active, inactive Forward cursor from a prior response's nextCursor. Accepted for forward compatibility, but neither platform hands one back in practice: Meta results are read from the Ad Library's server-rendered page, which exposes no usable cursor, and Google follows its own cursor inside the job rather than returning a position that only means anything alongside the region and date bounds that produced it. resultComplete (in both the result body and meta) is false when more ads exist than the response carries — retire ads you did not see only when it is true.
x <= 100op=archive only. Page loads the window walk may spend. One load returns 30 ads, so a back-catalogue of N ads costs roughly N/30 loads plus the splits that find the window boundaries; a 97-ad advertiser measured at ~23. The walk also stops early once a branch stops yielding new ads.
x <= 120op=archive only. How far back the walk reaches. Wider costs proportionally more loads.
x <= 120Response
Ad snapshot completed and returned a terminal response.
- Option 1
- Option 2
Show child attributes
Show child attributes