Quick Start

This guide walks you through three calls that cover the basics:

  1. List the latest active ads on a platform
  2. Paginate through results with a cursor
  3. Fetch the full detail of a single ad

If you can run these three, you can run anything else in the API.

Base URL

https://api.tryatria.com

All endpoints live under /open/v1/.

Authentication

Every request must include your API key in the X-API-Key header:

X-API-Key: atria-sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API keys start with the atria-sk_ prefix. To create one:

  1. In the Atria web app, click the avatar in the top-left corner.
  2. Go to Settings & members → API Keys.
  3. Click Create API key to issue a new key.
🚧

Copy your key immediately

The full key is shown only once at creation time — store it in a password manager or secrets vault before closing the modal. If you lose it, you'll need to rotate to a new key.

Each workspace can have up to two active keys.

If the X-API-Key header is missing, the gateway returns 401 with error: "unauthorized":

{
  "error": "unauthorized",
  "message": "No valid credentials provided",
  "request_id": "db254fccc64f0e57f3156b8274620426"
}

If the header is present but the key isn't recognized (unknown, disabled, suspended, or expired), you get 401 with error: "invalid_api_key":

{
  "error": "invalid_api_key",
  "message": "The provided API key is invalid",
  "request_id": "20a3d4af927efade86e6ff9f1cae3ff7"
}

Response envelope

Every successful endpoint returns the same wrapper:

{
  "code": 0,
  "message": "ok",
  "data": { ... }
}
  • code: 0 means success. A non-zero code indicates a business-level error — read message for context.
  • Gateway-level errors (401, 429, 5xx) bypass the envelope and return a flat { error, message, request_id } shape. Always check the HTTP status first.

Example 1 — List recent active ads on Facebook

The most common starting point: fetch the latest active ads, filtered by platform.

curl -sS \
  -H "X-API-Key: $ATRIA_API_KEY" \
  "https://api.tryatria.com/open/v1/ads?platform=facebook&status=active&sort_by=newest&page_size=10"
import os
import requests

resp = requests.get(
    "https://api.tryatria.com/open/v1/ads",
    headers={"X-API-Key": os.environ["ATRIA_API_KEY"]},
    params={
        "platform": "facebook",
        "status": "active",
        "sort_by": "newest",
        "page_size": 10,
    },
    timeout=30,
)
resp.raise_for_status()
body = resp.json()
assert body["code"] == 0, body["message"]
for ad in body["data"]["items"]:
    print(ad["id"], ad["brand_name"], ad["title"])
const url = new URL("https://api.tryatria.com/open/v1/ads");
url.searchParams.set("platform", "facebook");
url.searchParams.set("status", "active");
url.searchParams.set("sort_by", "newest");
url.searchParams.set("page_size", "10");

const resp = await fetch(url, {
  headers: { "X-API-Key": process.env.ATRIA_API_KEY },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const body = await resp.json();
if (body.code !== 0) throw new Error(body.message);
for (const ad of body.data.items) {
  console.log(ad.id, ad.brand_name, ad.title);
}

A truncated response:

{
  "code": 0,
  "message": "ok",
  "data": {
    "items": [
      {
        "id": "m1234567890123456",
        "ad_id": "m1234567890123456",
        "status": "active",
        "brand_id": "m1234567890123456",
        "brand_name": "Example Brand",
        "brand_avatar_url": "https://cdn.example.com/avatar.png",
        "platforms": ["facebook", "instagram"],
        "display_format": "video",
        "media_format": "video",
        "title": "Discover our new collection",
        "body": "Limited drop — free shipping this weekend.",
        "cta_type": "SHOP_NOW",
        "cta_text": "Shop Now",
        "link_url": "https://example.com/spring",
        "start_date": "2026-05-01",
        "end_date": null,
        "images": [],
        "videos": [
          {
            "url": "https://cdn.example.com/video.mp4",
            "preview_image_url": "https://cdn.example.com/preview.jpg",
            "width": 1080,
            "height": 1080,
            "duration": 18.5
          }
        ],
        "categories": ["shopping", "accessories"]
      }
    ],
    "cursor": "eyJjdCI6MTAsImRnIjoiZWI2OGZjNDY5NjI1MDgzOTg2N2FjZWMuLi4=",
    "total": 0,
    "page_size": 10
  }
}

What to notice:

  • items[].id is the canonical ad ID — use it for the detail endpoint in Example 3.
  • cursor is an opaque token. Pass it back as-is to get the next page — never try to decode or modify it.
  • total may be 0 or a clamped value; treat cursor pagination as the source of truth for "are there more results."

Example 2 — Paginate with the cursor

Cursor pagination is deterministic and retry-safe: replaying the same cursor against the same query returns the same items, so it's safe to retry on network errors.

Pass the cursor from Example 1 back as a query parameter to fetch page 2:

curl -sS \
  -H "X-API-Key: $ATRIA_API_KEY" \
  "https://api.tryatria.com/open/v1/ads?platform=facebook&status=active&sort_by=newest&page_size=10&cursor=$CURSOR"
import os
import requests

BASE = "https://api.tryatria.com/open/v1/ads"
headers = {"X-API-Key": os.environ["ATRIA_API_KEY"]}
params = {
    "platform": "facebook",
    "status": "active",
    "sort_by": "newest",
    "page_size": 10,
}

cursor = None
for page in range(1, 4):  # walk 3 pages
    if cursor:
        params["cursor"] = cursor
    resp = requests.get(BASE, headers=headers, params=params, timeout=30)
    resp.raise_for_status()
    data = resp.json()["data"]
    print(f"page {page}: {len(data['items'])} ads")
    cursor = data["cursor"]
    if not cursor:
        break  # last page
const BASE = "https://api.tryatria.com/open/v1/ads";
const headers = { "X-API-Key": process.env.ATRIA_API_KEY };
const baseParams = {
  platform: "facebook",
  status: "active",
  sort_by: "newest",
  page_size: "10",
};

let cursor = null;
for (let page = 1; page <= 3; page++) {
  const url = new URL(BASE);
  for (const [k, v] of Object.entries(baseParams)) url.searchParams.set(k, v);
  if (cursor) url.searchParams.set("cursor", cursor);

  const resp = await fetch(url, { headers });
  const body = await resp.json();
  console.log(`page ${page}: ${body.data.items.length} ads`);
  cursor = body.data.cursor;
  if (!cursor) break; // last page
}

A few things to keep in mind:

  • The cursor is opaque — don't parse, decode, or hand-construct it.
  • page_size caps at 250. Values above are silently clamped.
  • When cursor is null in the response, you've reached the last page.
🚧

Don't reuse a cursor across different filters

A cursor encodes the query it was issued under. Changing platform, query, sort_by, etc. and reusing an old cursor returns undefined results. Always start fresh (no cursor) when filters change.


Example 3 — Get an ad's full detail

Once you have an ad ID from the list endpoint, fetch the complete record:

curl -sS \
  -H "X-API-Key: $ATRIA_API_KEY" \
  "https://api.tryatria.com/open/v1/ads/m1234567890123456"
import os
import requests

ad_id = "m1234567890123456"
resp = requests.get(
    f"https://api.tryatria.com/open/v1/ads/{ad_id}",
    headers={"X-API-Key": os.environ["ATRIA_API_KEY"]},
    timeout=30,
)
resp.raise_for_status()
ad = resp.json()["data"]

print(f"{ad['brand_name']} — {ad['title']}")
print(f"  format: {ad['display_format']}  status: {ad['status']}")
print(f"  copy:   {ad['body']!r}")

# Media assets — every ad carries `images[]` and `videos[]`; either or
# both can be empty depending on display_format.
for img in ad["images"]:
    print(f"  image: {img['url']}  ({img['width']}x{img['height']})")
for vid in ad["videos"]:
    print(
        f"  video: {vid['url']}  "
        f"({vid['duration']}s, preview={vid['preview_image_url']})"
    )
if not ad["images"] and not ad["videos"]:
    print("  (no media — text-only ad)")
const adId = "m1234567890123456";
const resp = await fetch(
  `https://api.tryatria.com/open/v1/ads/${adId}`,
  { headers: { "X-API-Key": process.env.ATRIA_API_KEY } },
);
const ad = (await resp.json()).data;

console.log(`${ad.brand_name} — ${ad.title}`);
console.log(`  format: ${ad.display_format}  status: ${ad.status}`);
console.log(`  copy:   ${JSON.stringify(ad.body)}`);

// Media assets — every ad carries `images[]` and `videos[]`; either or
// both can be empty depending on display_format.
for (const img of ad.images) {
  console.log(`  image: ${img.url}  (${img.width}x${img.height})`);
}
for (const vid of ad.videos) {
  console.log(
    `  video: ${vid.url}  (${vid.duration}s, preview=${vid.preview_image_url})`,
  );
}
if (ad.images.length === 0 && ad.videos.length === 0) {
  console.log("  (no media — text-only ad)");
}

The detail response carries the same shape as one element of data.items from the list endpoint — same field names, same types. The endpoint exists so you can refetch a single ad without re-issuing the list query.

What media to expect

Every ad has images[] and videos[] arrays, but their contents depend on the ad's display_format:

display_formatimages[]videos[]Notes
image1 entryemptyStatic creative.
videoempty1 entryvideos[0].preview_image_url is the thumbnail.
carouselmultiple entriesmay also have videosCards are flattened — order is preserved.
dcovariesvariesDynamic creative; check both arrays.

Always iterate both arrays and tolerate empty lists — that one loop covers every case without branching on display_format. Treat display_format as informational, not as a contract on which array will be populated.

If the ad doesn't exist (or isn't visible to your workspace), you get:

{
  "code": 0,
  "message": "ok",
  "data": null
}

Where to go next

  • Use cases — composite recipes (competitor research, daily digest, board sync, etc.)
  • API reference — every endpoint, parameter, and enum value