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.

GET /v1/ads/search
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

  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
GET /v1/ads/search

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

GET Search Ads
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

200 OK JSON
{
  "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
  ]
}
GET /v1/advertisers/:id/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

GET Advertiser's Ads
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

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

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": ["new_ad", "ad_updated"],
    "filters": {
      "country": "US",
      "advertiser_id": "946786242013194",
      "keyword": "shopify"
    },
    "secret": "whsec_your_signing_secret_here"
  }'

Response Example

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

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

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

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

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

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

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

Iterating Through All Pages
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 Response Example 401
{
  "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

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

cURL Examples

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

Search Ads

GET Search ads by keyword and country
# 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 Retrieve a single ad
# 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

GET Find advertisers by name
# 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

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

fetch_all_ads.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 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

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}/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

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

search_ads_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"
  }
});

// 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

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(`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");
});