Tagrly agent guide.
Everything an AI assistant needs to use Tagrly: endpoints, the slot DSL, multi-call patterns, cost expectations, and what the API does on your behalf so you don't have to.
You're reading /agent-guide. The machine-readable summary lives at /llms.txt.
Contents
- What Tagrly is, in two paragraphs
- The core agent loop
- Authentication (one header)
- Endpoints, by use case
- POST /api/page: the magic call
- POST /api/brief: the power-user call
- The slot DSL (must_have, prefer, diversity)
- /api/usage, never recycle the same image
- /api/search: the low-level primitive
- Recipes: hub pages, blog posts, social cards
- What Tagrly filters by default
- The gap signal: when to ask the customer for more
- Workspaces, verticals, and overlays
- Cost expectations
- Errors, retries, idempotency
1. What Tagrly is, in two paragraphs
Tagrly is a workspace-scoped image catalog. When a customer connects their Google Drive (or Dropbox) folder, every image is analyzed by Anthropic Claude vision and stored with ~30 structured signals: focal subject, scene, lighting, time of day, mood, quality, marketing-graphic flag, what's visible, and (for verticals that ask) team gear, ceremony phase, room type, etc.
The API you're calling is the agent-facing surface on top of that catalog. You describe a page you're building, Tagrly returns the right images. The API never returns guesses or padding. When your customer's library is thin on a topic, the response includes an honest gap_signal with concrete suggestions for what to shoot or generate.
2. The core agent loop
For 90% of work, the loop is:
- Decide what section of the page you're filling (hero / gallery / room / detail).
- Call
POST /api/pagewith a plain-English topic for that section. - Render the returned images into the page using their
hosted_url+ thealt_rewriteTagrly wrote for you. - POST the rendered image ids to
/api/usageso the next call never repeats them. - Pass
exclude_recently_used_dayson subsequent calls so a 50-page automation never reuses an image.
/api/brief instead, but most calls should be /api/page.
3. Authentication
One header on every request:
Authorization: Bearer tagrly_pk_<workspace_slug>_<32 hex chars>
Keys are generated in the admin UI at /settings → API keys (or via the scripts/create-api-key.py CLI for self-hosted setups). The visible key is shown once at creation. Tagrly stores only the SHA-256 hash, so it cannot be recovered later.
Keys are workspace-scoped. The workspace_slug in the key (e.g. tagrly_pk_acme_…) makes it obvious in logs which tenant a call belongs to.
4. Endpoints, by use case
| If you want to… | Call |
|---|---|
| Build a section of a page from a plain topic | POST /api/page |
| Define an exact slot shape (count, must_have, prefer) | POST /api/brief |
| Tell Tagrly which images you actually rendered | POST /api/usage |
| List recently rendered images for this workspace | GET /api/usage/recent |
| Run a low-level keyword search | GET /api/search |
| Fetch a single image's full analysis payload | GET /api/image/{id} |
| List named collections in this workspace | GET /api/collections |
| Save a curated set for human review | POST /api/add-to-collection |
5. POST /api/page: the magic call
The simplest possible request:
POST /api/page
Authorization: Bearer tagrly_pk_acme_…
Content-Type: application/json
{ "topic": "weekend brunch on our rooftop patio" }
The response is a curated tile-plan plus diagnostics:
{
"ok": true,
"topic": "weekend brunch on our rooftop patio",
"layout": "food-drink",
"slots": {
"hero": { "wanted": 1, "got": 1, "picks": [ … ] },
"product_grid":{ "wanted": 6, "got": 6, "picks": [ … ] },
"experience": { "wanted": 3, "got": 3, "picks": [ … ] }
},
"gap_signal": { "fired": false, … },
"diagnostics": {
"claude_cost_usd": 0.029,
"time_context_detected": "daytime",
"promo_graphics_filtered": true,
"elapsed_ms": 9021
}
}
Each pick in slots[role].picks includes:
drive_id, stable id for the image. Use in/api/usagewhen you render it.hosted_url: the public CDN URL. Render directly with<img src>.alt_rewrite: alt text written in your page's voice (when you setalt_text_rewrite: true, the default).on_topic_score, 0-100. 100 = "would be the hero of a paid editorial." Below 50 means tangential; consider whether to use.quality_score, 0-100, computed from the analyzer's quality enum.why_picked, one sentence on why the curator chose this image.
Optional inputs
h1: the page's H1. Helps the curator anchor alt rewrites and prefer images with negative space.vocabulary.extra_terms, extra search terms, useful when you know your domain's vocabulary ("bottomless mimosa", "Sneaker Lounge").exclude_image_ids, array of drive_ids already used on this page from prior calls.exclude_recently_used_days, auto-excludes anything logged via/api/usagein the last N days for this workspace.model, defaults toclaude-haiku-4-5-20251001. Opt up to Sonnet or Opus for harder judgement (~5× the cost).dry_run, skips the LLM curation step. Useful for inspecting the candidate pool without paying.include_promo_graphics, defaultsfalse. Settrueif you're building a marketing landing page where promo art IS the goal.
Auto-detected layouts
| Topic contains… | Layout | Slots |
|---|---|---|
| Sports / team / watch-party vocab (hospitality-sports workspaces only) | sports | hero + action_grid + lifestyle |
| Food / drink / brunch / cocktail vocab (hospitality verticals) | food-drink | hero + product_grid + experience |
| Birthday / celebration / private event | events | hero + moments_grid + details |
| Patio / lounge / venue / room words | venue | hero + tour_grid + people_in_space |
| Anything else | generic | hero + body_grid + detail |
6. POST /api/brief, explicit slot control
Use this when /api/page's auto-layout doesn't match your page template. You write the slots yourself; everything else (FTS query, auto-discovery, diversity, Haiku curation, gap signal) works the same way.
POST /api/brief
{
"topic": "our outdoor patio brunch",
"h1": "Weekend Brunch on the Patio",
"slots": [
{
"role": "hero", "count": 1,
"must_have": ["focal:food-or-drink"],
"prefer": ["quality>=75", "negative_space"],
"diversity": "none",
"description": "Lead photo, drink + dish together if possible."
},
{
"role": "gallery", "count": 8,
"must_have": ["focal:food-or-drink|focal:product-or-object"],
"prefer": ["quality>=70"],
"diversity": "subject+angle",
"description": "Eight distinct dishes / drinks. Vary the angles."
},
{
"role": "atmosphere", "count": 4,
"must_have": ["people>=2"],
"prefer": ["mood:lively|mood:celebratory"],
"diversity": "subject+angle",
"description": "Group moments at the patio."
}
]
}
The response is identical shape to /api/page, same picks, same diagnostics, same gap signal.
7. The slot DSL
Used in must_have (hard filter, all items in the list must pass), prefer (soft scoring, matched items get a bonus). Inside a single string, alternatives are joined with |.
Available predicates
| Predicate | Means |
|---|---|
focal:<cat> | Image's focal_category equals one of the listed values. e.g. focal:food-or-drink|focal:venue-detail. |
scene:<a>,<b> | Image's scene is in the listed set. e.g. scene:bar-area,outdoor-patio. |
mood:<a>|mood:<b> | Image's mood matches at least one. e.g. mood:hype|mood:celebratory. |
people>=N / people>N / people<N | Number of people in frame. |
quality>=N | Computed quality score (0-100) at or above N. |
negative_space | True when image has clear negative space for text overlay. |
jerseys | Hospitality-sports overlay only. True when team_markers.jerseys is non-empty. |
team:<name> | Any mention of the team anywhere (jerseys, branded items, TV broadcasts, focal_subject, alt text). Most permissive. |
team_focal:<name> | Team appears in focal_subject or alt_text or jerseys array. Excludes background-only mentions. |
team_dominant:<name> | Strictest: team is the FIRST item in jerseys array, OR it's the lead subject in focal_subject. |
sport:<name> | sport_on_tv matches, or sport appears in tv_content / keywords. |
Diversity strategies
| Strategy | Effect |
|---|---|
none | No diversity rerank, best-scoring images, period. Use for single-image slots (hero). |
subject+angle | Round-robin buckets of (scene, shot_type, focal_category). The default for grids. |
scene | Bucket by scene only, maximizes location variety. |
shoot | Bucket by drive_folder_id, avoids same-shoot repetition. |
8. POST /api/usage, never recycle
The dedup-across-pages loop. After you render a set of picks, log them:
POST /api/usage
{
"items": [
{ "drive_id": "1xabcd...", "page_url": "https://customer.com/watch-party-page",
"page_topic": "weekend sports watch party", "slot_role": "hero" },
…
]
}
Then on subsequent calls:
POST /api/page
{ "topic": "…", "exclude_recently_used_days": 14 }
Tagrly auto-excludes anything in the last 14 days for this workspace. Result: a 50-post automation never recycles the same hero.
9. GET /api/search, low-level keyword search
When you want the raw matching list without curation:
GET /api/search?q=brunch&pillar=4&limit=24&sort=relevance
Returns a list of image dicts with the same fields as /api/page picks, minus alt_rewrite + on_topic_score + why_picked (no LLM ran).
10. Recipes
Recipe A, single blog-post hero
const res = await fetch("https://app.tagrly.com/api/page", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TAGRLY_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
topic: postTitle,
h1: postTitle,
exclude_recently_used_days: 30
})
});
const { slots } = await res.json();
const hero = slots.hero.picks[0];
// hero.hosted_url goes in <img src>
// hero.alt_rewrite goes in alt=""
Recipe B, multi-section hub page
For a hub page with several named sections, call /api/page once per section. Pass exclude_image_ids forward so nothing repeats:
import requests
HDRS = {"Authorization": f"Bearer {API_KEY}"}
seen = []
def call(topic):
body = {"topic": topic, "exclude_image_ids": seen}
j = requests.post("https://app.tagrly.com/api/page",
json=body, headers=HDRS).json()
for slot in j["slots"].values():
for p in slot["picks"]:
seen.append(p["drive_id"])
return j
food = call("weekend brunch food and signature dishes")
room_a = call("our covered patio with pink and teal lighting")
room_b = call("our tented yard for larger groups")
room_c = call("indoor sneaker-lounge bar with sneaker wall")
Recipe C: daily automation, no recycling
# 1. ask for picks
picks = requests.post(URL + "/api/page",
json={"topic": today_topic, "exclude_recently_used_days": 30},
headers=HDRS).json()
# 2. render the post (your code)
publish(picks)
# 3. log usage so tomorrow's call excludes them
requests.post(URL + "/api/usage", json={
"items": [
{"drive_id": p["drive_id"], "page_url": post_url,
"page_topic": today_topic, "slot_role": role}
for role, slot in picks["slots"].items()
for p in slot["picks"]
]
}, headers=HDRS)
11. What Tagrly filters by default
These run on every call without you asking. Most are universal; one is auto-detected from your topic.
Promo / marketing-graphic filter (always on)
Images Tagrly's analyzer flagged as promo posters, social-media cards, or text-overlay marketing assets are excluded by default. Set "include_promo_graphics": true if you're building a marketing landing page where the branded look IS the intent.
Time-of-day filter (auto-detected from topic)
Topics mentioning "brunch", "breakfast", "morning", "lunch" → daytime-only candidates. Topics mentioning "late night", "after hours", "midnight" → night-only candidates. Topics with no time signal → no filter.
Hidden / unsafe / duplicate images
Images flagged minors_visible, manually hidden, or duplicate-rank > 0 are always excluded. There's no opt-in for these.
Cross-slot dedup
Within a single /api/page or /api/brief call, no image appears in more than one slot. Slot order is priority order, the hero slot's pick is reserved before the action grid runs.
12. The gap signal
When the curator can't fill a slot honestly, the response includes:
"gap_signal": {
"fired": true,
"shortfall_total": 3,
"slots_short": [{ "role": "action_grid", "short_by": 3 }],
"explanations": [
"[action_grid] Only 3 candidates show Yankees gear as the focal subject; the rest had Yankees only in branded items."
],
"would_help": [
"Close-up of 3-4 fans wearing Yankees jerseys at the bar counter",
"Wide shot of a Yankees-fan crowd raising drinks together",
"Reaction shot of 2-3 Yankees fans celebrating a play"
],
"recommendations": [
{ "type": "retag_pass", "…": "…" },
{ "type": "generate", "…": "…" }
]
}
If your agent loop hits a fired gap, two reasonable responses:
- Tell the customer. Include the
would_helpphrases in a message, these read like a photo shoot brief. - Generate or substitute. The
recommendationsarray has actionable types:retag_pass(rescan likely lookalikes with a focused prompt),generate(fall back to AI image generation),broaden_topic(loosen the slot rules).
13. Workspaces, verticals, and overlays
Every API call is scoped to the workspace the API key unlocks. A workspace has:
- A vertical (e.g.
universal,hospitality-sports,wedding-venue,real-estate-listing,restaurant-bar). Drives which analyzer overlay applies + which auto-layout family the magic call uses. - A system prompt overlay, freeform text appended to the analyzer prompt for this workspace only. The hospitality-sports vertical writes team_markers / sport_on_tv automatically; a workspace owner can also inject brand-specific instructions ("prioritize the sneaker cocktail when you see it", "tag the bride and groom in every ceremony shot").
- An optional synonyms table, manual canonical→aliases pairs. Mostly redundant with auto-discovery; useful for slang the catalog itself never sees.
Customers configure vertical + overlay during onboarding. Your agent typically doesn't need to touch any of this, just trust that /api/page already knows the customer's domain.
14. Cost expectations
Costs scale with the number of slots curated by Claude. Typical per-call costs in the response's diagnostics.claude_cost_usd:
| Call shape | Cost | Time |
|---|---|---|
/api/page, single magic-call (3 slots) | ~$0.025-0.035 | 7-12 s |
/api/brief, 6 slots | ~$0.05-0.06 | 10-15 s parallel |
Multi-call hub page (4 × /api/page) | ~$0.12 total | ~12 s parallel / ~40 s sequential |
/api/search (no LLM) | $0 | <100 ms |
/api/usage, /api/usage/recent | $0 | <200 ms |
The cost_cap_usd request field puts a hard ceiling on a single call (default $0.50). When exceeded mid-call, remaining slots fall back to deterministic ranking without Haiku.
15. Errors, retries, idempotency
- 400, missing required field (
topic_required,slots_required, etc.). The response body always includes a structurederrorfield telling you which. - 401 / no auth context, your API key is missing, malformed, or revoked. Generate a new key in
/settings → API keys. - 500, server error. The response body includes a short trace excerpt; the underlying Claude API may be down. Retry with exponential backoff (1s, 2s, 4s, 8s; give up after 4 retries).
- All read endpoints are idempotent.
/api/page//api/briefmay return different picks on retry (cross-slot dedup uses non-deterministic random selection within score ties), to make a call reproducible, pass an explicitexclude_image_idslist. /api/usageinserts rows on every call. There's no dedup, the same image used twice on the same page legitimately writes two rows, which the freshness filter handles.
Plug Tagrly into your AI assistant.
Generate an API key, paste it into the header, start calling. The machine-readable summary is at /llms.txt.