Skip to main content
For AI assistants

Tagrly agent guide.

Everything needed to call Tagrly: one endpoint that works with no account, one header for everything else, twelve keyed endpoints, the slot language, the defaults applied on your behalf, and every limit and error code. If you read one section, read POST /api/page.

On this page

What Tagrly is#

A workspace-scoped image catalog. A customer connects a Google Drive or Dropbox folder; every image is analyzed by Claude vision against a structured schema and stored with the resulting fields: focal subject, scene, lighting, time of day, mood, quality, a marketing-graphic flag, what is visible, and whatever else the detected vertical adds.

The API is the agent-facing surface on that catalog. You describe a page; it returns images from that customer's library. It never pads a result to hit a count: when the library is thin on a subject the response carries a gap_signal naming the shots that are missing.

Scope

Tagrly reads photographs a customer already owns. It is not a stock library and it does not generate images. If your user needs a picture that was never taken, the gap signal will say so. That is the honest answer, not a failure.

Calling it without a key#

No key required POST /api/try/analyze-photo is the one endpoint that takes no authentication at all. Send a single image as multipart/form-data under the field name photo. It runs the same prompt and JSON schema a paying workspace gets, returns the reading, and persists nothing. No session, no disk write, no database row.

curl -X POST https://tagrly.com/api/try/analyze-photo \
  -F "photo=@kitchen.jpg"
The entire request.
Response fields on a 200.
FieldMeaning
okAlways true on a 200.
verticalWhich vertical the photo classified into. Decides the schema that ran.
prompt_versionVersion tag for the prompt that produced this reading.
analysisThe structured reading. Field list depends on vertical.
elapsed_msServer-side time for the classify and analysis calls.
remainingReads left in your current hour.
limitReads allowed per hour, so you can pace without guessing.
Limit

Five reads an hour per connection, on a sliding one-hour window. Eight megabytes per upload, refused on the Content-Length header before the body is read. Accepted formats: JPEG, PNG, WebP, HEIC, HEIF, AVIF and GIF. Anything else that happens to decode is rejected by name.

Errors from POST /api/try/analyze-photo. Every body carries an error slug and a human-readable message.
Status error Cause and response
400no-fileNo part named photo. Fix the field name; do not retry as-is.
400empty-fileZero bytes arrived. Re-read the source file.
400wrong-typeDecoded, but outside the format allowlist. The message names the format it found.
400bad-imageWould not decode. Different file.
413too-largeOver 8 MB. Re-export smaller and resend.
429rate-limitHourly budget spent. retry_after holds the seconds to wait.
500server-misconfiguredServer-side. Retrying will not help; report it.
502parse-failed · upstreamThe model call failed or returned an unusable shape. Retry once, then back off.

Authentication#

Every other endpoint takes one header:

Authorization: Bearer tagrly_pk_<workspace-slug>_<32 hex chars>
The random half is 128 bits. The workspace slug is visible on purpose, so a customer's own logs show which tenant a call belonged to.
Careful

Keys are created by a signed-in human at Settings → API access, by a workspace owner or admin. There is no programmatic endpoint that mints a key. If you are an agent and your user has no key, say so and ask them to create one there; do not guess at a key format or an issuance route.

Keys are workspace-scoped: one key reaches exactly one tenant's catalog. Only the SHA-256 hash is stored, so the visible key is shown once and cannot be recovered. A lost key is replaced, not read back. Revoking a key stops it resolving on the very next request.

Limit

Every key grants full read and write access to its workspace.

MCP server#

Everything below is also served over the Model Context Protocol at https://tagrly.com/mcp (Streamable HTTP, stateless, spec revision 2025-06-18). Authenticate with the same Authorization: Bearer tagrly_pk_... header; initialize and tools/list work without a key, every tools/call requires one.

Six tools map onto the endpoints in this guide: search_photos (GET /api/search, with compact records), find_photos (POST /api/find), get_page_images (POST /api/page), get_image (GET /api/image/{id}), log_usage (POST /api/usage) and list_collections (GET /api/collections). Each tool call is dispatched through the corresponding HTTP endpoint internally, so the behavior, scoping, metering and error semantics in this guide apply verbatim on both surfaces. The two model-curated tools spend one metered ask each, exactly like their endpoints.

Note

There is no server-push stream: GET /mcp returns 405 to SSE requests, and tool calls complete in the POST that made them. Client setup recipes live at /mcp-server.

The core loop#

For most work the loop is five steps, four of them one call each.

  1. Decide which section of the page you are filling (hero, gallery, room, detail).
  2. Call POST /api/page with a plain-English topic for that section.
  3. Render each pick's hosted_url with the alt_rewrite Tagrly wrote for it.
  4. Post the rendered drive_ids to /api/usage.
  5. Pass exclude_recently_used_days on the next call, so a long automation never repeats an image.
Tip

If you find yourself writing slot definitions by hand, you want /api/brief. Most calls should be /api/page: it infers the layout and returns the same response shape.

Endpoints#

Base URL https://tagrly.com. Bodies are JSON except two: /api/try/analyze-photo takes multipart/form-data with the image in a field named photo, and /api/add-to-collection takes application/x-www-form-urlencoded.
If you want to… Call Key
Read one photo you uploadPOST /api/try/analyze-photoNo
Fill a page section from a plain topicPOST /api/pageYes
Define slot shape yourselfPOST /api/briefYes
Check your remaining allowanceGET /api/usageYes
Log the images you renderedPOST /api/usageYes
List what was logged latelyGET /api/usage/recentYes
Run a keyword and facet searchGET /api/searchYes
Get one image's full analysisGET /api/image/{drive_id}Yes
List named collectionsGET /api/collectionsYes
Save a set for human reviewPOST /api/add-to-collection (form-encoded)Yes
Resolve ids to source URLsGET /api/urlsYes
Get every id matching a searchGET /api/match-idsYes
List workspaces this key reachesGET /api/workspacesYes

POST /api/page#

The smallest useful request is one field.

POST /api/page
Authorization: Bearer tagrly_pk_acme_…
Content-Type: application/json

{ "topic": "weekend brunch on our rooftop patio" }
Everything else has a default.

Request fields#

Body of POST /api/page. Only topic is required.
Field Default What it does
topicn/aPlain-English description of the section. Required; an empty one returns topic_required.
h1""The page's headline. Anchors the alt rewrites and nudges toward images with negative space.
content""The page's actual text (draft body, listing description). Grounds retrieval and ranking in what the page says, not just the topic; markup stripped and length capped server-side. Diagnostics report the extracted terms.
vocabulary{}Object with extra_terms and teams arrays. Use for words the catalog would not know.
exclude_image_ids[]Ids already used on this page by earlier calls.
exclude_recently_used_days0Skips anything logged via /api/usage in the last N days for this workspace.
include_promo_graphicsfalseSet true when branded promo art is the point, e.g. a campaign landing page.
safety"safe"Safety filter mode applied to the candidate pool.
dry_runfalseSkips model curation. Inspect the candidate pool at zero cost.
modelclaude-haiku-4-5Curation model. A larger model buys sharper judgment at higher cost. /api/brief defaults to the dated claude-haiku-4-5-20251001 instead.
cost_cap_usd0.50Circuit breaker for one call. Clamped to a hard ceiling of 5.00.

Response#

{
  "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, "elapsed_ms": 9021, … }
}
Abridged. layout and slots_inferred are the two fields /api/page adds over /api/brief; everything else is identical between them.

Each slot also reports candidates_considered, shortlisted and an on_topic_score_distribution, which together tell you whether a thin result means a thin library or a narrow topic.

Fields on every pick#

Items in slots[role].picks.
FieldUse
drive_idStable id. This is what you send to /api/usage and exclude_image_ids.
hosted_urlPublic URL. Render it directly in src.
thumb_urlSmaller variant for previews and pickers.
alt_rewriteAlt text rewritten for this page's topic. null when alt_text_rewrite is off.
alt_textThe catalog's own alt text, independent of this request.
on_topic_score0–100. Under 50 is tangential; decide deliberately whether to use it.
quality_score0–100, from the analyzer's quality reading.
prescore · prescore_signalsPre-curation ranking score and the signals that produced it.
why_pickedOne sentence on why the curator chose it. Good material for a human-facing explanation.
focal_subject · focal_categoryWhat the image is of.
scene · mood · shot_typeSetting and framing, for your own grouping.

Layout detection#

First match wins, and matching happens within the workspace's vertical, a food topic only reaches the food layout in a hospitality workspace.

Auto-detected layouts and the slots each produces.
When the topic…LayoutSlots
Names an entity the workspace defined, or passes vocabulary.teamssportshero · action_grid · lifestyle
Carries food or drink words, in a hospitality verticalfood-drinkhero · product_grid · experience
Carries celebration or private-event wordseventshero · moments_grid · details
Carries venue, room or space wordsvenuehero · tour_grid · people_in_space
Matches nothing abovegenerichero · body_grid · detail

POST /api/brief#

Same pipeline, your slots. Reach for it when the auto layout does not match your template (a multi-section homepage, an unusual grid, a template that needs exactly eight images in one band). Every field from /api/page applies, plus three.

Fields only POST /api/brief takes.
FieldDefaultWhat it does
slotsn/aArray of slot objects. Required; an empty array returns slots_required.
alt_text_rewritetrueOff returns alt_rewrite: null and skips that part of curation.
max_candidates_per_slot25How deep the shortlist goes before curation. Higher costs more. Clamped to 5–60; going over 60 is noted in diagnostics.validation_warnings.
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 and 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. Vary the angles." },
    { "role": "atmosphere", "count": 4,
      "must_have": ["people>=2"],
      "prefer":    ["mood:lively|mood:celebratory"],
      "diversity": "subject+angle",
      "description": "Group moments on the patio." }
  ]
}
The response shape is identical to /api/page, same picks, same gap signal, same diagnostics.

The slot language#

Two lists per slot. must_have is a hard filter and every item must pass. prefer is soft scoring: matches earn a bonus, misses are not excluded. Within one string, alternatives join with |.

Predicates, usable in must_have and prefer.
PredicateTrue when
focal:<a>,<b>The image's focal category is one of the listed values.
scene:<a>,<b>Its scene is in the set, e.g. scene:bar-area,outdoor-patio.
mood:<a>,<b>Its mood is in the set.
people>=N · people>N · people<NPeople in frame compares as stated.
quality>=NQuality score is at or above N.
negative_spaceThe image has clear space for a text overlay.
no_back_of_headNeither alt text nor description describes a back-of-head shot.
pillar:NThe image is tagged into content pillar N.
jerseysSports overlay. Team jerseys were detected.
team:<name>The team appears anywhere: jerseys, branded items, a broadcast on a screen. Most permissive.
team_focal:<name>The team is in the focal subject, alt text or jerseys. Excludes background-only mentions.
team_dominant:<name>Strictest: the team leads the jersey list or the focal subject.
sport:<name>The sport is on screen, or appears in the broadcast content or keywords.
Diversity strategies. An unrecognized value falls back to subject+angle.
StrategyEffect
noneNo rerank. Best-scoring images, in order. Use for single-image slots.
subject+angleRound-robin across scene, shot type and focal category. The default, and right for most grids.
sceneBucket by scene only. Maximizes location variety.
shootBucket by source folder. Avoids a grid that is visibly one shoot.

Usage ledger#

This is what stops a fifty-post automation from using the same hero fifty times. Log what you rendered, then exclude by age on the next call.

POST /api/usage
{
  "items": [
    { "drive_id": "1xabcd…",
      "page_url": "https://customer.com/brunch",
      "page_topic": "weekend brunch",
      "slot_role": "hero" }
  ]
}
A single use may also be posted as a bare object without items. Only drive_id is required.
Usage endpoints.
CallParametersReturns
POST /api/usage items array, or a single object. Batches over 100 are truncated to 100; entries with a malformed drive_id are skipped. { ok, recorded, skipped }
GET /api/usage/recent days, default 14, max 365. limit, default 100, max 500. { ok, count, days, items, drive_ids }

drive_ids comes back de-duplicated and sorted, which is exactly the shape exclude_image_ids expects, so a recent-use exclusion is one call and no reshaping.

Note

/api/usage inserts a row per item every time and does not de-duplicate. The same image legitimately used twice on one page writes two rows, and the freshness filter handles that.

The primitive under everything else: keyword and facet search with no model in the loop, so it is free and fast. Returns { count, results } where each result carries the catalog fields, but not alt_rewrite, on_topic_score or why_picked, because nothing curated it.

Query parameters.
ParameterAccepts
qFree-text query across the catalog's indexed fields.
scene · mood · shot_type · focal_category · editorial_fitExact facet values.
folderRestrict to one source folder.
sourcedrive or dropbox.
neg_spaceyes or no.
pillarInteger 1–5.
team · yearTeam name; four-digit year.
safetysafe (default) or any.
dupeshide (default) or show.
focal1 to match against focal subject only.
limitDefault 24. Clamped to 500.
sortResult ordering, e.g. by relevance or date.

The remaining endpoints#

Everything else, with the shape that matters.
CallInputReturns
GET /api/image/{drive_id} Path id, 5–100 characters of A–Z a–z 0–9 _ - : The full analysis payload for one image. 404 when the id is not in this workspace.
GET /api/collections None Summary of this workspace's named collections.
POST /api/add-to-collection Form-encoded: name, drive_ids as a comma-separated string { collection_id, collection_name, added }. Matches an existing collection by name, case-insensitively, or creates one.
GET /api/urls ids, comma-separated. Over 500 is truncated. { urls }, source URLs for ids present in this workspace.
GET /api/match-ids The same filters /api/search takes { ids, total, capped, cap }. capped tells you the match set was larger than the 500 returned.
GET /api/workspaces None Workspaces this credential reaches, and which one is active.
Careful

POST /api/add-to-collection is the one endpoint that takes application/x-www-form-urlencoded rather than JSON, and drive_ids is a comma-joined string, not an array. A JSON body returns missing name or drive_ids.

Applied defaults#

These run on every curated call whether you ask or not. Knowing them explains most surprising results.

What happens to the candidate pool before you see it.
FilterBehaviorOpt out
Promo graphics Images the analyzer flagged as posters, social cards or text-overlay marketing assets are excluded. include_promo_graphics: true
Time of day Detected from the topic. Brunch, breakfast, morning and lunch restrict to daytime; late night and after hours restrict to night. No time signal means no filter. Word the topic without a time cue
Hidden and duplicate Images you manually hid, or that rank as duplicates of another shot, are always excluded. None
Minors Images flagged as showing minors are excluded while safety is safe, which is the default. safety: "any"
Cross-slot dedup Within one call no image appears in two slots. Slot order is priority order: the hero is reserved before the grid runs. None
Recent use Off unless asked. Excludes anything logged via /api/usage in the window you name. Omit exclude_recently_used_days

The gap signal#

When the curator cannot fill a slot honestly, it returns fewer images and explains itself rather than padding the count.

"gap_signal": {
  "fired": true,
  "shortfall_total": 3,
  "slots_short": [ { "role": "action_grid", "short_by": 3 } ],
  "explanations": [
    "[action_grid] Only 3 candidates show team gear as the focal
     subject; the rest had it only in branded items."
  ],
  "would_help": [
    "Close-up of 3-4 fans in team jerseys at the bar counter",
    "Wide shot of a fan crowd raising drinks together"
  ],
  "recommendations": [ { "type": "retag_pass", … } ]
}
would_help reads like a shot brief because that is what it is for.
Recommendation types, and what an agent should do with each.
typeMeaningReasonable response
retag_passLikely lookalikes exist but were tagged too loosely.Tell the customer a re-scan would probably surface them.
generateThe library genuinely lacks the shot.Fall back to generation, or hand the customer the would_help list.
broaden_topicThe slot rules were too narrow for this library.Loosen a must_have and call again.
Do not pad

The curator is built to return fewer images rather than weak ones. If your layout needs a guaranteed count, broaden the topic, lower the quality threshold or generate, but do not quietly fill the gap with whatever came back. That is the decision the gap signal exists to hand you rather than make for you.

Workspaces and verticals#

Every call is scoped to the workspace its key unlocks. A workspace declares a vertical, which decides both the analyzer overlay applied to its photographs and the layout family /api/page can reach.

The seven verticals.
VerticalAdds
universalThe base schema only. The default when nothing more specific fits.
hospitality-sportsTeam markers, jerseys, what is playing on screen. Unlocks the sports layout.
restaurant-barFood and drink detail. Unlocks the food-drink layout.
wedding-venueCeremony phase and wedding-specific subjects.
real-estate-listingRoom type and property detail.
ecommerce-productProduct presentation and packshot signals.
automotive-listingVehicle detail and listing angles.

A workspace can also carry a free-text overlay appended to the analyzer prompt for that tenant only, and a manual synonyms table for slang the catalog would never see on its own. Both are configured by the customer. Your agent does not need to touch either. /api/page already knows the customer's domain.

Limits, ceilings and cost#

Enforced in code. Requests above a ceiling are clamped, not rejected.
Limit Default Ceiling
Anonymous photo reads, per hour, per connection55
Anonymous upload size8 MB8 MB
/api/search results24500
/api/usage items per POSTn/a100
/api/usage/recent lookback, days14365
/api/usage/recent items100500
/api/urls ids per calln/a500
/api/match-ids ids returned500500
Candidates shortlisted per slot2560
Model spend per curated call$0.50$5.00

Cost is reported, not estimated: every curated response carries diagnostics.claude_cost_usd for that exact call, alongside the token counts and the model used. Budget from that number rather than from a table on a marketing page. Uncurated calls, /api/search, the usage endpoints, everything under the remaining endpoints, run no model and cost nothing.

The spend cap is a circuit breaker rather than a quota. Before curating, the pipeline estimates the cost of the slots in the request; if that estimate exceeds cost_cap_usd, curation is skipped and the slots fall back to deterministic ranking. The response says so in diagnostics.cost_cap_hit, so a call never fails silently or bills past its ceiling.

Errors and retries#

Errors on the keyed endpoints.
Status Body What to do
400 topic_required · slots_required · drive_id_required · items_must_be_list · invalid_json A structured error names the field. Fix the request; retrying it unchanged will fail identically.
401 authentication required The key is missing, malformed or revoked. Ask your user for a new one; there is no endpoint that issues one.
404 n/a On /api/image/{drive_id}: the id is not in this workspace. Check the key is for the tenant you meant.
500 error plus a short trace excerpt Retry with exponential backoff (1s, 2s, 4s, 8s) and give up after four attempts.
Repeatability

Read endpoints are idempotent. /api/page and /api/brief are not: ties in the candidate scores break non-deterministically, so the same request can return a different selection. To make a call reproducible, pass an explicit exclude_image_ids list.

One more shape worth knowing: an anonymous request to a keyed endpoint does not always return 401. Signed-out browser traffic is redirected to sign-in before the endpoint runs, so a client that follows redirects can receive an HTML page with a 200. Send the Authorization header on every keyed call and the question never arises.

The whole reference, in one fetch.

/llms-full.txt carries this page as plain text for a context window. /llms.txt is the shorter index. Neither needs an account.