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 boardsGET /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_newestreturns ads in the order they were added to the board (newest first). Usesaved_oldestto start from the oldest pin.boards/{board_id}/adsdoesn't supportsort_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 earlybreakis safeBecause 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(orimages[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
tagsor bysaved_atwindow for larger boards.
