Analyze a Board with Claude

Your team curates inspiration into boards inside Atria. This recipe exports a single board's ads — copy, CTA, media URLs, plus the saved_at and tags your team added — and feeds them to Claude so you can ask higher-order questions: "what creative themes are working?", "draft 5 variants in this brand's voice", or "rank these by hook strength."

Endpoints used

  • GET /open/v1/boards — list available boards
  • GET /open/v1/boards/{board_id}/ads — ads inside a single board
📘

New to the API? Read the Quickstart for auth and the response envelope.

Step 1 — Pick the board you want to export

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

resp = requests.get(
    "https://api.tryatria.com/open/v1/boards",
    headers={"X-API-Key": os.environ["ATRIA_API_KEY"]},
    timeout=30,
)
for board in resp.json()["data"]["items"]:
    print(board["board_id"], board["name"], f"({board['ad_num']} ads)")
const resp = await fetch(
  "https://api.tryatria.com/open/v1/boards",
  { headers: { "X-API-Key": process.env.ATRIA_API_KEY } },
);
const { data } = await resp.json();
for (const board of data.items) {
  console.log(board.board_id, board.name, `(${board.ad_num} ads)`);
}

Boards are returned as a tree — nested boards live under each parent's children[] array. Pick the board_id of whichever board you want to feed to the model.

Step 2 — Walk every ad in the board

Boards can be large, so always page through the cursor — don't try to fetch in one shot.

import os
import requests

API_KEY = os.environ["ATRIA_API_KEY"]
BOARD_ID = "board_abc123"  # from Step 1

def all_ads_in_board(board_id: str):
    params = {"page_size": 100, "sort_by": "saved_newest"}
    cursor = None
    while True:
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"https://api.tryatria.com/open/v1/boards/{board_id}/ads",
            headers={"X-API-Key": API_KEY},
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()["data"]
        yield from data["items"]
        cursor = data.get("cursor")
        if not cursor:
            return

ads = list(all_ads_in_board(BOARD_ID))
print(f"loaded {len(ads)} ads from board {BOARD_ID}")
const API_KEY = process.env.ATRIA_API_KEY;
const BOARD_ID = "board_abc123"; // from Step 1

async function* allAdsInBoard(boardId) {
  let cursor = null;
  while (true) {
    const url = new URL(
      `https://api.tryatria.com/open/v1/boards/${boardId}/ads`,
    );
    url.searchParams.set("page_size", "100");
    url.searchParams.set("sort_by", "saved_newest");
    if (cursor) url.searchParams.set("cursor", cursor);

    const resp = await fetch(url, { headers: { "X-API-Key": API_KEY } });
    const { data } = await resp.json();
    for (const ad of data.items) yield ad;
    cursor = data.cursor;
    if (!cursor) return;
  }
}

const ads = [];
for await (const ad of allAdsInBoard(BOARD_ID)) ads.push(ad);
console.log(`loaded ${ads.length} ads from board ${BOARD_ID}`);

Each ad inside a board carries an extra saved_details block — useful both for filtering and as additional context for the model:

{
  "saved_details": {
    "saved_at": "2026-04-15T14:31:08Z",
    "boards": ["board_abc123", "board_xyz789"],
    "tags": ["holiday", "video"]
  }
}

Step 3 — Feed the ads to Claude

The simplest pattern: format each ad as a short text block, concatenate them, and ask Claude a question.

import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

def fmt(ad: dict) -> str:
    saved = ad.get("saved_details") or {}
    return (
        f"- [{ad['display_format']}] {ad['brand_name']} — {ad['title']}\n"
        f"  copy:   {ad.get('body') or ''}\n"
        f"  cta:    {ad.get('cta_type')} → {ad.get('link_url') or ''}\n"
        f"  tags:   {', '.join(saved.get('tags') or [])}\n"
        f"  saved:  {saved.get('saved_at')}"
    )

prompt = (
    "Below are ads my team has saved for inspiration. "
    "Identify the top 3 creative themes and the CTA patterns each theme uses. "
    "Be specific and quote ad copy where helpful.\n\n"
    + "\n\n".join(fmt(ad) for ad in ads)
)

resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    messages=[{"role": "user", "content": prompt}],
)
print(resp.content[0].text)

Parameter notes

  • sort_by=saved_newest returns ads in the order they were added to the board (newest first). Use saved_oldest to start from the oldest pin.
  • boards/{board_id}/ads doesn't support sort_by=best_match — board scopes use a nested filter that's incompatible with the relevance rescore.

Incremental sync (only feed what's new)

If you're running the digest on a schedule and want to give the model only fresh additions, store the highest saved_at you've seen and skip everything older on the next run.

import datetime

WATERMARK = "2026-01-01T00:00:00Z"  # load from your DB

cutoff = datetime.datetime.fromisoformat(WATERMARK.replace("Z", "+00:00"))
new_ads = []
for ad in all_ads_in_board(BOARD_ID):
    saved_at = (ad.get("saved_details") or {}).get("saved_at")
    if not saved_at:
        continue
    if datetime.datetime.fromisoformat(saved_at.replace("Z", "+00:00")) <= cutoff:
        break  # results are saved_newest first → we're past the watermark
    new_ads.append(ad)

# Persist new high-water mark after a successful run.
if new_ads:
    latest = max(a["saved_details"]["saved_at"] for a in new_ads)
    print("new watermark:", latest)
📘

Why the early break is safe

Because we sort saved_newest, the first ad older than the watermark guarantees every subsequent ad is also older. No need to walk the rest of the cursor — close the loop and save your downstream requests.

Going further

  • Vision input: pass videos[0].preview_image_url (or images[0].url) into Claude's multimodal input and ask the model to describe visual style, not just copy.
  • Variant generation: feed the top N ads as exemplars and ask for 5 new headlines in the same voice.
  • Theme clustering: embed each ad's copy with a Claude-compatible embedding model, then k-means / cosine cluster — useful when boards grow past a few hundred ads and you want structure before prompting.
  • Token budget: each ad's text block is ~150-300 tokens. A 500-ad board easily fits in Claude Sonnet's 200K context; chunk by tags or by saved_at window for larger boards.