← heart.photos

heart.photos — the photo library

One library of photographs, used by he.art (where photos are items you edit), avif.he.art (an AVIF converter that files what it converts), and this gallery. Upload a picture through any of them — or through the API or MCP below — and every one of them sees it. This page is the honest account: what it does, how to drive it, and where its edges are.

What it does

Where its edges are

Weaknesses, stated plainly — judge for yourself whether they matter for your use.

Getting a token

Two ways, both producing a heart_… bearer token:

  1. By hand: sign in at he.art → avatar → API tokens → New. The secret is shown exactly once.
  2. By exchange (for products): verify your user with Google Sign-In, then post the ID token here. The library verifies it itself and answers with a token for that account. One token per (account, product) — exchanging again replaces it.
    curl -s https://heart.photos/api/v1/token \
      -d '{"google_id_token":"eyJ…","product":"my-app"}'
    → {"token":"heart_…","sub":"1044…","email":"…"}

The API (v1)

All under https://heart.photos/api/v1, JSON in and out, Authorization: Bearer heart_…. The v1 prefix is a promise: these paths and shapes stay.

RouteWhat it does
GET /meWho the token is. The cheap way to test one.
GET /assetsYour photos, newest first, with URLs, variants, pairing, visibility.
POST /assetsMultipart upload: file, optional name, quality (1–100, default 60). Duplicate bytes return the existing photo with duplicate_of.
GET /assets/{id}One photo.
PATCH /assets/{id}Any of name, description, rotation, visibility.
DELETE /assets/{id}Drop your reference; bytes go only when nothing else points at them.
POST /assets/{id}/pairForm field with=<id> joins wide+tall; empty splits.
POST /assets/{id}/tags{"tags":[…]} — the photo carries exactly these words afterwards.
GET /blobs/{sha256}Do you already hold these bytes? 200 with the photo, or 404 — the way to skip a 25 MB upload. Scoped to you: it never reveals other accounts' holdings.
POST /assets/{id}/candidatesRe-render at another quality/speed; a candidate is kept until one is chosen.
POST /assets/{id}/choose/{candidate}Serve this rendering from now on.
POST /assets/{id}/variants/{kind}Make mobile, 1080p or 4k.
GET /assets/{id}/rawThe served AVIF bytes, owner-authenticated - works for private photos. ?variant=source for the untouched original, ?candidate=<id> for one named rendering rather than whichever is being served - which is what lets two renderings of the same photograph be compared against each other. A named rendering is immutable, so it is cached outright.
GET /tagsYour tag vocabulary.
GET /changes?since=NYour events after cursor N, oldest first: created deleted renamed described rotated visibility paired unpaired tagged. Keep next and hand it back.

Upload example

curl -s https://heart.photos/api/v1/assets \
  -H "Authorization: Bearer $TOKEN" \
  -F [email protected] -F name="Sunset at the pier" -F quality=60

The MCP

The library speaks the Model Context Protocol, so an AI agent handed a token can work with your photographs like any other product. Streamable HTTP, stateless, at https://heart.photos/mcp; every request carries the same Authorization header.

Tools: whoami, list_photos, get_photo, upload_photo (fetches a public http(s) image URL server-side), delete_photo, set_visibility, rename_photo, describe_photo, pair_photos, set_tags, check_hash. They are the API above wearing names an agent can pick from a list — the two cannot drift apart, because the tools call the API.

Connecting (Claude Code / any MCP client)

{
  "mcpServers": {
    "heart-photos": {
      "type": "http",
      "url": "https://heart.photos/mcp",
      "headers": { "Authorization": "Bearer heart_…" }
    }
  }
}

Or: claude mcp add --transport http heart-photos https://heart.photos/mcp --header "Authorization: Bearer heart_…"

For product builders

The intended shape: your product signs users in with Google, exchanges the ID token for a library token once per sign-in, and then talks v1 on the user's behalf. Identity is verified by the library itself on every exchange — your product is never trusted about who someone is. Use by-hash before uploading, consume /changes instead of re-listing, and never key anything on email.