API Documentation
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 POST "https://api.metapi.io/v1/tasks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q": "shopify", "country": "US"}'
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.
Create a new scraping task. Performs a keyword search or advertiser search depending on the parameters provided. Returns a task ID that you can poll for progress and results.
Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | Required* | Search query. Matches against ad body text, page name, and link URL. Required for keyword search, not needed when advertiser_id is provided. |
country |
string | Required* | ISO 3166-1 alpha-2 country code (e.g. US, DE, GB), or ALL for all countries. Required for keyword search. Optional for advertiser search (defaults to ALL). |
advertiser_id |
string | Optional | Facebook advertiser/page ID. When provided, returns all ads from that advertiser — q is not required. Defaults: country=ALL, active_status=all. |
count |
integer | Optional | Number of results (1-50,000, default 50). |
ad_type |
string | Optional | Filter by ad type. Values: all (default), political_and_issue_ads, housing_ads, credit_ads. |
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. |
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 Examples
Keyword search — find ads matching a query:
curl -X POST "https://api.metapi.io/v1/tasks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "shopify",
"country": "US",
"count": 100,
"sort_data": {
"mode": "relevancy_monthly_grouped",
"direction": "desc"
}
}'
Advertiser search — get all ads from a specific Facebook page (no q needed):
curl -X POST "https://api.metapi.io/v1/tasks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiser_id": "946786242013194",
"count": 500
}'
Response Example
{
"task_id": "abc123",
"status": "running",
"message": "Task created. Poll GET /v1/tasks/abc123/status for progress."
}
Check the status and progress of a task. Returns real-time progress information including parsed items count and completion percentage.
Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
uuid |
string | Required | The task ID returned from the Create Task endpoint. |
Response Fields
| Field | Type | Description |
|---|---|---|
task_id |
string | Unique identifier of the task. |
status |
string | Current task status. Values: running, succeeded, failed, timed_out, aborted. |
results_count |
integer | Total number of results collected so far. |
progress_percent |
integer | Completion percentage (0-100). |
items_parsed |
integer | Number of items successfully parsed. |
stage |
string | Current processing stage (e.g. scraping, parsing, finalizing). |
started_at |
string | ISO 8601 timestamp when the task started. |
finished_at |
string|null | ISO 8601 timestamp when the task finished. Null if still running. |
duration_seconds |
number|null | Total duration in seconds. Null if still running. |
error_message |
string|null | Error description if the task failed. Null otherwise. |
Request Example
curl -X GET "https://api.metapi.io/v1/tasks/abc123/status" \
-H "Authorization: Bearer YOUR_API_KEY"
Response Example
{
"task_id": "abc123",
"status": "running",
"results_count": 47,
"progress_percent": 47,
"items_parsed": 47,
"stage": "scraping",
"started_at": "2026-02-20T10:30:00Z",
"finished_at": null,
"duration_seconds": null,
"error_message": null
}
Retrieve the ads collected by a task. Available when the task has reached a terminal status (succeeded, failed, timed_out, or aborted). Returns a paginated list of ad objects in Tyver flat format (see Ad Object Reference). Use Task Status to check when results are ready.
Path and query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
uuid |
string | Required | The task ID from Create Task. |
offset |
integer | Optional | Zero-based index of the first item. Default: 0. See Pagination. |
limit |
integer | Optional | Maximum items per page (default 100, max typically 500). |
Response
Same structure as Response Format: data is an array of ad objects (each matches the Ad Object Reference), and pagination contains total, offset, limit, and has_more.
Request Example
curl -X GET "https://api.metapi.io/v1/tasks/abc123/results?offset=0&limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"
Response Example
{
"data": [
{
"provider_id": 669937005838022,
"provider_page_id": "1765633470373416",
"provider_page_name": "MINISO",
"cta_text": "Like Page",
"bodies": ["Pink petals, soft smiles, and spring in full swing."],
"captions": ["miniso.com"],
"original_image_url": "https://scontent-dfw5-1.xx.fbcdn.net/v/...",
"creation_time": "2025-10-20",
"delivery_start_time": "2025-10-20",
"delivery_stop_time": "2026-02-03"
}
],
"pagination": {
"total": 150,
"offset": 0,
"limit": 10,
"has_more": true
}
}
Each item in data is a full ad object in Tyver format; only a subset of fields is shown above. See Ad Object Reference for all fields.
Stop a running task. Returns 409 Conflict if the task has already finished. Partial results collected before stopping are preserved.
Field Reference
| Parameter | Type | Required | Description |
|---|---|---|---|
uuid |
string | Required | The task ID returned from the Create Task endpoint. |
Request Example
curl -X POST "https://api.metapi.io/v1/tasks/abc123/stop" \
-H "Authorization: Bearer YOUR_API_KEY"
Response Examples
{
"task_id": "abc123",
"status": "aborted",
"message": "Task stopped successfully."
}
{
"error": {
"code": "conflict",
"message": "Task is already finished"
}
}
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 | Optional criteria stored with the webhook. Accepts country (string), advertiser_id (string), and keyword (string). Filters are persisted but are not currently applied when delivering events — all subscribed events are sent regardless of run parameters. You can use this field for your own labeling or future compatibility. |
signing_secret |
string | Optional | A signing key used to generate the HMAC-SHA256 signature in the X-Metapi-Signature header. If omitted, a random 64-character hex string 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": ["run.completed", "run.failed"],
"filters": {
"country": "US",
"advertiser_id": "946786242013194",
"keyword": "shopify"
},
"signing_secret": "whsec_your_signing_secret_here"
}'
Response Example
{
"data": {
"webhook_id": "wh_abc123def456",
"url": "https://your-app.com/webhooks/metapi",
"events": ["run.completed", "run.failed"],
"filters": {
"country": "US",
"advertiser_id": "946786242013194",
"keyword": "shopify"
},
"signing_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": [
{
"webhook_id": "wh_abc123def456",
"url": "https://your-app.com/webhooks/metapi",
"events": ["run.completed", "run.failed"],
"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"
},
{
"webhook_id": "wh_xyz789ghi012",
"url": "https://your-app.com/webhooks/competitor-alerts",
"events": ["run.completed", "run.progress"],
"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 |
|---|---|---|---|
webhook_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 the status of your search tasks changes. Subscribe to one or more event types when creating a webhook.
Event Types
| Event | Description |
|---|---|
run.completed |
Triggered when a search task finishes successfully. The payload includes the task ID and results count. |
run.failed |
Triggered when a search task fails due to an error or timeout. Partial results may still be available. |
run.progress |
Triggered periodically as a search task collects ads. Includes current progress percentage and items parsed. |
Signature Verification
Every webhook request includes an X-Metapi-Signature header containing the raw hex digest of an HMAC-SHA256 signature of the request body (no sha256= prefix). The header is signed with your signing_secret. If you did not provide one when creating the webhook, Metapi generated it for you. Always verify this signature before processing the payload.
# Verify the webhook signature (header value is raw hex, no sha256= prefix)
import hmac
import hashlib
def verify_signature(payload_body, signature_header, secret):
# payload_body: raw request body as bytes; signature_header: X-Metapi-Signature value (hex string)
if isinstance(payload_body, str):
payload_body = payload_body.encode("utf-8")
expected = hmac.new(
secret.encode("utf-8"),
payload_body,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
Delivery Headers
Each delivery sends only these headers. No X-Metapi-Delivery or other delivery-ID header is sent.
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Metapi-Signature |
Raw hex HMAC-SHA256 of the request body (see Signature Verification above). |
X-Metapi-Event |
Event type (e.g. run.completed, run.failed, run.progress). |
X-Metapi-Timestamp |
Unix timestamp (seconds since epoch) when the webhook was sent. Use it for replay protection or logging. |
Webhook Payload Example
// Headers
Content-Type: application/json
X-Metapi-Signature: a1b2c3d4e5f6...
X-Metapi-Event: run.completed
X-Metapi-Timestamp: 1740700200
// Body
{
"event": "run.completed",
"run_id": "abc123def456ghij",
"status": "succeeded",
"results_count": 150,
"finished_at": "2026-02-28T12:00:00Z"
}
Response Format
All list endpoints return responses in a standard envelope format with offset-based pagination. The response has a data array and a pagination object.
Envelope Fields
| Field | Type | Description |
|---|---|---|
data |
array | Array of result objects (ads, advertisers, etc.) for the current window. |
pagination |
object | Pagination metadata: total, offset, limit, has_more. |
pagination.total |
integer | Total number of records matching the query. |
pagination.offset |
integer | Zero-based offset of the first item in this response. |
pagination.limit |
integer | Maximum number of items requested per request. |
pagination.has_more |
boolean | Whether more results are available (offset + limit < total). |
Example
{
"data": [
// ... result objects
],
"pagination": {
"total": 4328,
"offset": 0,
"limit": 100,
"has_more": true
}
}
Ad Object Reference
Ad objects are returned in Tyver flat format: a single-level structure with no nested snapshot. Each field may be null if not provided by the parser. Below is the full field reference.
Ad Object Fields
| Field | Type | Description |
|---|---|---|
provider_id |
integer | Unique identifier for the ad in the Facebook Ad Library. |
provider_page_id |
string | Facebook page ID of the advertiser. |
provider_page_name |
string | Display name of the advertiser's Facebook page. |
query_params |
string | Search query parameters that returned this ad. |
cta_text |
string | Call-to-action button text (e.g. "Shop Now", "Learn More"). |
page_profile_id |
string | Facebook page profile ID (not URL). |
languages |
array | Language codes for the ad (e.g. ["EN"]). |
countries |
array | ISO country codes where the ad was targeted or reached. |
bodies |
array of strings | Primary text content of the ad creative (multiple variants). |
captions |
array of strings | Captions or link captions (e.g. domain). |
creative_link_titles |
array of strings | Link title text. |
creative_link_descriptions |
array of strings | Link description text. |
creative_link_captions |
array of strings | Link caption text. |
video_hd_url |
string | URL of the ad video (HD). Null for image-only ads. |
video_sd_url |
string | URL of the ad video (SD). Null for image-only ads. |
original_image_url |
string | URL of the ad image. Null for video-only ads. |
age_country_gender_reach_breakdown |
array of objects | Demographic reach (e.g. country, gender, age_range, reach). |
beneficiary_payers |
object | Sponsor / payer information when available. |
creation_time |
string (date) | When the ad was created (e.g. 2026-01-15). |
delivery_start_time |
string (date) | When the ad started delivering. |
delivery_stop_time |
string (date) | When the ad stopped or is scheduled to stop. Null if still running. |
gender |
string | Gender targeting if applicable. Null if not set. |
age_from |
integer | Minimum age for targeting. Null if not set. |
age_until |
integer | Maximum age for targeting. Null if not set. |
created_at |
string (timestamp) | When the record was stored by the parser. |
updated_at |
string (timestamp) | When the record was last updated by the parser. |
Pagination
The Metapi API uses offset-based pagination. List endpoints accept offset and limit and return a data array plus a pagination object with total, offset, limit, and has_more.
Request Parameters
| Parameter | Type | Description |
|---|---|---|
offset |
integer | Zero-based index of the first item to return. Default: 0. |
limit |
integer | Maximum number of items to return. Default varies by endpoint (e.g. 100). |
Response Fields
Pagination metadata is returned inside the pagination object (see Response Format).
| Field | Type | Description |
|---|---|---|
pagination.total |
integer | Total matching records. |
pagination.offset |
integer | Offset used for this response. |
pagination.limit |
integer | Limit used for this response. |
pagination.has_more |
boolean | true if offset + limit < total. |
Python Task Polling Example
import requests
import time
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.metapi.io/v1"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# Create a task with a large count
response = requests.post(
f"{BASE_URL}/tasks",
headers=headers,
json={
"q": "shopify",
"country": "US",
"count": 50000
}
)
task_id = response.json()["task_id"]
# Poll until the task finishes
while True:
status = requests.get(
f"{BASE_URL}/tasks/{task_id}/status",
headers=headers
).json()
print(f"Progress: {status['progress_percent']}% ({status['items_parsed']} items)")
if status["status"] in ("succeeded", "failed", "timed_out", "aborted"):
break
time.sleep(2)
print(f"Fetched {status['results_count']} 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 code and message for debugging.
HTTP Status Codes
| Code | Error Code | Description |
|---|---|---|
400 |
bad_request |
The request was malformed or missing required parameters. |
401 |
unauthorized |
No valid API key was provided in the Authorization header. |
402 |
quota_exceeded |
Your account has exceeded its monthly record limit. Upgrade your plan or wait for the next billing cycle. |
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": {
"code": "unauthorized",
"message": "Invalid or missing API key"
}
}
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 |
120 |
1,000 |
lite |
250 |
5,000 |
pro |
500 |
25,000 |
business |
750 |
50,000 |
scale |
1,000 |
100,000 |
enterprise |
1,500 |
Unlimited |
Record Limits
In addition to request rate limits, each plan has a monthly cap on the total number of records your account can retrieve. A record is one ad object returned in task results. Record limits reset at the start of each billing cycle (monthly).
| Plan | Records / Month |
|---|---|
free |
3,000 * |
lite |
25,000 ** |
pro |
100,000 ** |
business |
500,000 ** |
scale |
2,000,000 ** |
enterprise |
Unlimited |
* Free plan has a hard block at its record limit -- requests are rejected once the cap is reached. Upgrade to a paid plan for overage billing.
** Paid plans (Lite through Scale) allow overage beyond the monthly cap. Overage rates: Lite $1.00/1K, Pro $0.80/1K, Business $0.30/1K, Scale $0.15/1K records.
402 Quota Exceeded Response
When your account exceeds its record limit, the API returns a 402 response with the quota_exceeded error code. The message includes your current usage and limit.
{
"error": {
"code": "quota_exceeded",
"message": "You have exceeded your plan's record limit (3,200/3,000). Please upgrade your plan."
}
}
Rate Limit Headers
Every API response includes the following headers to help you manage your request rate.
| Header | Description |
|---|---|
X-RateLimit-Limit-Minute |
Maximum number of requests allowed per minute for your plan. |
X-RateLimit-Remaining-Minute |
Number of requests remaining in the current minute window. |
X-RateLimit-Limit-Day |
Maximum number of requests allowed per day for your plan. |
X-RateLimit-Remaining-Day |
Number of requests remaining in the current day window. |
429 Response Example
// Response Headers
X-RateLimit-Limit-Minute: 120
X-RateLimit-Remaining-Minute: 0
X-RateLimit-Limit-Day: 1000
X-RateLimit-Remaining-Day: 432
Retry-After: 45
// Response Body
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Please retry after 45 seconds."
}
}
cURL Examples
Quick command-line examples using cURL. Replace YOUR_API_KEY with your actual API key.
Create a Keyword Search Task
# Create a keyword search task for Shopify ads in the US
curl -X POST "https://api.metapi.io/v1/tasks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q": "shopify", "country": "US", "count": 100, "sort_data": {"mode": "relevancy_monthly_grouped", "direction": "desc"}}'
Create an Advertiser Search Task
# Create an advertiser search task by page ID
curl -X POST "https://api.metapi.io/v1/tasks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"advertiser_id": "946786242013194", "country": "US"}'
Check Task Status
# Check the status of a running task
curl -X GET "https://api.metapi.io/v1/tasks/abc123/status" \
-H "Authorization: Bearer YOUR_API_KEY"
Stop a Running Task
# Stop a running task
curl -X POST "https://api.metapi.io/v1/tasks/abc123/stop" \
-H "Authorization: Bearer YOUR_API_KEY"
Python Examples
Examples using the requests library. Install it with pip install requests.
Create Task and Poll for Results
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}",
"Content-Type": "application/json"
}
# Create a keyword search task
response = requests.post(
f"{BASE_URL}/tasks",
headers=headers,
json={
"q": "shopify",
"country": "US",
"count": 100,
"sort_data": {
"mode": "total_impressions",
"direction": "desc"
}
}
)
task = response.json()
task_id = task["task_id"]
print(f"Task created: {task_id}")
# Poll for task completion
while True:
status_response = requests.get(
f"{BASE_URL}/tasks/{task_id}/status",
headers=headers
)
status = status_response.json()
print(f" Status: {status['status']} - {status['progress_percent']}% ({status['items_parsed']} items)")
if status["status"] in ("succeeded", "failed", "timed_out", "aborted"):
break
time.sleep(2)
print(f"Task finished with {status['results_count']} results")
Advertiser Search
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}",
"Content-Type": "application/json"
}
def create_and_wait(payload, poll_interval=2):
"""Create a task and wait for it to complete."""
# Create the task
response = requests.post(f"{BASE_URL}/tasks", headers=headers, json=payload)
response.raise_for_status()
task_id = response.json()["task_id"]
print(f"Task created: {task_id}")
# Poll until finished
while True:
status = requests.get(f"{BASE_URL}/tasks/{task_id}/status", headers=headers).json()
print(f" {status['status']} - {status['progress_percent']}%")
if status["status"] in ("succeeded", "failed", "timed_out", "aborted"):
return status
time.sleep(poll_interval)
# Usage: search by advertiser page ID
result = create_and_wait({
"advertiser_id": "946786242013194",
"country": "US",
"count": 500
})
print(f"Total results: {result['results_count']}")
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}/tasks/{task_id}/status")
print(f"Found {data['results_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";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
};
async function createTask(params) {
const response = await fetch(`${BASE_URL}/tasks`, {
method: "POST",
headers,
body: JSON.stringify(params)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.error.message}`);
}
return response.json();
}
async function getTaskStatus(taskId) {
const response = await fetch(`${BASE_URL}/tasks/${taskId}/status`, { headers });
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.error.message}`);
}
return response.json();
}
async function waitForTask(taskId, pollInterval = 2000) {
while (true) {
const status = await getTaskStatus(taskId);
console.log(` ${status.status} - ${status.progress_percent}% (${status.items_parsed} items)`);
if (["succeeded", "failed", "timed_out", "aborted"].includes(status.status)) {
return status;
}
await new Promise(r => setTimeout(r, pollInterval));
}
}
// Usage
const task = await createTask({ q: "shopify", country: "US", count: 100 });
console.log(`Task created: ${task.task_id}`);
const result = await waitForTask(task.task_id);
console.log(`Finished with ${result.results_count} results`);
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"
}
});
// Create a keyword search task
async function createKeywordTask(query, country, count = 100) {
const { data } = await client.post("/tasks", {
q: query, country, count
});
console.log(`Task created: ${data.task_id}`);
return data;
}
// Create an advertiser search task
async function createAdvertiserTask(advertiserId, country) {
const { data } = await client.post("/tasks", {
advertiser_id: advertiserId, country
});
return data;
}
// Check task status
async function getTaskStatus(taskId) {
const { data } = await client.get(`/tasks/${taskId}/status`);
return data;
}
// Stop a running task
async function stopTask(taskId) {
const { data } = await client.post(`/tasks/${taskId}/stop`);
return data;
}
// Usage
(async () => {
const task = await createKeywordTask("shopify", "US", 200);
const status = await getTaskStatus(task.task_id);
console.log("Task status:", status.status, `${status.progress_percent}%`);
})();
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(expected, "hex"),
Buffer.from(signature, "hex")
);
}
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 "run.completed":
console.log("Task completed:", payload.run_id);
console.log(" Results:", payload.results_count);
break;
case "run.failed":
console.log("Task failed:", payload.run_id);
console.log(" Status:", payload.status);
break;
case "run.progress":
console.log("Task progress:", payload.run_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");
});
Need Help?
If you have questions about the API, need help with integration, or want to discuss your use case, reach out to us at metapi.main@gmail.com.