PhotoPromptingjoin
hermit api · reference

A distinct voice, as an API.

Hermit is a personality distilled from thousands of community “takes” on music. Give it a song and it returns a one-line take in that voice; ask it to write and it produces longer prose in the same register. Text in, text out — the model reads a song's title, artist, and the community's own corpus — never any third-party media, lyrics, or audio. One POST, no key to start.

▶ try it live — paste a song, watch the api answer →the doors →read the voice →

Quickstart

try every door live → /doors — the read, the card, the playlist, the embed, the filing and the wire, with real data and the exact call under each. then /recipes — one hundred ways to put a line to work, each with the call. the rest of the house for developers: connect an assistant · the bot · pricing · credits · a key · the license.

One call. Pass a Spotify track id, get a take back. No authentication for the shared daily read — paste this into a terminal and it works right now:

curl
curl -X POST https://photoprompting.org/api/hermit/interpret \
  -H "Content-Type: application/json" \
  -d '{ "spotifyId": "0bu0to8kgrXDzWklgZ3BuQ" }'
JavaScript
const res = await fetch(
  "https://photoprompting.org/api/hermit/interpret",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ spotifyId: "0bu0to8kgrXDzWklgZ3BuQ" }),
  }
);
const { interpretation, source } = await res.json();
console.log(source, "→", interpretation);
Python
import requests

r = requests.post(
    "https://photoprompting.org/api/hermit/interpret",
    json={"spotifyId": "0bu0to8kgrXDzWklgZ3BuQ"},
)
data = r.json()
print(data["source"], "→", data["interpretation"])

You get back a single line and the tier that produced it:

200
{
  "interpretation": "reminds me of that warehouse where nobody talked for hours",
  "source": "claude",
  "cached": true
}

POST /api/hermit/interpret

The core endpoint. Provide a song; get a one-line take. No auth required for the cached read.

Any Spotify track works. Unknown spotifyIds are resolved through the official Spotify API and join the catalog automatically on first request (rate-limited; the response carries discovered: true). The catalog doesn't gate the API — the API grows the catalog.

Request body:

FieldTypeNotes
spotifyIdstringAny Spotify track id — known or not. Provide this or songId.
songIdstringPhotoPrompting song UUID. Provide this or spotifyId.
useClaudebooleanOptional, default true. Set false to force Tier 1 community retrieval only.
bundlebooleanOptional. true (or ?bundle=1) attaches the bundle — the take packaged with its song: the element it comments on, preview, art, and doors.

Response fields:

FieldTypeMeaning
interpretationstring · nullThe one-line take. null only when a Tier 1-only request finds no take.
sourcestringcommunity or claude — which tier answered.
qualitynumber0–1. Community = votes-derived; generated = 0.7 until users vote on it.
cachedbooleanTier 2 only. true = the shared daily generation; false = freshly generated.
provenanceobjectOrigin metadata; shape depends on the tier (see below).
modelstringThe model or source id that produced the line.
fresh_remainingnumberKeyed ?fresh=1 requests only — your remaining daily fresh quota.
bundleobjectOnly when requested — see the bundle.

A Tier 2 (generated) response in full:

200 — generated
{
  "interpretation": "reminds me of that warehouse where nobody talked for hours",
  "source": "claude",
  "quality": 0.7,
  "cached": false,
  "provenance": {
    "song_id": "dff32426-…",
    "spotify_id": "0bu0to8kgrXDzWklgZ3BuQ",
    "model": "claude-haiku-4-5",
    "exemplar_count": 5
  },
  "model": "claude-haiku-4-5"
}

A Tier 1 (community) response carries the original author's provenance instead:

200 — community
{
  "interpretation": "this song looks like hawaii",
  "source": "community",
  "quality": 0.5,
  "provenance": {
    "take_id": "14bfce98-…",
    "author": "mclary",
    "votes": 5,
    "quality_score": 5
  },
  "model": "hermit-community-v0"
}

GET /api/hermit/read — the simplest door

Everything /interpret does, reachable from a URL bar. Paste a whole Spotify share link — no JSON, no POST body. Built for Apple Shortcuts, curl, spreadsheet formulas, and every bot framework’s laziest HTTP call. Same tiers, same caching, same limits underneath.

ParamNotes
urlA full Spotify share link (https://open.spotify.com/track/…). Or use song with a bare track id.
formattext returns the bare line + credit as plain text — perfect for notifications. Omit for JSON (line, element, song, artist, preview, doors).
community1 forces the community tier (a filed take, credited — never costs a fresh read).
fresh1 forces fresh generation — metered, same as /interpret.
try it
curl "https://photoprompting.org/api/hermit/read?url=https://open.spotify.com/track/75IQVo8hqI1iwVZyvkN2VT&format=text"

The iPhone/Mac shortcut, three actions. Open the Shortcuts app → new shortcut → enable “Show in Share Sheet” (accepts URLs):

#ActionSet it to
1Receive inputURLs from the Share Sheet (share any song from the Spotify app)
2Get Contents of URLhttps://photoprompting.org/api/hermit/read?format=text&url= + Shortcut Input
3Show Resultthe line appears; long-press to copy or share it onward

Name it “what does the hermit think”. Anonymous quota applies (20 fresh reads a day per reader, from a public allowance of 150); cached and community reads don’t count against it.

GET /api/hermit/card — the read as a picture

The same answer /read gives, drawn on the wall’s index card: the line in serif, the record, the credit, the address. One PNG, for anything that posts images rather than unfurling links — Bluesky, a Discord bot, a newsletter, a script that saves to Photos. Same tiers and limits as /read; pass your key the same way and fresh reads land on your ledger. ?take= draws a filed take as filed, no generation. Bad input comes back as a PNG too (an <img> has no other way to say so): 400 when the link is not a track, 404 when the song has no line yet. The status also rides in an X-Card-Status header.

ParamNotes
urlA Spotify share link — ?si= junk and the “check out this song” wrapper are fine. Or song with the bare 22-character track id.
takeA take id or its 8-character prefix (the /t/ short form) — the line is the take, the credit is the writer; skips the voice entirely.
formatcard 1200×630 (default, the ivory index card) · story 1080×1920 · square 1080×1080 · discord 1040×440 — the dark embed the Discord bot answers with, as a picture.
compact1 renders the card at 1000×525, under Bluesky’s ~976 KB image cap.
community1 — filed lines only, zero cost, never generates.
fresh1 — skip the 24h cache; metered on your tier exactly like /read.
headersauthorization / x-api-key pass through to the voice. Cache-Control: public, max-age=86400; CORS open.
try it
curl -o card.png "https://photoprompting.org/api/hermit/card?url=https://open.spotify.com/track/75IQVo8hqI1iwVZyvkN2VT&format=square"

GET /api/hermit/playlist — a whole playlist, one line each

Hand it the tracks, get a line per song. Spotify stopped sharing playlist contents with server apps (the name arrives, the tracks don’t — even for public playlists people made), so the door that works today is ?ids=: a comma list of track ids, read by whatever holds the listener’s own Spotify session — Raycast, a browser, a script. A playlist link is still accepted and will start working the day Spotify lets it. Each track is read community-first (a filed take, free); only a miss falls through to the normal read — cached, then fresh. Fresh generations are capped at 6 per call on top of your daily tier, so one link cannot spend the day: tracks past the cap with nothing filed and nothing cached come back line: null, reason: "quota". Reads run four at a time. ?format=card draws the first 12 rows on one 1080×1350 card. Spotify-owned and editorial playlists are not shared with apps anymore (the November 2024 restrictions) — those 404 with a note; playlists people made and left public work.

ParamNotes
idsComma-separated Spotify track ids (up to 30). The path that works today. name labels the card (optional).
urlA Spotify playlist share link, or id with the bare 22-character playlist id — answers 404 with a note until Spotify opens playlist contents to server apps again.
nTracks to read — default 12, max 30 (the card always takes the first 12).
formatOmit for JSON · card for the PNG grid (cached 1h).
community1 — filed lines only, never generates.
headersauthorization / x-api-key pass through so fresh reads land on your ledger; CORS open.
200
{
  "name": "…", "total": 38, "shown": 12,
  "tracks": [
    { "id": "75IQVo8hqI1iwVZyvkN2VT", "title": "Show Me How", "artist": "Men I Trust", "art": "https://…",
      "line": "this song reminds me of waiting in a coffee shop that's trying too hard to be cool",
      "source": "community", "take_id": "f9fdc3d9-…", "author": "Matthew Clary", "url": "/s/75IQVo8hqI1iwVZyvkN2VT" }
  ],
  "quota": { "fresh_used": 0, "fresh_cap": 6 }
}
try it
curl "https://photoprompting.org/api/hermit/playlist?ids=75IQVo8hqI1iwVZyvkN2VT,0bu0to8kgrXDzWklgZ3BuQ&format=card" -o playlist.png

POST /api/v1/takes — filing from anywhere

The read goes out; this is the door in. One line about one element of a Spotify song, filed from a bot, a script, a spreadsheet — anything that can send JSON with a key. The same gate the site uses, with walls in front of it. The take comes back with its address on the wall. Header x-api-key is required; there is no anonymous filing.

FieldNotes
urlA Spotify track link (open.spotify.com/track/… or spotify:track:…). Or spotifyId, the 22-character id.
textThe line. 1–280 characters after trim. No links — the record is the link.
elementOptional. One of sound, lyrics, title, canvas, art — the part of the song it is about.
authorOptional display handle, 40 characters max. @ and control characters are dropped. Default the api.
sourceOptional. discord, api, or a lowercase slug up to 20 characters. Default api; counted in analytics as take_filed_<source>.
external_idOptional, up to 64 characters — your side’s writer id (a Discord user id, say). It sets the per-writer wall; it is not stored on the take.

Unknown song? If Spotify knows the track, the record joins the catalog on the spot and the take files. If Spotify does not, 400 and nothing is written. The same line already on that record answers 200 with the existing take and "deduped": true — nothing is filed twice.

201
{
  "take": {
    "id": "a832e584-784d-401a-b960-bcbcf88f6315",
    "short": "a832e584",
    "url": "https://photoprompting.org/t/a832e584",
    "text": "this bass line walks home the long way on purpose",
    "element": "sound",
    "song": { "spotify_id": "75IQVo8hqI1iwVZyvkN2VT", "title": "Show Me How", "artist": "Men I Trust" }
  }
}
StatusMeaning
400Bad body, or Spotify does not know the track. error says which.
401No key, or a key we do not know.
422{ "blocked": true, "error": "…" } — the gate said no (a slur, a line with no words, or the wall’s own rules). The message is the reason.
429A wall. Retry-After is set.
502The wall did not answer. Try again in a minute.

Limits: 60 filings a day per key · 10 a day per writer (source + external_id) · 30 a day per address · 300 a day across everyone. Each filing also spends one fresh-read unit of the key’s daily quota — the key check and the meter are the same call for now. Reading them back: GET /api/v1/takes?song=<track id or link> — the filed takes on a record, newest first, 50 max, no key needed.

try it
curl -X POST https://photoprompting.org/api/v1/takes \
  -H "x-api-key: YOUR-API-KEY-HERE" -H "content-type: application/json" \
  -d '{"url":"https://open.spotify.com/track/75IQVo8hqI1iwVZyvkN2VT","text":"this bass line walks home the long way on purpose","element":"sound","author":"you"}'

The Discord bot files through this door too — /take link: line: part: in any server it has joined.

oEmbed — paste a take anywhere

A take is one line. It should survive being pasted into someone else’s page — a blog, a forum, a newsletter CMS, Notion. Anything that speaks oEmbed can unfurl a take link into the card without asking us. Every take page carries the discovery tag (<link rel="alternate" type="application/json+oembed">), so consumers find the endpoint on their own.

endpoint
curl "https://photoprompting.org/api/oembed?url=https://photoprompting.org/t/f9fdc3d9&format=json"
ParamNotes
urlA take page — https://photoprompting.org/t/<8 characters> or /take/<uuid>. Nothing else does (400).
formatjson. Only JSON — xml answers 501, which the spec allows.
maxwidthShrinks the card; the height follows at the same ratio. Default 560.
maxheightCaps the height and pulls the width in with it. Default 280.
200
{
  "version": "1.0", "type": "rich",
  "provider_name": "photoprompting", "provider_url": "https://photoprompting.org",
  "title": "this song reminds me of someone going to work in a bad mood",
  "author_name": "Matthew Clary", "author_url": "https://photoprompting.org",
  "html": "<iframe src=\"https://photoprompting.org/embed/take/f9fdc3d9-…?theme=light\" width=\"560\" height=\"280\" style=\"border:0;max-width:100%\" loading=\"lazy\" title=\"…\"></iframe>",
  "width": 560, "height": 280,
  "thumbnail_url": "https://photoprompting.org/api/og/take/f9fdc3d9-…", "thumbnail_width": 1200, "thumbnail_height": 630,
  "cache_age": 86400,
  "song": { "title": "Show Me How", "artist": "Men I Trust", "spotify_id": "75IQVo8hqI1iwVZyvkN2VT" }
}

The card itself lives at https://photoprompting.org/embed/take/<id>?theme=dark|light — a standalone page, no app chrome, framable anywhere: the art, the line, the writer, the record, which element it’s about, and one door back to the wall. <id> is the uuid or the 8-character /t prefix; cached an hour. On any take page the quiet row has embed next to share; tap it and the iframe snippet is on your clipboard.

Song pages — /s/<id>

Every record on the wall has one page: https://photoprompting.org/s/<spotify track id>. Paste a Spotify share link in place of the id and it redirects to the short form. The page is the record as the frame and the takes as what hangs in it — the art (tap it for the 30 seconds), title, artist, the Spotify door; what the house read, if the voice has already read this one, and how many days ago; every line filed on the record, most seen first, each linking to its own /t page; and one button to write yours, which opens the composer on that song. A page view never generates a read and never adds a record — the house only reads inside the API, and the catalog only grows when somebody files. Records with nothing on them yet are followed by search engines but not indexed. The url field on every /playlist track and the footer of every /card point here.

The wire

Every midnight UTC the wall crowns one take; at 13:00 UTC the site posts it — the card as the picture, the line, the writer and the record under it, the /t/ link — to Bluesky, Mastodon and Threads. Nobody presses anything; the same take everyone sees on /today is the one that goes out, and the site keeps a ledger so a day is posted once. Follow it where you already are: @photoprompting.bsky.social · @[email protected] · @photoprompting on Threads. One take a day, human lines only, never an ad.

MCP — hermit as a tool inside your assistant

The short version for anyone: photoprompting.org/connect — pick your assistant, three steps, done.

A remote Model Context Protocol server at https://photoprompting.org/api/mcp. Add it once to Claude, Cursor, Claude Code, or anything else that speaks MCP, and “what does the hermit think of this song” works wherever the conversation already is — no account, no key; the assistant signs in once on one screen. Streamable HTTP, stateless, CORS open. The hermit here is a librarian to the wall, not a writer on it: reads hand back a human line first, credited, and the house voice only when nobody has filed one. No Spotify data goes to the assistant: songs are named as MusicBrainz, the open music encyclopedia, lists them, and cards are handed over as links.

ToolWhat it does
find_songSpotify search → up to 5 tracks with the 22-character id every other tool takes, named as MusicBrainz lists them. A track MusicBrainz does not know comes without a name.
read_songThe line about a track, with its credit and the song page. fresh: true asks the house for a new read (metered).
speak_takeFor spoken conversations: the same read as one plain sentence to say aloud — the record, the credit, the line — and nothing else. Every read also carries a spoken field.
song_cardThe read on the index card: the card’s address, for the person to open. The picture itself never goes to the assistant, because it prints Spotify’s song details. take: renders a filed take.
read_playlistUp to 12 tracks, one line each, plus the grid card’s address. Pass the track ids or links — Spotify keeps playlist contents from server apps.
takes_on_songEvery human line filed on a record, with its /t/ address. Free.
file_takeFiles the human user’s own line under the human’s handle. The tool refuses model-shaped authors; the description tells the assistant never to file words the user did not dictate. Ten a day per handle.
claude desktop · claude.ai
Settings → Connectors → Add custom connector
  name:  photoprompting
  url:   https://photoprompting.org/api/mcp
.cursor/mcp.json
{
  "mcpServers": {
    "photoprompting": { "url": "https://photoprompting.org/api/mcp" }
  }
}
claude code
claude mcp add --transport http photoprompting https://photoprompting.org/api/mcp
chatgpt (plus / pro / business)
Settings → Security and login → Developer mode: on
Settings → Apps (or Connectors) → Create → MCP server URL:
  https://photoprompting.org/api/mcp
  authentication: OAuth  →  sign in on the one screen  →  let it in
then in a chat: + → the photoprompting app → ask about a song

It works by voice. In the Claude app, tap the microphone and ask what the hermit thinks of a song — the assistant calls speak_take and says the line word for word, credit first, no addresses read aloud. Filing by voice reads your line back before it goes on the wall.

Every client signs in once when you add it — one screen, no account, no password: it names the app asking, you can tell the wall what to call you (lines you file carry that name), and you let it in. The pass renews itself for a month; delete the connector to end it sooner. Standard OAuth 2.1 with dynamic registration and PKCE, so it works the same in Claude, Cursor, Claude Code, and anything built on the MCP SDKs.

Bring a key and it rides along: clients that send headers can set Authorization: Bearer <your api key>, which reaches every door as x-api-key — fresh reads land on your ledger and filings carry your key. Without one, reads share the anonymous allowance and filings go through the house’s own key, tagged source: mcp. The machine-readable spec for everything on this page is /openapi.json; the plain-text version for agents is /llms.txt (long form: /llms-full.txt); the discovery manifest is /.well-known/mcp.json.

The two tiers

One endpoint, two quality levels — it automatically uses the highest one available:

Tier 1
community

Returns a real high-voted community take. Authentic, instant, free. Limited to songs the community has seen.

Tier 2
claude

Claude writes a fresh first-person take from text only — the song's name as MusicBrainz lists it (never Spotify's), or its human takes when MusicBrainz does not know it, steered by the distilled community voice + high-voted exemplars.

The source field in the response tells you which tier answered.

The bundle

A take is shown with its song. The bundle ships both in one payload — the take, the element it comments on (half are about the sound, a quarter about the canvas or art), the 30-second preview, the art, and the doors — so an embed, a bot, or a newsletter template can show them together without understanding the site.

Add "bundle": true to the body (or ?bundle=1) on /api/hermit/interpret. Works on every tier — generated, cached, and community reads (community bundles also carry the take's permanent atom url).

200 — with bundle
{
  "interpretation": "this song looks like hawaii",
  "source": "community",
  "bundle": {
    "line": "this song looks like hawaii",
    "element": {
      "class": "canvas",
      "stamp": "▞ the canvas",
      "phrase": "the canvas of"
    },
    "evidence": {
      "class": "look-canvas",
      "stamp": "▞ the canvas",
      "deliver": "the take is about the canvas — send the reader through the spotify door; it only plays there"
    },
    "song": {
      "spotify_id": "3AJwUDP919kvQ9QcozQPxg",
      "title": "Yellow",
      "artist": "Coldplay",
      "album_art_url": "https://i.scdn.co/image/…"
    },
    "preview": null,
    "doors": {
      "spotify": "https://open.spotify.com/track/3AJwUDP919kvQ9QcozQPxg",
      "embed": "https://open.spotify.com/embed/track/3AJwUDP919kvQ9QcozQPxg?theme=0",
      "console": "https://photoprompting.org/doors?song=3AJwUDP919kvQ9QcozQPxg",
      "atom": "https://photoprompting.org/t/14bfce98-…"
    }
  }
}

element.class is the part of the song the line is about — one of title · lyrics · canvas · art · sound: the writer's declared element when the take was filed with one, else classified from the text. stamp and phrase are the display forms. evidence.class is the legacy finer grain, one of sound · look-canvas · look-art · memory · words · moment, classified deterministically from the text — no model call, nothing sent anywhere. deliver is the rendering instruction: play the preview under sound-takes, show the art beside picture-takes, send canvas-takes through the Spotify door (the canvas only plays on Spotify — the bundle never carries canvas urls). preview is always null now: play the song through doors.embed, Spotify’s own player. A clip from another service may not sit beside Spotify’s cover art and song details, so the bundle no longer carries one. Bundles are computed on demand and never cached stale.

try the bundle live →

POST /api/hermit/prose

The same voice, longer form. Ask Hermit to write an about blurb, a vignette, a deadpan review, a launch post — grounded on the identical distilled personality plus real community takes as anchors. A GET to the same URL lists the available forms.

Request body:

FieldTypeNotes
formstringabout, vignette, review, riff, launch-post, micro-essay, or story. Defaults to about.
topicstringWhat to write about. Optional for about (defaults to PhotoPrompting itself); required otherwise.
lengthstringOptional. short or medium (default).
curl
curl -X POST https://photoprompting.org/api/hermit/prose \
  -H "Content-Type: application/json" \
  -d '{ "form": "review", "topic": "a 24-hour laundromat at 2am" }'

Response:

200
{
  "form": "review",
  "topic": "a 24-hour laundromat at 2am",
  "prose": "the fluorescent light is the honest kind…",
  "voice": { "grounded": true, "profile_source": "distilled", "anchor_count": 8 },
  "drafts": 1,
  "model": "claude-sonnet-4-5"
}

voice.grounded is true when the piece was anchored on the live distilled profile (vs. the static persona). Prose is rate-limited and honors the samex-api-key header as interpret.

POST /api/v1/voice — the licensed endpoint

The productized voice: key-required, metered, and every response stamped with provenance (voice version, corpus size) plus the voice license — outputs are yours commercially; the corpus, profile, and voice itself stay ours. Style isn't copyrightable; a corpus, a distilled profile, metered access, and a contract are.

FieldTypeNotes
promptstringRequired. What to write about.
formstringSame seven forms as prose. Defaults to riff.
lengthstringOptional. short or medium.
bestbooleanOptional. true → three drafts + a judge; costs 3 quota units.
voice_versionstringOptional pin. The corpus re-distills weekly; a drifted voice returns 409 with current_version instead of silently changing register.
curl
curl -X POST https://photoprompting.org/api/v1/voice \
  -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"prompt":"an apology to my houseplants","form":"riff","best":true}'

No key yet? Request one — reviewed personally. The doors and /api/hermit/* remain the free taste.

POST /api/hermit/audition — the imitation judge

The public toy: submit one line written as if you were the voice, and the distilled profile judges it — a 0–100 match score, a deadpan verdict delivered in the voice, and “the tell” that gave you away. Scores of 75+ earn a pointed suggestion to visit the license. No key needed; six auditions per ten minutes per IP.

FieldTypeNotes
textstringRequired. Your one-line attempt (≤280 chars).
songTitlestringOptional. Lets the judge weigh topicality, not just register.
songArtiststringOptional, pairs with songTitle.
curl
curl -X POST https://photoprompting.org/api/hermit/audition \
  -H "Content-Type: application/json" \
  -d '{"text":"this song sounds like my landlord apologizing"}'

Returns { score, verdict, tell }. Try it interactively in the audition booth on the doors.

GET /api/hermit/voice-profile

The aggregated personality, distilled from a broad random sample of the corpus. Useful for showing “who” is writing — and it's the same profile that steers Tier 2/3 takes and all prose.

200
{
  "profile": {
    "summary": "a detached night-owl who reads music like overheard conversations…",
    "moves": ["narrates the scene as a fragment of a bigger story", "…"],
    "vocabulary": ["lowercase everything", "food and place metaphors", "…"],
    "avoids": ["superlatives", "music-theory talk", "…"]
  },
  "corpus_size": 534,
  "sample_count": 150,
  "model": "claude-sonnet-4-5",
  "source": "distilled"
}

The embed widget

No code at all. Paste this anywhere on your page and Hermit's take on the track renders inline, auto-sized:

HTML
<script src="https://photoprompting.org/embed.js"
        data-pp-embed="0bu0to8kgrXDzWklgZ3BuQ"
        data-theme="dark" async></script>

data-pp-embed accepts a Spotify track id, a Spotify track URL, or a PhotoPrompting song UUID. data-theme is dark (default) or light. Multiple embeds per page are fine. A track outside the catalog renders a “be the first to read it” prompt instead of failing.

Fresh generation & API keys

By default you get the cached take — one generation per song per day, shared, instant, and free ("cached": true in the response). Append ?fresh=1 to force a brand-new generation on demand. Fresh is the metered feature:

FREE · no key
A taste of fresh — up to 20 per reader per day, from a public allowance of 150. When the public allowance is spent you still get the cached or filed read; only an empty record answers 429. Enough to evaluate; not for production.
MEMBER · signed in, no key
A free account lifts the taste to 12/day, tied to the account (works on shared IPs). The site sends it automatically; programmatic callers can pass the session as x-user-token.
KEYED · with an API key
A daily fresh quota tied to your key (25–1,200/day by plan). The response includes fresh_remaining.

Pass your key in the x-api-key header:

Keyed fresh request
curl -X POST "https://photoprompting.org/api/hermit/interpret?fresh=1" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pp_live_…" \
  -d '{ "spotifyId": "0bu0to8kgrXDzWklgZ3BuQ" }'

An invalid key returns 401; an exhausted quota returns 429. See pricing & plans, or get in touch.

Pricing

Cached and community reads are free and unlimited. You only pay for fresh on-demand generations (?fresh=1) — each is a live model call.

PlanFresh / dayPrice
Taste5 · no keyFree
Developer25Free
Starter100$29 / mo
Pro350$99 / mo
Business1,200$299 / mo
Scalecustomcontact

Plans and checkout on the pricing page.

Rate limits

Limits are per-IP token buckets; a keyed fresh quota is per-key. Over a limit returns 429; windowed limits include a Retry-After header (seconds).

ScopeLimitApplies to
interpret · overall100 / 10 minEvery /interpret call, cached or fresh.
interpret · free fresh20 / day · 150 shared?fresh=1 without a key. Per reader, from one public allowance; falls through to cached/filed reads when spent.
interpret · keyed freshdaily quota?fresh=1 with a key — see fresh_remaining.
v1/takes · filing60 / day per keyPlus 10 / day per writer, 30 / day per address, 300 / day across everyone.
prose8 / 10 minPer IP, plus a global ceiling. Honors x-api-key.

Errors

Errors are JSON: { "error": "…" }. The status code is the contract:

StatusMeaningWhen
400Bad requestMalformed JSON, or missing a required field (e.g. neither spotifyId nor songId, or a prose topic).
401UnauthorizedInvalid API key on a ?fresh=1 request.
404Not foundTrack isn't in the catalog, or a Tier 1-only request found no take yet.
429Rate limited / quotaOver a rate limit or out of fresh quota. Check Retry-After.
502 / 503Generation failedModel call failed (retry), or the AI backend isn't configured.
500Internal errorTransient — safe to retry with backoff.

Machine-readable

For agents and tooling:

/openapi.json/llms.txt
pricing →the doors →the voice →← the wall