# Images

How to put a photo on a page when all you have is a URL: the upload route, storage keys, kinds and size caps.

Image fields — `avatar`, `theme.backgroundImage`, a link's `image` — accept only **storage keys**, never URLs. A key is what `POST /api/upload` returns after it has fetched, re-encoded (AVIF, EXIF stripped) and stored the picture. This keeps third-party pixels off pages that promise not to track anyone.

## Upload by URL

```bash
curl -X POST https://tapmy.link/api/upload \
  -H "Authorization: Bearer tapmy_sk_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/photo.jpg", "kind": "avatar" }'
```

```json
{ "key": "avatar/66dc…/3f9c….avif", "bytes": 84120, "width": 1000, "height": 1000, "lqip": "data:image/webp;base64,…" }
```

Then save the key into the page:

```bash
curl -X PATCH https://tapmy.link/api/profiles/{id} -H "Authorization: Bearer …" \
  -H "Content-Type: application/json" -d '{ "avatar": "avatar/66dc…/3f9c….avif" }'
```

## Kinds and caps

| `kind` | Used for | Max source size | Stored at |
| --- | --- | --- | --- |
| `avatar` | the profile photo | 8 MB | ≤ 1000 px wide, ≈300 KB |
| `card` | a link block's `image` | 8 MB | ≤ 1400 px wide, ≈350 KB |
| `background` | `theme.backgroundImage` | 30 MB | ≤ 1600 px wide, ≈600 KB |

## Where the crop is anchored

Templates crop: the photo is round, the background is full-bleed, a card image is a strip. Each image field has a companion anchor — `avatarFocus`, `theme.backgroundFocus`, a link's `imageFocus` — as `{ "x": 0–100, "y": 0–100 }` percent of the picture, the point that stays in view. Absent or `null` means the centre. A face in the top-right of a landscape photo wants something like `{ "x": 70, "y": 25 }`.

```bash
curl -X PATCH https://tapmy.link/api/profiles/{id} -H "Authorization: Bearer …" \
  -H "Content-Type: application/json" -d '{ "avatar": "avatar/…avif", "avatarFocus": { "x": 70, "y": 25 } }'
```

At creation, `page.avatarFocus` applies to the photo fetched from `avatarUrl`. The MCP tool `set_avatar_from_url` takes a `focus` argument.

## Multipart

The same route accepts `multipart/form-data` with fields `file` and `kind` — what the editor uses — if you do have the bytes.

## Shortcut at creation

`POST /api/agent/pages` takes `avatarUrl`; the server does the fetch and the save for you. A failure there is reported in `warnings` and does not stop the page.

## Errors

`bad_url`, `blocked` (private address, or the host refused us), `unreachable`, `too_large`, `not_image`, `processing_failed`. HEIC from an iPhone is accepted. Instagram CDN URLs expire within days — copy them at once, which is what `avatarUrl` does.

---
Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/images.md
