Skip to main content
For AI assistants

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.

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:

  1. Decide what section of the page you're filling (hero / gallery / room / detail).
  2. Call POST /api/page with a plain-English topic for that section.
  3. Render the returned images into the page using their hosted_url + the alt_rewrite Tagrly wrote for you.
  4. POST the rendered image ids to /api/usage so the next call never repeats them.
  5. Pass exclude_recently_used_days on subsequent calls so a 50-page automation never reuses an image.
One topic in, a page-ready set of images out. If you're tempted to write structured slot definitions, you probably want /api/brief instead, but most calls should be /api/page.

3. Authentication

One header on every request:

HTTP header
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 topicPOST /api/page
Define an exact slot shape (count, must_have, prefer)POST /api/brief
Tell Tagrly which images you actually renderedPOST /api/usage
List recently rendered images for this workspaceGET /api/usage/recent
Run a low-level keyword searchGET /api/search
Fetch a single image's full analysis payloadGET /api/image/{id}
List named collections in this workspaceGET /api/collections
Save a curated set for human reviewPOST /api/add-to-collection

5. POST /api/page: the magic call

The simplest possible request:

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:

response (abridged)
{
  "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/usage when 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 set alt_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/usage in the last N days for this workspace.
  • model, defaults to claude-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, defaults false. Set true if you're building a marketing landing page where promo art IS the goal.

Auto-detected layouts

Topic contains…LayoutSlots
Sports / team / watch-party vocab (hospitality-sports workspaces only)sportshero + action_grid + lifestyle
Food / drink / brunch / cocktail vocab (hospitality verticals)food-drinkhero + product_grid + experience
Birthday / celebration / private eventeventshero + moments_grid + details
Patio / lounge / venue / room wordsvenuehero + tour_grid + people_in_space
Anything elsegenerichero + 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.

request
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

PredicateMeans
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<NNumber of people in frame.
quality>=NComputed quality score (0-100) at or above N.
negative_spaceTrue when image has clear negative space for text overlay.
jerseysHospitality-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

StrategyEffect
noneNo diversity rerank, best-scoring images, period. Use for single-image slots (hero).
subject+angleRound-robin buckets of (scene, shot_type, focal_category). The default for grids.
sceneBucket by scene only, maximizes location variety.
shootBucket 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:

request
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:

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

10. Recipes

Recipe A, single blog-post hero

javascript
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:

python
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

python
# 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:

response field
"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_help phrases in a message, these read like a photo shoot brief.
  • Generate or substitute. The recommendations array 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).
Don't pad. The curator is explicitly trained to return fewer-than-asked rather than weak picks. If you need N images guaranteed, broaden the topic, lower the quality threshold, or generate, but never silently lower your editorial standards.

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 shapeCostTime
/api/page, single magic-call (3 slots)~$0.025-0.0357-12 s
/api/brief, 6 slots~$0.05-0.0610-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 structured error field 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/brief may return different picks on retry (cross-slot dedup uses non-deterministic random selection within score ties), to make a call reproducible, pass an explicit exclude_image_ids list.
  • /api/usage inserts 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.