Introduction
The Metapi API provides programmatic access to the Facebook Ad Library. Search ads by keyword, advertiser, or country. Retrieve ad creatives, spend data, targeting details, and more. The API returns structured JSON (or CSV) responses and supports real-time webhook notifications.
All endpoints are RESTful, use standard HTTP methods, and return consistent response envelopes. Authentication is handled via Bearer tokens in the Authorization header.
Quick Start
Make your first API call in seconds. Replace YOUR_API_KEY with the key from your dashboard.
curl -X GET "https://api.metapi.io/v1/ads/search?q=shopify&country=US" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Authentication
All API requests require a valid API key sent as a Bearer token in the Authorization header. Requests without a valid key will receive a 401 Unauthorized response.
How to get an API key
- Sign up for a Metapi account at metapi.io
- Navigate to the API Keys section in your dashboard
- Click "Create New Key" and give it a descriptive name
- Copy the key immediately -- it will only be shown once
Example Header
Authorization: Bearer mk_live_abc123def456ghi789jkl012mno345
Security Best Practices
- Store your API key in environment variables, never hard-code it in source files
- Never expose your API key in client-side JavaScript or public repositories
- Rotate your keys periodically and revoke any compromised keys immediately
- Use separate API keys for development and production environments
Base URL
All API requests are made to the following base URL. Every endpoint described in this documentation is relative to this base.
https://api.metapi.io/v1
- HTTPS required -- all requests must use HTTPS. Plain HTTP requests will be rejected.
-
JSON content type -- set
Content-Type: application/jsonfor all requests with a body. -
Versioning -- the current version is
v1. Breaking changes will be introduced in a new version. Thev1namespace will remain stable.
Search the Facebook Ad Library by keyword, country, advertiser, date range, and more. Returns a paginated list of ads matching the specified criteria.
Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | Required | Search query. Matches against ad body text, page name, and link URL. |
country |
string | Required | ISO 3166-1 alpha-2 country code (e.g. US, DE, GB). |
ad_type |
string | Optional | Filter by ad type. Values: all (default), political_and_issue_ads, housing_ads, credit_ads. |
advertiser_id |
string | Optional | Filter by advertiser Facebook page ID. |
start_date[min] |
string | Optional | Ads delivered on or after this date. Format: YYYY-MM-DD (e.g. 2024-01-01). |
start_date[max] |
string | Optional | Ads delivered on or before this date. Format: YYYY-MM-DD (e.g. 2024-12-31). |
media_type |
string | Optional | Filter by media type. Values: all (default), image, video, meme. |
active_status |
string | Optional | Filter by ad status. Values: active (default), inactive, all. |
page |
integer | Optional | Page number for pagination. Default: 1. |
format |
string | Optional | Response format. Values: json (default), csv. |
publisher_platforms |
array | Optional | Filter by platform. Values: facebook, instagram, messenger, audience_network, whatsapp. |
content_languages |
array | Optional | Filter by ad text language. Array of language codes (e.g. ["en"]). |
search_type |
string | Optional | Search matching methodology. Values: keyword_unordered (default), keyword_exact_phrase. |
is_targeted_country |
boolean | Optional | Filter ads targeted to the specified country. Default: false. |
sort_data[mode] |
string | Optional | Sort mode. Values: total_impressions (default), relevancy_monthly_grouped. |
sort_data[direction] |
string | Optional | Sort direction. Values: desc (default), asc. |
Request Example
curl -X GET "https://api.metapi.io/v1/ads/search?q=shopify&country=US&max_results=100&sort_data[mode]=total_impressions&sort_data[direction]=desc" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Response Example
{
"total_count": 4328,
"page": 1,
"per_page": 100,
"total_pages": 44,
"data": [
{
"ad_archive_id": "669937005838022",
"collation_count": 1,
"collation_id": "1607325453315755",
"page_id": "1765633470373416",
"page_name": "MINISO",
"is_active": true,
"categories": ["UNKNOWN"],
"start_date": 1760943600,
"end_date": 1770105600,
"start_date_formatted": "2025-10-20 07:00:00",
"end_date_formatted": "2026-02-03 08:00:00",
"publisher_platform": ["FACEBOOK"],
"currency": "",
"spend": null,
"impressions_with_index": {
"impressions_text": null,
"impressions_index": -1
},
"targeted_or_reached_countries": [],
"ad_library_url": "https://www.facebook.com/ads/library/?id=669937005838022",
"snapshot": {
"body": {
"text": "Pink petals, soft smiles, and spring in full swing."
},
"display_format": "IMAGE",
"images": [
{
"original_image_url": "https://scontent-dfw5-1.xx.fbcdn.net/...",
"resized_image_url": "https://scontent-dfw5-3.xx.fbcdn.net/..."
}
],
"videos": [],
"cta_text": "Like Page",
"cta_type": "LIKE_PAGE",
"link_url": "https://www.facebook.com/1765633470373416",
"page_profile_uri": "https://www.facebook.com/miniso/",
"page_profile_picture_url": "https://scontent-dfw5-2.xx.fbcdn.net/...",
"page_categories": ["Product/service"],
"page_like_count": 5955865,
"page_is_deleted": false
},
"regional_regulation_data": {
"finserv": {
"is_deemed_finserv": false,
"is_limited_delivery": false
}
}
},
// ... 99 more ads
]
}
Retrieve all ads for a specific advertiser by their Facebook page ID. Supports filtering by status and sorting options.
Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Required | The Facebook page ID of the advertiser. |
status |
string | Optional | Filter by ad status. Values: active, inactive, all (default). |
page |
integer | Optional | Page number for pagination. Default: 1. |
per_page |
integer | Optional | Number of results per page. Range: 1-1000. Default: 100. |
sort_data[mode] |
string | Optional | Sort mode. Values: total_impressions (default), relevancy_monthly_grouped. |
sort_data[direction] |
string | Optional | Sort direction. Values: desc (default), asc. |
Request Example
curl -X GET "https://api.metapi.io/v1/advertisers/946786242013194/ads?status=active&sort_data[mode]=total_impressions&sort_data[direction]=desc" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Response Example
{
"total_count": 342,
"page": 1,
"per_page": 100,
"total_pages": 4,
"data": [
{
"ad_archive_id": "1122334455667788",
"page_id": "946786242013194",
"page_name": "Shopify",
"is_active": true,
"categories": ["UNKNOWN"],
"start_date": 1762012800,
"end_date": null,
"start_date_formatted": "2025-11-02 00:00:00",
"end_date_formatted": null,
"publisher_platform": ["FACEBOOK", "INSTAGRAM"],
"snapshot": {
"body": {
"text": "Start selling online today. Try Shopify free for 14 days."
},
"display_format": "VIDEO",
"images": [],
"videos": [
{
"video_sd_url": "https://video.xx.fbcdn.net/...",
"video_hd_url": "https://video.xx.fbcdn.net/..."
}
],
"cta_text": "Sign Up",
"cta_type": "SIGN_UP"
}
},
// ... more ads
]
}
Create a new webhook subscription. When matching events occur, Metapi will send an HTTP POST request to your specified URL with the event payload.
Request Body Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
url |
string | Required | The HTTPS URL to receive webhook events. Must be publicly accessible. |
events |
array | Required | List of event types to subscribe to. See Webhook Events for available types. |
filters |
object | Optional | Filter criteria. Accepts country (string), advertiser_id (string), and keyword (string). |
secret |
string | Optional | A secret key used to generate the HMAC-SHA256 signature in the X-Metapi-Signature header. If omitted, one will be generated for you. |
Request Example
curl -X POST "https://api.metapi.io/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/metapi",
"events": ["new_ad", "ad_updated"],
"filters": {
"country": "US",
"advertiser_id": "946786242013194",
"keyword": "shopify"
},
"secret": "whsec_your_signing_secret_here"
}'
Response Example
{
"id": "wh_abc123def456",
"url": "https://your-app.com/webhooks/metapi",
"events": ["new_ad", "ad_updated"],
"filters": {
"country": "US",
"advertiser_id": "946786242013194",
"keyword": "shopify"
},
"secret": "whsec_your_signing_secret_here",
"active": true,
"created_at": "2025-06-15T10:30:00Z"
}
List all webhook subscriptions for your account. Returns an array of webhook objects with their current configuration and status.
Request Example
curl -X GET "https://api.metapi.io/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Response Example
{
"data": [
{
"id": "wh_abc123def456",
"url": "https://your-app.com/webhooks/metapi",
"events": ["new_ad", "ad_updated"],
"filters": {
"country": "US",
"advertiser_id": "946786242013194",
"keyword": "shopify"
},
"active": true,
"created_at": "2025-06-15T10:30:00Z",
"last_triggered_at": "2025-10-14T08:22:15Z"
},
{
"id": "wh_xyz789ghi012",
"url": "https://your-app.com/webhooks/competitor-alerts",
"events": ["new_ad", "creative_changed"],
"filters": {
"country": "DE",
"keyword": "saas"
},
"active": true,
"created_at": "2025-07-20T14:00:00Z",
"last_triggered_at": "2025-10-13T19:45:30Z"
}
]
}
Delete a webhook subscription. The webhook will stop receiving events immediately. This action cannot be undone.
Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Required | The webhook ID (e.g. wh_abc123def456). |
Request Example
curl -X DELETE "https://api.metapi.io/v1/webhooks/wh_abc123def456" \
-H "Authorization: Bearer YOUR_API_KEY"
Response
Returns 204 No Content on success with an empty response body.
Webhook Events
Metapi sends webhook events when specific changes occur in the ad library. Subscribe to one or more event types when creating a webhook.
Event Types
| Event | Description |
|---|---|
new_ad |
Triggered when a new ad is detected that matches your filters. |
ad_updated |
Triggered when an existing ad's metadata changes (status, targeting, etc.). |
ad_removed |
Triggered when an ad is removed from the library or marked inactive. |
creative_changed |
Triggered when the ad creative (image, video, copy) is modified. |
spend_change |
Triggered when the spend or impressions data is updated for an ad. |
Signature Verification
Every webhook request includes an X-Metapi-Signature header containing an HMAC-SHA256 signature of the request body, signed with the secret you provided (or the one Metapi generated for you). Always verify this signature before processing the payload.
# Verify the webhook signature
import hmac
import hashlib
def verify_signature(payload_body, signature_header, secret):
expected = hmac.new(
secret.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature_header)
Webhook Payload Example
// Headers
Content-Type: application/json
X-Metapi-Signature: sha256=a1b2c3d4e5f6...
X-Metapi-Event: new_ad
X-Metapi-Delivery: evt_98765432
// Body
{
"id": "evt_98765432",
"event": "new_ad",
"created_at": "2025-10-15T14:30:00Z",
"data": {
"ad_archive_id": "998877665544332",
"page_id": "946786242013194",
"page_name": "Shopify",
"is_active": true,
"start_date": 1762012800,
"start_date_formatted": "2025-11-02 00:00:00",
"publisher_platform": ["FACEBOOK", "INSTAGRAM"],
"snapshot": {
"body": {
"text": "Launch your online store in minutes."
},
"display_format": "VIDEO",
"cta_text": "Sign Up",
"cta_type": "SIGN_UP"
}
}
}
Response Format
All list endpoints return responses in a standard envelope format. This provides consistent pagination metadata alongside the results data array.
Envelope Fields
| Field | Type | Description |
|---|---|---|
total_count |
integer | Total number of records matching the query across all pages. |
page |
integer | Current page number (1-indexed). |
per_page |
integer | Number of results returned per page. |
total_pages |
integer | Total number of pages available. |
data |
array | Array of result objects (ads, advertisers, etc.). |
Example
{
"total_count": 4328,
"page": 1,
"per_page": 100,
"total_pages": 44,
"data": [
// ... result objects
]
}
Ad Object Reference
Each ad object contains comprehensive metadata, creative content, and delivery information. Below is a complete reference of all fields.
Top-Level Fields
| Field | Type | Description |
|---|---|---|
ad_archive_id |
string | Unique identifier for the ad in the Facebook Ad Library. |
page_id |
string | Facebook page ID of the advertiser. |
page_name |
string | Display name of the advertiser's Facebook page. |
is_active |
boolean | Whether the ad is currently active and running. |
categories |
array | Ad categories assigned by Facebook (e.g. UNKNOWN, EMPLOYMENT, HOUSING). |
start_date |
integer | Unix timestamp when the ad started running. |
end_date |
integer | Unix timestamp when the ad stopped or is scheduled to stop. Null if still running. |
start_date_formatted |
string | Human-readable start date (e.g. 2025-10-20 07:00:00). |
end_date_formatted |
string | Human-readable end date. Null if still running. |
publisher_platform |
array | Platforms where the ad is running. Values: FACEBOOK, INSTAGRAM, MESSENGER, AUDIENCE_NETWORK. |
currency |
string | ISO 4217 currency code for spend data. Empty string if not available. |
spend |
object|null | Spend range with lower_bound and upper_bound fields. Null if not available. |
impressions_with_index |
object | Impressions data with impressions_text (string) and impressions_index (integer). |
targeted_or_reached_countries |
array | List of ISO country codes where the ad was targeted or reached users. |
collation_count |
integer | Number of ad variations in this collation group. |
collation_id |
string | Identifier linking ad variations within the same campaign. |
ad_library_url |
string | Direct URL to view this ad in the Facebook Ad Library. |
Snapshot Sub-Object
The snapshot object contains the ad creative content, page information, and call-to-action details.
| Field | Type | Description |
|---|---|---|
body.text |
string | The primary text content of the ad creative. |
display_format |
string | Creative format. Values: IMAGE, VIDEO, DCO, CAROUSEL. |
images |
array | Array of image objects with original_image_url and resized_image_url. |
videos |
array | Array of video objects with video_sd_url and video_hd_url. |
cta_text |
string | Call-to-action button text (e.g. "Shop Now", "Learn More"). |
cta_type |
string | CTA type identifier (e.g. SHOP_NOW, LEARN_MORE, SIGN_UP). |
link_url |
string | Destination URL the ad links to. |
page_profile_uri |
string | Facebook page URL of the advertiser. |
page_profile_picture_url |
string | URL of the advertiser's profile picture. |
page_categories |
array | Facebook page categories (e.g. ["Product/service"]). |
page_like_count |
integer | Number of likes on the advertiser's Facebook page. |
page_is_deleted |
boolean | Whether the advertiser's page has been deleted. |
Regional Regulation Data
The regional_regulation_data sub-object contains region-specific regulatory flags. This includes financial services disclaimers (finserv) and anti-scam indicators (tw_anti_scam). Each regulatory section contains boolean fields like is_deemed_finserv and is_limited_delivery indicating the ad's regulatory status in different jurisdictions.
Pagination
The Metapi API uses page-based pagination. All list endpoints accept page and per_page parameters and return pagination metadata in the response envelope.
Pagination Field Reference
| Parameter | Type | Description |
|---|---|---|
page |
integer | Page number to retrieve. Starts at 1. |
per_page |
integer | Number of results per page. Maximum: 1000. Default varies by endpoint. |
Response Fields
| Field | Type | Description |
|---|---|---|
total_count |
integer | Total matching records across all pages. |
page |
integer | Current page number. |
per_page |
integer | Number of results on this page. |
total_pages |
integer | Total number of pages. |
Python Iteration Example
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.metapi.io/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
all_ads = []
page = 1
while True:
response = requests.get(
f"{BASE_URL}/ads/search",
headers=headers,
params={
"q": "shopify",
"country": "US",
"per_page": 1000,
"page": page
}
)
data = response.json()
all_ads.extend(data["data"])
# Stop when we reach the last page
if page >= data["total_pages"]:
break
page += 1
print(f"Fetched {len(all_ads)} ads in total")
Error Codes
The Metapi API uses standard HTTP status codes to indicate the success or failure of a request. Error responses include a JSON body with a type and message for debugging.
HTTP Status Codes
| Code | Type | Description |
|---|---|---|
400 |
bad_request |
The request was malformed or missing required parameters. |
401 |
unauthorized |
No valid API key was provided in the Authorization header. |
403 |
forbidden |
The API key does not have permission to access the requested resource. |
404 |
not_found |
The requested resource (ad, advertiser, webhook) does not exist. |
409 |
conflict |
A webhook with the same URL and event configuration already exists. |
422 |
unprocessable_entity |
The request body was valid JSON but contained invalid field values. |
429 |
rate_limited |
Too many requests. Check the rate limit headers and retry after the reset time. |
500 |
internal_error |
An unexpected error occurred on our servers. Contact support if this persists. |
503 |
service_unavailable |
The API is temporarily unavailable due to maintenance or high load. Retry shortly. |
Error Response Format
{
"error": {
"type": "unauthorized",
"message": "Invalid or expired API key. Please check your Authorization header.",
"status": 401,
"request_id": "req_abc123xyz789"
}
}
Rate Limits
Rate limits are applied per API key and vary by plan tier. All responses include rate limit headers so you can track your usage in real time.
Limits by Plan
| Plan | Requests / Minute | Requests / Day |
|---|---|---|
| Free Trial | 100 |
1,000 |
| Starter | 300 |
10,000 |
| Growth | 600 |
50,000 |
| Enterprise | 1,200 |
Unlimited |
Rate Limit Headers
Every API response includes the following headers to help you manage your request rate.
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum number of requests allowed per minute for your plan. |
X-RateLimit-Remaining |
Number of requests remaining in the current rate limit window. |
X-RateLimit-Reset |
Unix timestamp when the current rate limit window resets. |
429 Response Example
// Response Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1697299260
Retry-After: 45
// Response Body
{
"error": {
"type": "rate_limited",
"message": "Rate limit exceeded. Please retry after 45 seconds.",
"status": 429,
"request_id": "req_rl_001abc"
}
}
cURL Examples
Quick command-line examples using cURL. Replace YOUR_API_KEY with your actual API key.
Search Ads
# Search for Shopify ads in the US, sorted by newest first
curl -X GET "https://api.metapi.io/v1/ads/search?q=shopify&country=US&sort_data[mode]=relevancy_monthly_grouped&sort_data[direction]=desc&max_results=100" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
# Search with date range and media type filter
curl -X GET "https://api.metapi.io/v1/ads/search?q=fitness&country=GB&media_type=video&start_date[min]=2025-01-01&start_date[max]=2025-06-30" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
# Search for a specific advertiser's ads, including inactive
curl -X GET "https://api.metapi.io/v1/ads/search?q=&country=US&advertiser_id=946786242013194&active_status=all" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Get Ad by ID
# Get a specific ad by its archive ID
curl -X GET "https://api.metapi.io/v1/ads/669937005838022" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Search Advertisers
# Search for advertisers matching "nike" in Germany
curl -X GET "https://api.metapi.io/v1/advertisers/search?q=nike&country=DE&per_page=10" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Python Examples
Examples using the requests library. Install it with pip install requests.
Basic Search
import os
import requests
API_KEY = os.environ["METAPI_API_KEY"]
BASE_URL = "https://api.metapi.io/v1"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# Search for ads
response = requests.get(
f"{BASE_URL}/ads/search",
headers=headers,
params={
"q": "shopify",
"country": "US",
"max_results": 100,
"sort_data[mode]": "total_impressions",
"sort_data[direction]": "desc"
}
)
data = response.json()
print(f"Found {data['total_count']} ads")
for ad in data["data"]:
print(f" [{ad['ad_archive_id']}] {ad['page_name']} - {ad['snapshot']['body']['text'][:60]}...")
Pagination Iteration
import os
import requests
import time
API_KEY = os.environ["METAPI_API_KEY"]
BASE_URL = "https://api.metapi.io/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
def fetch_all_ads(query, country, per_page=1000):
"""Fetch all ads matching the query, handling pagination automatically."""
all_ads = []
page = 1
while True:
response = requests.get(
f"{BASE_URL}/ads/search",
headers=headers,
params={
"q": query,
"country": country,
"per_page": per_page,
"page": page
}
)
response.raise_for_status()
data = response.json()
all_ads.extend(data["data"])
print(f" Page {page}/{data['total_pages']} - fetched {len(data['data'])} ads")
if page >= data["total_pages"]:
break
page += 1
# Respect rate limits
time.sleep(0.2)
return all_ads
# Usage
ads = fetch_all_ads("ecommerce", "US")
print(f"Total ads fetched: {len(ads)}")
Error Handling
import os
import requests
import time
API_KEY = os.environ["METAPI_API_KEY"]
BASE_URL = "https://api.metapi.io/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
def make_request(url, params=None, max_retries=3):
"""Make an API request with retry logic and error handling."""
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
return response.json()
elif response.status_code == 429:
# Rate limited - wait and retry
retry_after = int(response.headers.get("Retry-After", 60))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
continue
elif response.status_code == 401:
raise Exception("Invalid API key. Check your METAPI_API_KEY.")
elif response.status_code >= 500:
# Server error - retry with backoff
wait = 2 ** attempt
print(f"Server error {response.status_code}. Retrying in {wait}s...")
time.sleep(wait)
continue
else:
error = response.json().get("error", {})
raise Exception(f"API error: {error.get('message', 'Unknown error')}")
except requests.exceptions.ConnectionError:
wait = 2 ** attempt
print(f"Connection failed. Retrying in {wait}s...")
time.sleep(wait)
raise Exception(f"Failed after {max_retries} retries")
# Usage
data = make_request(f"{BASE_URL}/ads/search", {"q": "shopify", "country": "US"})
print(f"Found {data['total_count']} ads")
JavaScript Examples
Examples using the native fetch API and axios. Both work in Node.js and modern browsers (though API keys should never be exposed client-side).
Fetch API
const API_KEY = process.env.METAPI_API_KEY;
const BASE_URL = "https://api.metapi.io/v1";
async function searchAds(query, country, options = {}) {
const params = new URLSearchParams({
q: query,
country: country,
max_results: options.maxResults || 100,
"sort_data[mode]": options.sortMode || "total_impressions",
"sort_data[direction]": options.sortDirection || "desc",
page: options.page || 1,
...options.filters
});
const response = await fetch(`${BASE_URL}/ads/search?${params}`, {
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.error.message}`);
}
return response.json();
}
// Usage
const data = await searchAds("shopify", "US", { maxResults: 100, sortMode: "relevancy_monthly_grouped" });
console.log(`Found ${data.total_count} ads`);
data.data.forEach(ad => {
console.log(` [${ad.ad_archive_id}] ${ad.page_name}`);
});
Axios
const axios = require("axios");
const client = axios.create({
baseURL: "https://api.metapi.io/v1",
headers: {
"Authorization": `Bearer ${process.env.METAPI_API_KEY}`,
"Content-Type": "application/json"
}
});
// Search ads
async function searchAds(query, country) {
const { data } = await client.get("/ads/search", {
params: { q: query, country, max_results: 100 }
});
console.log(`Found ${data.total_count} ads across ${data.total_pages} pages`);
return data;
}
// Get a single ad
async function getAd(adArchiveId) {
const { data } = await client.get(`/ads/${adArchiveId}`);
return data;
}
// Search advertisers
async function searchAdvertisers(query, country) {
const { data } = await client.get("/advertisers/search", {
params: { q: query, country }
});
return data;
}
// Usage
(async () => {
const results = await searchAds("shopify", "US");
const ad = await getAd(results.data[0].ad_archive_id);
console.log("Ad details:", ad.page_name, ad.snapshot.body.text);
})();
Express Webhook Receiver
const express = require("express");
const crypto = require("crypto");
const app = express();
const WEBHOOK_SECRET = process.env.METAPI_WEBHOOK_SECRET;
// Parse raw body for signature verification
app.use("/webhooks/metapi", express.raw({ type: "application/json" }));
// Verify HMAC-SHA256 signature
function verifySignature(payload, signature) {
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature)
);
}
app.post("/webhooks/metapi", (req, res) => {
const signature = req.headers["x-metapi-signature"];
const event = req.headers["x-metapi-event"];
// Verify the webhook signature
if (!signature || !verifySignature(req.body, signature)) {
console.error("Invalid webhook signature");
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body);
// Handle different event types
switch (event) {
case "new_ad":
console.log("New ad detected:", payload.data.ad_archive_id);
console.log(" Advertiser:", payload.data.page_name);
console.log(" Text:", payload.data.snapshot.body.text);
break;
case "ad_updated":
console.log("Ad updated:", payload.data.ad_archive_id);
break;
case "ad_removed":
console.log("Ad removed:", payload.data.ad_archive_id);
break;
case "creative_changed":
console.log("Creative changed:", payload.data.ad_archive_id);
break;
case "spend_change":
console.log("Spend updated:", payload.data.ad_archive_id);
break;
default:
console.log("Unknown event:", event);
}
// Acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});