API Documentation

Getting Started

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.

POST /v1/tasks
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

  1. Sign up for a Metapi account at metapi.io
  2. Navigate to the API Keys section in your dashboard
  3. Click "Create New Key" and give it a descriptive name
  4. Copy the key immediately -- it will only be shown once

Example Header

Authorization 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/json for all requests with a body.
  • Versioning -- the current version is v1. Breaking changes will be introduced in a new version. The v1 namespace will remain stable.
Endpoints
POST /v1/tasks

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:

POST Keyword Search
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):

POST Advertiser Search
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

202 Accepted JSON
{
  "task_id": "abc123",
  "status": "running",
  "message": "Task created. Poll GET /v1/tasks/abc123/status for progress."
}
GET /v1/tasks/:uuid/status

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

GET Task Status
curl -X GET "https://api.metapi.io/v1/tasks/abc123/status" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response Example

200 OK JSON
{
  "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
}
GET /v1/tasks/:uuid/results

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

GET Task Results
curl -X GET "https://api.metapi.io/v1/tasks/abc123/results?offset=0&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response Example

200 OK JSON
{
  "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.

POST /v1/tasks/:uuid/stop

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

POST Stop Task
curl -X POST "https://api.metapi.io/v1/tasks/abc123/stop" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response Examples

200 OK JSON
{
  "task_id": "abc123",
  "status": "aborted",
  "message": "Task stopped successfully."
}
409 Conflict 409
{
  "error": {
    "code": "conflict",
    "message": "Task is already finished"
  }
}
Webhooks
POST /v1/webhooks

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

POST Create Webhook
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

201 Created JSON
{
  "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"
  }
}
GET /v1/webhooks

List all webhook subscriptions for your account. Returns an array of webhook objects with their current configuration and status.

Request Example

GET List Webhooks
curl -X GET "https://api.metapi.io/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"

Response Example

200 OK JSON
{
  "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 /v1/webhooks/:webhook_id

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

DELETE Delete Webhook
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.

Signature Verification (Python)
# 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

POST Webhook Delivery
// 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"
}
Data Reference

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

Standard Response Envelope
{
  "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

Creating a Task and Polling for Completion
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 Response Example 401
{
  "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.

Quota Exceeded 402
{
  "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

Rate Limit Exceeded 429
// 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."
  }
}
Code Examples

cURL Examples

Quick command-line examples using cURL. Replace YOUR_API_KEY with your actual API key.

Create a Keyword Search Task

POST Search ads by keyword and country
# 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

POST Retrieve all ads for a specific advertiser
# 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

GET Poll for task progress
# 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

POST Stop a task and preserve partial results
# 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

create_task.py
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

advertiser_search.py
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

error_handling.py
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

create_task.js (fetch)
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

create_task_axios.js
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

webhook_server.js
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.