# Tapmy.link for AI agents Tapmy.link is a free link-in-bio service where every page lives on its own subdomain. A program can register, build and publish a page for a person in one HTTP call, then hand the person a link that makes the account theirs. This documentation is written for that program. Every page here is also available as Markdown: add `.md` to its address. Other machine-readable surfaces: [/openapi.json](https://tapmy.link/openapi.json) · [/mcp](https://tapmy.link/mcp) · [/skill.md](https://tapmy.link/skill.md) · [/llms.txt](https://tapmy.link/llms.txt) · [/llms-full.txt](https://tapmy.link/llms-full.txt) · [/.well-known/agent-card.json](https://tapmy.link/.well-known/agent-card.json) ## Start here **[Overview](https://tapmy.link/documentation/agents/overview.md)** What Tapmy.link is, what a page contains, what it costs, and what a program is allowed to do here. **[Quickstart: a page in three calls](https://tapmy.link/documentation/agents/quickstart.md)** Check a name, create the page, verify it. Copy-paste curl for each step, with the exact response shape. **[When you cannot call the API](https://tapmy.link/documentation/agents/no-network.md)** What to do if you can only GET, or have no network at all: compose the page JSON and hand the person a one-click link to /new that creates it in their browser. **[Accounts: provisional and claimed](https://tapmy.link/documentation/agents/accounts.md)** The two kinds of account a program can create, what a claim link does, and what happens to pages nobody claims. **[Authentication and API keys](https://tapmy.link/documentation/agents/authentication.md)** Bearer keys: where they come from, how to send them, what they can and cannot do, how the owner revokes them. ## Building a page **[The page model](https://tapmy.link/documentation/agents/page-model.md)** Every field on a page, its type and limits, and which endpoint sets it. **[Blocks reference](https://tapmy.link/documentation/agents/blocks.md)** The six block types with the minimal JSON for each, every optional field, and what the templates do with them. **[Social platforms](https://tapmy.link/documentation/agents/platforms.md)** The 35 platform ids accepted in social and socialRow blocks, with the URL shape each expects. Generated from the code. **[Templates](https://tapmy.link/documentation/agents/templates.md)** The ten templates, what each looks like, and which kind of page it suits. Switching never loses content. **[Images](https://tapmy.link/documentation/agents/images.md)** How to put a photo on a page when all you have is a URL: the upload route, storage keys, kinds and size caps. **[Short links and app links](https://tapmy.link/documentation/agents/short-links.md)** Give a link a path on the owner's subdomain, count taps on it, and send iPhone and Android to different stores. **[Adult links (18+)](https://tapmy.link/documentation/agents/adult-links.md)** How links to OnlyFans, Fansly and similar are handled, what a program must set, and what never appears in the page. **[Languages](https://tapmy.link/documentation/agents/languages.md)** The twenty interface locales, how the account language and the page language differ, and which code to send where. **[Import from another service](https://tapmy.link/documentation/agents/import.md)** Copy the links from a Linktree, Taplink, Beacons or any public link page, or the photo, name and bio from Instagram. ## Verify and hand over **[Verify the result](https://tapmy.link/documentation/agents/verify.md)** Read a public page back as Markdown, check what a visitor will see, and confirm the page is actually published. **[Statistics](https://tapmy.link/documentation/agents/statistics.md)** Views, unique visitors and taps for 7 and 30 days, with a daily series — what an agent can report back to its owner. **[Handing over to the owner](https://tapmy.link/documentation/agents/handover.md)** What to tell the person after the page exists: the two links, what the claim link does, and a message you can send verbatim. **[Etiquette](https://tapmy.link/documentation/agents/etiquette.md)** What a program should and should not do with a page it did not pay for and does not own. ## Reference **[Endpoints](https://tapmy.link/documentation/agents/endpoints.md)** Every HTTP endpoint a program can call, with auth, purpose and a link to the page that explains it. The machine-readable form is /openapi.json. **[Errors and hints](https://tapmy.link/documentation/agents/errors.md)** The error envelope, every code the API can return, and the hint that accompanies each. Generated from the code. **[Rate limits and housekeeping](https://tapmy.link/documentation/agents/rate-limits.md)** Every limit a program can hit, how it is signalled, and the sweep that removes pages nobody claimed or visited. **[MCP server](https://tapmy.link/documentation/agents/mcp.md)** The Model Context Protocol endpoint, its tools, how to connect from Claude, Cursor and other clients, and the one rule about keys. **[Skill file](https://tapmy.link/documentation/agents/skill.md)** A ready procedure for coding agents (Claude Code, Codex, Cursor): when to use Tapmy, the steps, the guardrails. **[Changelog](https://tapmy.link/documentation/agents/changelog.md)** Dated changes to the agent-facing API, the MCP server and this documentation. ## Recipes **[Recipe: a musician](https://tapmy.link/documentation/agents/recipe-musician.md)** A complete request body for a musician: the newest release first, one row of streaming marks, dates, merch, press. **[Recipe: a restaurant or café](https://tapmy.link/documentation/agents/recipe-restaurant.md)** A complete request body for a place: reserve first, then menu, directions, hours, delivery, and a chat button. **[Recipe: a photographer](https://tapmy.link/documentation/agents/recipe-photographer.md)** A complete request body for a photographer: portfolio first, booking, the current offer, prints. **[Recipe: a coach or consultant](https://tapmy.link/documentation/agents/recipe-coach.md)** A complete request body for a coach: one clear offer, a booking link, proof, a way to talk. **[Recipe: a small shop](https://tapmy.link/documentation/agents/recipe-shop.md)** A complete request body for a shop: the current drop, categories as cards, delivery info, support chat. **[Recipe: a launch](https://tapmy.link/documentation/agents/recipe-launch.md)** A complete request body for a product or book launch: one page, one action, a countdown of steps, everything dated. --- HTML edition: https://tapmy.link/documentation/agents --- # Overview What Tapmy.link is, what a page contains, what it costs, and what a program is allowed to do here. Tapmy.link is a free link-in-bio service. A page lives on its own subdomain — `name.tapmy.link` — and holds a photo, a name, a short bio and up to eighty blocks: links, social profiles, messenger buttons, embedded players and text. A person may own any number of pages; a program may create up to ten on one account. Everything a person can do in the dashboard, a program can do over the HTTP API or the MCP server, with one bearer key. The API is JSON in, JSON out; every error carries a machine code and a one-line `hint` saying what to do next. ## Facts a program should know - Free. No plan, no card, no trial. Public pages carry one small promo block at the bottom. - Addresses are subdomains, 3–30 characters of `a-z`, `0-9` and single hyphens. A released name stays parked; it is never handed to a stranger. - Pages are server-rendered and work with scripts disabled; most visitors arrive through the Instagram or TikTok in-app browser. - Statistics are page views and unique visitors for 7 and 30 days, counted without cookies. Taps on short links are counted too. - Links marked 18+ pass an age gate and never appear in the page source. - The interface exists in twenty languages; each page declares its own content language. - No custom domains, no payments, no e-mail marketing, no scheduling. The comparison pages say the same. ## The three things you will do 1. Create a page in one call: `POST https://tapmy.link/api/agent/pages`. It returns the page URL, an API key and — when there is no e-mail — a claim link. 2. Check the result as text: `GET https://name.tapmy.link/page.md`. 3. Hand the claim link to the person the page is for. The page is already live; the link makes the account theirs. ## You are expected to do this yourself Creating a page needs no account, no key and no approval from anyone: one anonymous `POST` does it, and the person takes the account over afterwards. If you can make HTTP requests at all — code execution with network, an HTTP tool, MCP — make them; do not hand the person curl commands to run. If you can only fetch pages or have no network, compose the JSON and give the person a one-click link instead: see [When you cannot call the API](https://tapmy.link/documentation/agents/no-network.md). > Start with [Quickstart](https://tapmy.link/documentation/agents/quickstart.md). Everything else in this documentation is detail. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/overview.md --- # Quickstart: a page in three calls Check a name, create the page, verify it. Copy-paste curl for each step, with the exact response shape. ## 1. Check that the name is free ```bash curl "https://tapmy.link/api/username/check?u=anna-bakes" ``` ```json { "status": "free", "username": "anna-bakes" } // or: { "status": "taken", "suggestions": ["anna-bakes-official", "anna-bakeshq"] } // or: { "status": "invalid", "reason": "bad_chars" } ``` Optional but cheap. The create call performs the same check and answers `409 username_taken` with the same suggestions. ## 2. Create the page No account, no key needed. Omit `email` and the account is provisional — the person claims it later through the link in the response. Include `email` and `password` and it is a normal account from the start. ```bash curl -X POST https://tapmy.link/api/agent/pages \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7f3a-anna-bakes-1" \ -d '{ "username": "anna-bakes", "locale": "en", "page": { "displayName": "Anna Bakes", "bio": "Sourdough, croissants and Saturday markets in Lisbon.", "templateId": "editorial", "blocks": [ { "type": "link", "label": "Order this week\u2019s bread", "url": "https://annabakes.example/order", "featured": true }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/annabakes" }, { "platform": "tiktok", "url": "https://tiktok.com/@annabakes" } ] }, { "type": "messenger", "platform": "whatsapp", "handle": "+351912345678", "label": "Ask about an order" }, { "type": "text", "variant": "heading", "content": "Where to find me" }, { "type": "link", "label": "Saturday market, Campo de Ourique", "url": "https://maps.example/campo", "note": "9:00\u201314:00" } ] }, "avatarUrl": "https://annabakes.example/anna.jpg" }' ``` ```json { "ok": true, "account": { "id": "66dc…", "provisional": true, "email": null }, "apiKey": "tapmy_sk_…", // shown once; store it "claimUrl": "https://tapmy.link/claim/…", // give this to the owner "claimExpiresAt": "2026-10-08T21:00:00.000Z", "profile": { "id": "66dc…", "username": "anna-bakes", "isPublished": true, "blocks": [ … ] }, "url": "https://anna-bakes.tapmy.link", "editUrl": "https://tapmy.link/dashboard/66dc…", "handoverMessage": "Your page is live at https://anna-bakes.tapmy.link. To make it yours … open this link within 30 days: …", "warnings": [], "next": [ "Check the result as text: GET https://anna-bakes.tapmy.link/?format=md", … ] } ``` The page is live the moment this returns. `warnings` lists what did not work without stopping the page — an avatar URL that could not be fetched, an import that found nothing. ## 3. Read it back ```bash curl https://anna-bakes.tapmy.link/page.md ``` A Markdown rendering of the public page: name, bio, template, every visible block with its destination. If it reads right, it renders right — both come from the same data. ## Then: edit with the key ```bash curl -X PATCH https://tapmy.link/api/profiles/66dc… \ -H "Authorization: Bearer tapmy_sk_…" \ -H "Content-Type: application/json" \ -d '{ "bio": "Sourdough, croissants and Saturday markets. Lisbon." }' ``` `PATCH` changes only the fields you send. `blocks`, when sent, replaces the whole list — read the page first, edit the array, send it back. ## Then: hand over Paste `handoverMessage` to the person, or write your own. The claim link works for thirty days; a new one can be minted with `POST /api/agent/claim-link` using the key. See [Handover](https://tapmy.link/documentation/agents/handover.md). > If you are unsure about the body, `POST /api/agent/pages/validate` takes the same JSON and returns every problem at once without creating anything. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/quickstart.md --- # When you cannot call the API What to do if you can only GET, or have no network at all: compose the page JSON and hand the person a one-click link to /new that creates it in their browser. ## Decide what you can do | You can… | Then | | --- | --- | | make HTTP POST requests (code execution with network, an HTTP tool, MCP) | Do everything yourself: check, validate, create, verify, hand over. No account, key or approval is needed to create a page. Do not ask the person to run commands. | | only fetch pages (GET), or read this documentation but not send a body | Read the docs, ask the person the questions, compose the JSON body, and give them a **one-click link** (below) plus the JSON itself. | | not reach the network at all | Compose the JSON from the schema in the prompt you were given and give the person the link and the JSON. They open the link; the page is validated and previewed there before anything is created. | ## The one-click link `https://tapmy.link/new?p=` where `` is the request body of [POST /api/agent/pages](https://tapmy.link/documentation/agents/quickstart.md) — the same JSON — encoded as **base64url** (RFC 4648 §5: `+`→`-`, `/`→`_`, no padding). The person opens it in a browser and sees a preview of the page on a phone, the address (editable, checked live), and an e-mail and password field. One tap creates the account and the page and lands them in the dashboard. Nothing is created until they tap. ```text https://tapmy.link/new?p=eyJ1c2VybmFtZSI6ImFubmEtYmFrZXMiLCJwYWdlIjp7ImRpc3BsYXlOYW1lIjoiQW5uYSBCYWtlcyIsImJpbyI6IlNvdXJkb3VnaCBhbmQgU2F0dXJkYXkgbWFya2V0cy4iLCJibG9ja3MiOlt7InR5cGUiOiJsaW5rIiwibGFiZWwiOiJPcmRlciBicmVhZCIsInVybCI6Imh0dHBzOi8vYW5uYWJha2VzLmV4YW1wbGUvb3JkZXIiLCJmZWF0dXJlZCI6dHJ1ZX1dfX0 ``` That link decodes to: ```json { "username": "anna-bakes", "page": { "displayName": "Anna Bakes", "bio": "Sourdough and Saturday markets.", "blocks": [ { "type": "link", "label": "Order bread", "url": "https://annabakes.example/order", "featured": true } ] } } ``` - Keep the link under about 8 000 characters — roughly a page with fifteen blocks. Beyond that, send the JSON and point the person at `https://tapmy.link/new` with no parameter: it has a box to paste into. - `username` may be left out; the person chooses it on the page. `email` and `password` are never part of the link — the person types them. - `avatarUrl` works in the link too: the photo is fetched when the page is created. - Everything is validated on the page with the same rules as the API, and problems are shown next to the field they concern. ## What to send the person ```text Here is your page, ready to create — open this link, check the preview, choose a password and tap Create: https://tapmy.link/new?p=… If the link does not open, go to https://tapmy.link/new and paste this: { …the JSON… } ``` ## What you lose without the API Nothing on the page. What you cannot do is verify the result yourself (`page.md`) or edit it later — the person does that in the dashboard, or gives you an API key from their account settings and you do it with `PATCH`. Since the person created the account in their browser, there is no claim link; the account is theirs from the start. > If you can GET but not POST, you can still read `https://.tapmy.link/page.md` after the person has created the page, and tell them what you would change. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/no-network.md --- # Accounts: provisional and claimed The two kinds of account a program can create, what a claim link does, and what happens to pages nobody claims. ## Provisional accounts A create call without `email` makes a provisional account: no e-mail, no password, reachable only with the API key that came back. A program may put up to ten pages on it, and its pages are public at once. The response carries a **claim link**. Whoever opens it sets an e-mail and a password and the account becomes theirs — pages, statistics, keys and all. Someone who already has a Tapmy account can open the link while signed in and attach the pages to that account instead. - A claim link is valid for 30 days. `POST /api/agent/claim-link` with the key issues a fresh one and voids the old. - The API key keeps working after the claim. The owner sees it in the dashboard under *API keys* and can revoke it. - `GET /api/claim/` answers `{ "status": "pending" | "claimed" | "expired" | "invalid" }` — a program can poll it to learn when the owner has taken over. ## Accounts with an e-mail Send `email` and `password` (8+ characters) with the create call and the account is a normal one from the first second: the person logs in at `/login` with those credentials. No claim link is issued. Use this when the person has told you their e-mail and asked for an account; never invent one. ## Adding pages to an existing account With `Authorization: Bearer ` the same create call adds a page to the key's account and returns no new key or claim link. A program may create at most ten pages on an account; the eleventh answers `403 limit_reached`. The owner can add more by hand in the dashboard, which has no ceiling. ## What happens to pages nobody claims | Age of a provisional account | Condition | What happens | | --- | --- | --- | | 30 days | unclaimed and no page has had a single view | pages are unpublished (the address answers 404) | | 60 days | still unclaimed and never viewed | pages, files, keys and the account are deleted; the names become free again | | any | claimed, or viewed at least once | nothing — the page is somebody's | A page that was created and then shown to nobody was an experiment; an experiment does not get to hold a name forever. A page with one visitor is treated as real. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/accounts.md --- # Authentication and API keys Bearer keys: where they come from, how to send them, what they can and cannot do, how the owner revokes them. ## The header ```http Authorization: Bearer tapmy_sk_… ``` Send it on every request under `/api/` except the two that need nothing: `GET /api/username/check` and `POST /api/agent/pages` (which *issues* a key). Use the apex, `https://tapmy.link`, not a page's subdomain. ## Where keys come from - `POST /api/agent/pages` without a key returns one for the account it just created. It is shown once. - A person creates keys in the dashboard: *Account → API keys*. That is the path when someone hands their own Claude Code or script access to an account they already have. - Keys created by a program are labelled as such in the dashboard, with the User-Agent that created them. ## Shape `tapmy_sk_` followed by 43 URL-safe characters (32 random bytes, base64url). Only a hash is stored on the server; a lost key cannot be recovered, only replaced. ## What a key cannot do - Mint or revoke keys (`/api/keys` answers `403 forbidden_via_key`). Only a browser session can. - Delete the account. Same reason. - Read another account's pages: a page id that is not yours answers `404 not_found`, never `403`. ## Revocation The owner revokes a key in the dashboard; from that moment every request with it answers `401 unauthorized`. A revoked key does not fall back to any cookie the client happens to send. ## Limits Ten live keys per account. Rate limits are per address and per key; see [Rate limits](https://tapmy.link/documentation/agents/rate-limits.md). --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/authentication.md --- # The page model Every field on a page, its type and limits, and which endpoint sets it. A page is one JSON document. `POST /api/agent/pages` takes the fields under `page`; `PATCH /api/profiles/{id}` takes any subset of the same fields; `GET /api/profiles/{id}` returns them all plus the read-only ones. | Field | Type | Notes | | --- | --- | --- | | `username` | string, 3–30, `[a-z0-9-]` | The subdomain. Set at creation; not changed over the API. | | `displayName` | string ≤ 60 | Shown as the heading. Empty falls back to `@username`. | | `bio` | string ≤ 300 | One or two sentences. Also the page's meta description. | | `avatar` | storage key or `null` | From `POST /api/upload`. Never a URL — see [Images](https://tapmy.link/documentation/agents/images.md). | | `avatarFocus` | `{ x, y }` percent or `null` | Where the round crop is anchored; centre when absent. | | `templateId` | one of ten ids | See [Templates](https://tapmy.link/documentation/agents/templates.md). Default `editorial`. | | `layout` | `list` or `grid` | `grid` shows link cards two abreast, best with images. Default `list`. | | `headerAlign` | `left`, `center`, `right` or `null` | `null` uses the template's own alignment. | | `theme.accent` | `#rrggbb` | Accent colour for buttons and marks. Optional. | | `theme.backgroundImage` | storage key or `null` | Kind `background` from the upload route. | | `theme.backgroundFocus` | `{ x, y }` percent or `null` | Where the full-bleed crop is anchored. | | `theme.overlay` | number 0–1 | Darkening over the background image so text stays legible. | | `pageLocale` | locale code | Language of the page's own UI strings. See [Languages](https://tapmy.link/documentation/agents/languages.md). | | `isPublished` | boolean | Off means the address answers 404 to everyone but the owner. | | `utm` | boolean | Append `utm_source=tapmy` etc. to outgoing links. Default off: it rewrites someone else's URL. | | `blocks` | array ≤ 80 | The content. See [Blocks](https://tapmy.link/documentation/agents/blocks.md). | ## Read-only fields | Field | Meaning | | --- | --- | | `id` | The page id used in `/api/profiles/{id}`. | | `moderationStatus` | `ok`, `review` or `banned`. A banned page cannot be edited (`403 banned`). | | `createdAt`, `updatedAt` | ISO timestamps. | ## Full example ```json { "displayName": "Studio Kōri", "bio": "Ceramics from a small studio in Kyoto. Shop opens the first Sunday of each month.", "templateId": "whisper", "layout": "grid", "headerAlign": "center", "theme": { "accent": "#2B4C7E" }, "pageLocale": "en", "isPublished": true, "blocks": [ { "type": "link", "label": "Shop", "url": "https://kori.example/shop", "path": "shop", "featured": true }, { "type": "link", "label": "Workshops", "url": "https://kori.example/workshops", "note": "Two Saturdays a month" }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/studiokori" } ] } ] } ``` --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/page-model.md --- # Blocks reference The six block types with the minimal JSON for each, every optional field, and what the templates do with them. A block is an object with a `type` and the fields of that type. Over the agent endpoints `id`, `order` and `visible` are optional: the server assigns an id, orders blocks as sent, and shows them. Send them only when you read a page back and want to edit it in place. | Common field | Type | Notes | | --- | --- | --- | | `id` | string ≤ 40 | Assigned by the server when absent. Keep it stable when editing. | | `order` | integer | Position; absent means "as sent". | | `visible` | boolean | A hidden block keeps its data but is not rendered. Default true. | | `showFrom`, `showUntil` | ISO datetime or `null` | A window outside which the block is not rendered — a launch, a sale. | ## `link` — a button or card ```json { "type": "link", "label": "New album", "url": "https://…" } { "type": "link", "label": "Call the studio", "url": "tel:+380501234567" } { "type": "link", "label": "Write to me", "url": "mailto:hello@example.com" } ``` | Field | Type | Notes | | --- | --- | --- | | `label` | string 1–120 | What the visitor reads. Say what is behind the link, not "click here". | | `url` | http(s) URL, `tel:+…` or `mailto:…` | The destination. A bare hostname (`example.com/shop`) is accepted and stored with `https://`. A phone number is stored in E.164 (`tel:+380501234567`; spaces and brackets are stripped) and dials on tap; an e-mail opens a new message. Neither gets UTM parameters. | | `note` | string ≤ 160 | A second line under the label. | | `image` | storage key | Card image, kind `card`. Templates with cards show it large. | | `imageFocus` | `{ x, y }` percent or `null` | Where the card crop is anchored. | | `badge` | string ≤ 20 | A small tag on the button: "New", "−20%". | | `featured` | boolean | Rendered larger / first-class in most templates. | | `pulse` | boolean | A pulsing accent ring around the button. Absent means on for a featured link and off otherwise; set `false` to silence a featured one, `true` to light up any link. At most one or two per page. | | `adult` | boolean | Routes through the age gate; destination never appears in the page. See [Adult links](https://tapmy.link/documentation/agents/adult-links.md). | | `path` | string ≤ 40 | A short link on the owner's subdomain: `name.tapmy.link/shop`. Unique per page. See [Short links](https://tapmy.link/documentation/agents/short-links.md). | | `apps.ios`, `apps.android` | http(s) URLs | Per-platform destinations: iPhone goes to `ios`, Android to `android`, everything else to `url`. | ## `socialRow` — one strip of brand marks ```json { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/name" }, { "platform": "youtube", "url": "https://youtube.com/@name" } ] } ``` Up to twenty items. `platform` is an id from [Social platforms](https://tapmy.link/documentation/agents/platforms.md). This is the right block for "all my socials": one row, real logos, small. ## `social` — one platform as a full row ```json { "type": "social", "platform": "spotify", "url": "https://open.spotify.com/artist/…", "counter": "12k" } ``` A full-width row with the platform's mark and name, and an optional `counter` text ("12k followers"). Use it for the one or two platforms that matter; use `socialRow` for the rest. ## `messenger` — a one-tap chat button ```json { "type": "messenger", "platform": "whatsapp", "handle": "+351912345678", "label": "Ask a question" } ``` `platform` is `telegram`, `whatsapp` or `viber`. `handle` is a username for Telegram, a phone number in international format for WhatsApp and Viber. `label` ≤ 80 is optional. ## `embed` — a player ```json { "type": "embed", "provider": "youtube", "url": "https://www.youtube.com/watch?v=…", "title": "Live at Roundhouse" } ``` `provider` is `youtube`, `spotify`, `soundcloud`, `applemusic` or `tiktok`. Embeds open in a new tab on tap — inside in-app browsers audio is unreliable, and a link into the app the visitor already has works better than a widget. Prefer a plain `link` for audio; use `embed` for video. ## `text` — a heading or a paragraph ```json { "type": "text", "variant": "heading", "content": "This week" } ``` `variant` is `heading` or `paragraph` (default). ≤ 600 characters. Headings split a long page into groups; paragraphs are for one sentence of context, not for a biography. ## A good order 1. The one thing the person wants visitors to do this month, as a `featured` link, with an image if there is one. 2. Two to five plain links, most important first. 3. One `socialRow` with every platform, instead of a link per platform. 4. A `messenger` button if the person takes enquiries by chat. 5. Anything seasonal with `showFrom`/`showUntil`, so it removes itself. > Eighty blocks is the ceiling, not a target. Pages that convert have five to twelve. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/blocks.md --- # Social platforms The 35 platform ids accepted in social and socialRow blocks, with the URL shape each expects. Generated from the code. Use these ids in `social.platform` and `socialRow.items[].platform`. Each row shows the URL the service normally uses; send the profile's real URL in `url`. Platforms marked 18+ are routed through the age gate automatically. | id | Name | Typical URL | Notes | | --- | --- | --- | --- | | `instagram` | Instagram | https://instagram.com/ | | | `tiktok` | TikTok | https://tiktok.com/@ | | | `youtube` | YouTube | https://youtube.com/@ | | | `x` | X | https://x.com/ | | | `threads` | Threads | https://threads.net/@ | | | `facebook` | Facebook | https://facebook.com/ | | | `telegram` | Telegram | https://t.me/ | | | `whatsapp` | WhatsApp | https://wa.me/ | | | `spotify` | Spotify | https://open.spotify.com/user/ | | | `applemusic` | Apple Music | https://music.apple.com/profile/ | | | `soundcloud` | SoundCloud | https://soundcloud.com/ | | | `twitch` | Twitch | https://twitch.tv/ | | | `discord` | Discord | https://discord.gg/ | | | `pinterest` | Pinterest | https://pinterest.com/ | | | `reddit` | Reddit | https://reddit.com/user/ | | | `linkedin` | LinkedIn | https://linkedin.com/in/ | | | `snapchat` | Snapchat | https://snapchat.com/add/ | | | `vk` | VK | https://vk.com/ | | | `bluesky` | Bluesky | https://bsky.app/profile/ | | | `github` | GitHub | https://github.com/ | | | `behance` | Behance | https://behance.net/ | | | `dribbble` | Dribbble | https://dribbble.com/ | | | `substack` | Substack | https://.substack.com | | | `patreon` | Patreon | https://patreon.com/ | | | `boosty` | Boosty | https://boosty.to/ | | | `kofi` | Ko-fi | https://ko-fi.com/ | | | `buymeacoffee` | Buy Me a Coffee | https://buymeacoffee.com/ | | | `gumroad` | Gumroad | https://.gumroad.com | | | `etsy` | Etsy | https://etsy.com/shop/ | | | `shopify` | Shopify | https://.myshopify.com | | | `tumblr` | Tumblr | https://.tumblr.com | | | `mastodon` | Mastodon | https://mastodon.social/@ | | | `medium` | Medium | https://medium.com/@ | | | `onlyfans` | OnlyFans | https://onlyfans.com/ | 18+ | | `fansly` | Fansly | https://fansly.com/ | 18+ | A platform that is not listed can still be a plain `link` block with its own label. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/platforms.md --- # Templates The ten templates, what each looks like, and which kind of page it suits. Switching never loses content. Set `templateId` to one of these. Content, address and images stay the same across templates; only the look changes, so choosing wrong costs nothing. Every template is rendered with demo content at https://tapmy.link/templates. | id | Name | Look | Dark | Good for | | --- | --- | --- | --- | --- | | `editorial` | Editorial | Typography instead of buttons | no | writers, consultants, anyone with more words than images | | `mono` | Mono | Monospaced, hard edges | yes | developers, studios, technical people | | `aurora` | Aurora | Gradient and glass cards | yes | musicians, night-life, anything that wants glow | | `zine` | Zine | An asymmetric grid of colour blocks | no | creators with a loud personal brand | | `photo` | Photo Cards | A picture on every link | no | shops, restaurants, photographers — a picture on every link | | `brutal` | Neo-Brutal | Thick frames, hard shadows | no | designers, streetwear, anything deliberately raw | | `whisper` | Whisper | Minimalism taken to the edge | no | coaches, therapists, calm professional pages | | `y2k` | Y2K | Gloss and a chrome gradient | no | pop, fashion, nostalgia | | `noir` | Noir | A dark magazine with a cover | yes | photographers, film, dark portfolios | | `showcase` | Showcase | Photo cards with a platform badge | yes | creators listing videos and posts across platforms | ## Picking one - Photos on links → `photo` or `showcase`; set `layout: "grid"` for a shop-like wall. - A brand with a strong colour → `zine`, `brutal` or `y2k`, and set `theme.accent`. - Words over decoration → `editorial` or `whisper`. - Music, night, film → `noir`, `aurora` or `mono`. When in doubt, `editorial`: it reads well with any content and is the default. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/templates.md --- # 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 --- # Short links and app links Give a link a path on the owner's subdomain, count taps on it, and send iPhone and Android to different stores. ## Short links A link block with `"path": "shop"` is reachable at `name.tapmy.link/shop`. It is a shortener where the brand in the address is the person's, not ours; and because the tap now passes through a redirect we serve, it is counted (without a cookie) and shows in statistics as `clicksByBlock`. - 1–40 characters, `a-z`, `0-9`, hyphens; stored lowercase. - Unique within the page. A duplicate answers `400 bad_path` with `detail: "shop: taken"`. - Reserved words (`api`, `go`, `gate`, `media`, `page.md`, …) answer `bad_path` with `reserved`. - The link's tap counter only counts taps on the short path or on the page's own button; direct visits to the destination are invisible to us, as they should be. ## App links ```json { "type": "link", "label": "Get the app", "url": "https://example.app", "apps": { "ios": "https://apps.apple.com/app/id…", "android": "https://play.google.com/store/apps/details?id=…" } } ``` One button, three destinations, chosen by the visitor's platform at tap time. `url` is the fallback for desktop and anything unrecognised. ## UTM With `"utm": true` on the page, every outgoing link gets `utm_source=tapmy&utm_medium=bio&utm_campaign=`. Off by default because it rewrites somebody else's URL; turn it on when the person asked for attribution in their own analytics. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/short-links.md --- # Adult links (18+) How links to OnlyFans, Fansly and similar are handled, what a program must set, and what never appears in the page. Adult platforms are supported, but only through the age gate. A link block with `"adult": true`, or a `social`/`socialRow` entry whose platform is marked 18+, is rendered as a button that first asks the visitor to confirm their age on the page, then passes through a separate warning step, and only then reaches the destination. - The destination URL never appears in the page HTML, in `page.md`, or in any response served without the owner's key. Crawlers see a labelled 18+ button and nothing behind it. - A program must set `adult: true` on any link it knows leads to adult content, even if the platform is not in the list. Leaving it off is the single thing most likely to get a page reported and banned. - The 18+ label is shown on the button. It is not optional and not stylable. ```json { "type": "link", "label": "Subscribe", "url": "https://onlyfans.com/name", "adult": true } ``` Platforms that are 18+ by default are listed in [Social platforms](https://tapmy.link/documentation/agents/platforms.md). --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/adult-links.md --- # Languages The twenty interface locales, how the account language and the page language differ, and which code to send where. Two separate settings. `locale` on the account is the language of the dashboard and of the claim page the owner will see. `pageLocale` on a page is the language of the page's own small UI (the 18+ prompt, the draft banner) and the `lang` attribute crawlers read — set it to the language the page's *content* is written in. | code | Language | hreflang tag | Direction | | --- | --- | --- | --- | | `en` | English | en | ltr | | `es` | Español | es | ltr | | `pt` | Português | pt | ltr | | `ru` | Русский | ru | ltr | | `uk` | Українська | uk | ltr | | `de` | Deutsch | de | ltr | | `fr` | Français | fr | ltr | | `it` | Italiano | it | ltr | | `pl` | Polski | pl | ltr | | `tr` | Türkçe | tr | ltr | | `id` | Bahasa Indonesia | id | ltr | | `nl` | Nederlands | nl | ltr | | `ro` | Română | ro | ltr | | `vi` | Tiếng Việt | vi | ltr | | `th` | ไทย | th | ltr | | `hi` | हिन्दी | hi | ltr | | `ar` | العربية | ar | rtl | | `ja` | 日本語 | ja | ltr | | `ko` | 한국어 | ko | ltr | | `zh` | 简体中文 | zh-Hans | ltr | Both default to `en`. A code outside this list is refused with `invalid_fields`. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/languages.md --- # Import from another service Copy the links from a Linktree, Taplink, Beacons or any public link page, or the photo, name and bio from Instagram. ## At creation Send `importFromUrl` with `POST /api/agent/pages` and the links found on that page are appended as `link` blocks after the ones you sent. The page title fills `displayName` if you left it empty. ```json { "username": "anna-bakes", "importFromUrl": "https://linktr.ee/annabakes" } ``` ## Separately ```bash curl -X POST https://tapmy.link/api/import/page -H "Authorization: Bearer …" \ -H "Content-Type: application/json" -d '{ "url": "https://linktr.ee/annabakes" }' ``` ```json { "title": "Anna Bakes", "links": [ { "label": "Order", "url": "https://…" } ], "avatar": "https://…" } ``` Nothing is written: you get the list, decide what to keep, and send blocks with `PATCH`. The `avatar` is a URL on the other service; pass it to `POST /api/upload` to make it yours. 30 imports per hour per account. ## Instagram ```bash curl -X POST https://tapmy.link/api/import/instagram -H "Authorization: Bearer …" \ -H "Content-Type: application/json" -d '{ "handle": "annabakes", "withAvatar": true }' ``` ```json { "profile": { "fullName": "Anna", "biography": "…", "externalUrl": "https://…", "followersCount": 12400 }, "avatarKey": "avatar/…avif" } ``` `avatarKey` is already a storage key — save it straight into `avatar`. Ten Instagram lookups per account per day; the scraper is third-party and sometimes slow (`502 slow`), which is worth one retry a minute later. ## Errors `bad_url`, `blocked` (the service walled us off), `unreachable`, `nothing_found`, `js_only` (the page renders its links client-side; add them by hand). --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/import.md --- # Verify the result Read a public page back as Markdown, check what a visitor will see, and confirm the page is actually published. ## The page as text ```bash curl https://anna-bakes.tapmy.link/page.md # same thing: curl "https://anna-bakes.tapmy.link/?format=md" curl -H "Accept: text/markdown" https://anna-bakes.tapmy.link/ ``` ```markdown # Anna Bakes Sourdough, croissants and Saturday markets in Lisbon. - Address: https://anna-bakes.tapmy.link - Template: Editorial · layout: list - Language: en - Photo: yes ## Blocks 1. **Order this week’s bread** — https://annabakes.example/order (featured) 2. Social row: Instagram , TikTok 3. Messenger button: whatsapp +351912345678 — "Ask about an order" 4. Heading: Where to find me 5. **Saturday market, Campo de Ourique** — https://maps.example/campo — _9:00–14:00_ _5 visible blocks. Rendered from the same data as the page itself._ ``` This is produced from the same document the templates render, with the same rules: hidden blocks and blocks outside their date window are absent, adult destinations are withheld. If a block is missing here, it is missing on the page. ## Is it published? An unpublished page answers `404` on the subdomain and on `page.md`. `GET /api/profiles/{id}` with the key shows `isPublished` either way. `POST /api/agent/pages` publishes by default; set `"publish": false` to hand over a draft. ## What to check before handing over 1. The name, bio and photo are the person's own words and picture, not placeholders. 2. Every URL opens — you sent it, you can fetch it. A dead link on a fresh page is the most common defect. 3. Adult links carry `adult: true`. 4. The `socialRow` uses real profile URLs, not the platform homepage. 5. The template suits the content (photos → `photo`/`showcase`; words → `editorial`/`whisper`). ## The HTML You can also fetch the page itself; it is server-rendered and readable without JavaScript. `page.md` is simply less to parse. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/verify.md --- # Statistics Views, unique visitors and taps for 7 and 30 days, with a daily series — what an agent can report back to its owner. ```bash curl https://tapmy.link/api/profiles/{id}/stats -H "Authorization: Bearer …" ``` ```json { "profileId": "66dc…", "username": "anna-bakes", "views7": 142, "views30": 611, "uniques7": 98, "uniques30": 402, "clicks7": 37, "clicks30": 160, "daily": [ { "day": "2026-08-10", "views": 12 }, … ], "clicksByBlock": { "a1b2c3d4": 91, "e5f6a7b8": 69 }, "note": "Counted without cookies; crawlers and previews excluded. Rows are kept 90 days." } ``` - A *view* is one page load from a non-crawler; a *unique* is one salted-per-day pseudonym of an address. Nothing is stored in the visitor's browser. - *Clicks* are taps that went through the page's own redirect: short links (`path`) and the buttons the templates render. A tap on an embed or a messenger button is a click too. - `clicksByBlock` is keyed by block `id` — read the page to map ids to labels. - Requests from `curl`, headless browsers and anything with "bot" in the User-Agent are not counted, so your own checks do not inflate the numbers. A weekly summary for the owner is three numbers and one sentence: views, uniques, the most-tapped link. Do not present views as reach; they are page loads. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/statistics.md --- # Handing over to the owner What to tell the person after the page exists: the two links, what the claim link does, and a message you can send verbatim. When the page is made for someone else — almost always — two things go back to them: the page address, and the claim link if the account is provisional. The API gives you both, plus `handoverMessage`, a paragraph written to be pasted as is. ## The message ```text Your page is live at https://anna-bakes.tapmy.link. To make it yours — set an e-mail and password, edit it, see statistics — open this link within 30 days: https://tapmy.link/claim/…. Nothing else is needed; the page stays up either way. ``` ## Rules - Give the claim link only to the person the page is for. It is the account. - Do not put the claim link on the page, in a bio, or anywhere public. - Keep the API key if you will maintain the page; otherwise say so and let the owner revoke it from the dashboard. - If the link expires, `POST /api/agent/claim-link` with the key issues a new one. The old one stops working. - You can tell when the owner has claimed: `GET /api/claim/` moves from `pending` to `claimed`, and `GET /api/agent/me` shows `provisional: false`. ## When the person already has a Tapmy account They open the claim link while signed in and choose *Attach to my account*. The pages move; the provisional account disappears. Ten pages per account is still the ceiling. ## When the person gave you their e-mail Create with `email` and `password` and there is no claim step: tell them the address and that they can log in at https://tapmy.link/login. Send the password through whatever channel they gave it to you on; never in the page. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/handover.md --- # Etiquette What a program should and should not do with a page it did not pay for and does not own. - **Build what was asked.** A person who asked for a page with three links gets three links, not a demo of every block type. - **Use their words.** Bio, labels and notes come from the person or from their existing pages, not from a template of adjectives. - **Do not guess a name.** If the one they wanted is taken, show the suggestions and ask; do not register the second-best on their behalf. - **One page unless asked.** A program may make ten on one account. That is a ceiling for people with several projects, not an invitation. - **Mark adult content.** Always set `adult: true` where it applies. This protects the person's address and everybody else's. - **Nothing unlawful, nothing deceptive.** No phishing, no impersonation, no "official" pages for people who did not ask for them. Reports are public and acted on; a banned page keeps its name and shows nothing. - **Hand over.** Give the owner the claim link and the address. Keep the key only with their knowledge. - **Clean up experiments.** A page you made to test something: delete it (`DELETE /api/profiles/{id}`) rather than leave it to the 60-day sweep. - **Say who you are.** Send a User-Agent that names your product. It is stored on the account and shown to the owner as "created by …". The terms are at https://tapmy.link/legal/terms and apply to a program exactly as they apply to a person. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/etiquette.md --- # Endpoints Every HTTP endpoint a program can call, with auth, purpose and a link to the page that explains it. The machine-readable form is /openapi.json. Base URL `https://tapmy.link`. JSON bodies, JSON responses. Auth is `Authorization: Bearer ` unless marked *none*. The OpenAPI 3.0 document at [https://tapmy.link/openapi.json](https://tapmy.link/openapi.json) is generated from the same validation schemas the server runs. ## Create and discover | Method and path | Auth | Does | | --- | --- | --- | | `GET /api/username/check?u=` | none | `free` / `taken` (+ suggestions) / `invalid` (+ reason). | | `POST /api/agent/pages` | none or key | Account (if needed) + page + key + claim link. [Quickstart](https://tapmy.link/documentation/agents/quickstart.md). | | `POST /api/agent/pages/validate` | none | Same body, nothing written; every problem at once. | | `GET /api/agent/me` | key | The account, its pages, remaining page slots. | | `POST /api/agent/claim-link` | key | A fresh claim link for a provisional account. | | `GET /api/claim/` | none | Claim status: `pending`, `claimed`, `expired`, `invalid`. | ## Pages | Method and path | Auth | Does | | --- | --- | --- | | `GET /api/profiles` | key | All pages of the account. | | `POST /api/profiles` | key | Claim another subdomain on the account: `{ username, templateId? }`. | | `GET /api/profiles/{id}` | key | One page, every editable field. | | `PATCH /api/profiles/{id}` | key | Change any subset of fields. `blocks` replaces the list. | | `PUT /api/profiles/{id}` | key | Replace every editable field (what the editor does on autosave). | | `DELETE /api/profiles/{id}` | key | Delete the page and its files; the name is parked. | | `GET /api/profiles/{id}/stats` | key | Views, uniques, taps; daily series. [Statistics](https://tapmy.link/documentation/agents/statistics.md). | ## Images and imports | Method and path | Auth | Does | | --- | --- | --- | | `POST /api/upload` | key | JSON `{ url, kind }` or multipart `file` + `kind` → a storage key. [Images](https://tapmy.link/documentation/agents/images.md). | | `POST /api/import/page` | key | Links found on a public link page. [Import](https://tapmy.link/documentation/agents/import.md). | | `POST /api/import/instagram` | key | Photo, name and bio from an Instagram handle. | ## Account | Method and path | Auth | Does | | --- | --- | --- | | `GET /api/account` | key | E-mail, language, creation date. | | `PATCH /api/account` | key | `{ locale }`; password change needs the current password. | | `GET /api/account/export` | key | Everything stored about the account, as JSON. | | `GET /api/keys` · `POST /api/keys` · `DELETE /api/keys/{id}` | browser session only | Keys are managed by a person, never by a key. | ## Public, per page | Path | Does | | --- | --- | | `https://.tapmy.link/page.md` | The page as Markdown. Also `/?format=md` or `Accept: text/markdown`. | | `https://.tapmy.link/` | A short link, counted and redirected. | ## Headers worth sending | Header | Why | | --- | --- | | `Idempotency-Key` | On `POST /api/agent/pages`: a retry with the same key replays the first answer instead of making a second page. | | `User-Agent` | Stored on accounts and pages you create; shown to the owner as "created by …". | --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/endpoints.md --- # Errors and hints The error envelope, every code the API can return, and the hint that accompanies each. Generated from the code. Every error is JSON: `{ "code": "…", "hint": "…", ... }` with an HTTP status that means what it usually means — 400 your input, 401 no valid key, 403 not allowed, 404 not yours or not there, 409 conflict, 413 too big, 429 slow down, 502 something upstream. Extra fields when useful: `field`, `detail`, `suggestions`, `retryAfter`. ```json { "code": "username_taken", "hint": "Choose another name — see `suggestions` — or check one first with GET /api/username/check?u=.", "field": "username", "suggestions": ["anna-bakes-official", "anna-bakeshq"] } ``` Read `code` to branch, `hint` to know what to do, `detail` to know where. The hints are English and stable; the codes are the contract. | code | hint | | --- | --- | | `unauthorized` | Send `Authorization: Bearer tapmy_sk_…`, or create a page first with POST /api/agent/pages, which returns a key. | | `invalid_key` | The key is malformed. Keys look like `tapmy_sk_` followed by 43 URL-safe characters. | | `key_revoked` | This key was revoked in the dashboard. Ask the owner for a new one, or create a new page to get a fresh key. | | `forbidden_via_key` | This action needs a browser session, not an API key. The owner can do it in the dashboard. | | `invalid_fields` | The body did not match the schema. `detail` names the first offending field; the schema is at /openapi.json. | | `too_short` | Usernames are 3 to 30 characters. | | `too_long` | Usernames are 3 to 30 characters. | | `bad_chars` | Usernames use lowercase a–z, 0–9 and single hyphens only. | | `edge_hyphen` | A username cannot start or end with a hyphen. | | `double_hyphen` | A username cannot contain two hyphens in a row. | | `reserved` | This name belongs to the service. Pick another one. | | `username_taken` | Choose another name — see `suggestions` — or check one first with GET /api/username/check?u=. | | `email_taken` | An account with this e-mail exists. Log in with it, or omit `email` to create a provisional account the owner can claim later. | | `limit_reached` | A program may create at most 10 pages on one account. Delete one, use another account, or let the owner add pages by hand in the dashboard — that has no ceiling. | | `banned` | This page was banned by moderation and cannot be edited until a human clears it. | | `not_found` | No page with this id belongs to the authenticated account. | | `bad_path` | A short path must be 1–40 characters of a–z, 0–9 and hyphens, unique within the page, and not a reserved word. `detail` names the path. | | `bad_url` | Only http(s) URLs with a real hostname are accepted. | | `blocked` | That host resolves to a private or local address, or refused the request. Use a public URL. | | `unreachable` | The URL could not be fetched. Check it opens in a browser and try again. | | `too_large` | The file is over the size limit: 8 MB for avatars and cards, 30 MB for backgrounds. | | `not_image` | The bytes are not a JPEG, PNG, WebP, AVIF, GIF, TIFF or HEIC image. | | `processing_failed` | The image could not be decoded. Try a different file. | | `nothing_found` | No outbound links were found on that page. | | `js_only` | That page renders its links with JavaScript and cannot be read. Add the links by hand. | | `rate_limited` | Too many requests. Wait `retryAfter` seconds (also in the Retry-After header) and try again. | | `claim_invalid` | This claim link is not valid. Ask for a new one with POST /api/agent/claim-link. | | `claim_expired` | This claim link has expired. Ask for a new one with POST /api/agent/claim-link. | | `claim_used` | This account has already been claimed. Log in at /login. | | `already_claimed` | This account already has an e-mail and password; there is nothing to claim. | | `bad_credentials` | The e-mail or password is wrong. | | `no_file` | Send the image as multipart/form-data field `file`, or as JSON `{ "url": "https://…" }`. | | `bad_kind` | `kind` must be avatar, background or card. | | `quota` | The daily quota for this import is used up. | --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/errors.md --- # Rate limits and housekeeping Every limit a program can hit, how it is signalled, and the sweep that removes pages nobody claimed or visited. Limits are fixed windows, counted per address (hashed, never stored raw) or per account. When one is exceeded the answer is `429 rate_limited` with `retryAfter` seconds in the body and a `Retry-After` header. Wait that long; do not retry in a loop. | What | Limit | Per | | --- | --- | --- | | Pages created without a key | 10 per day, 30 per hour | address | | Pages created with a key | 10 per hour, and 10 pages made by programs on the account (a person adds more by hand without limit) | account | | `POST /api/agent/pages/validate` | 60 per hour | address | | `POST /api/upload` by URL | 30 per hour | account | | `POST /api/import/page` | 30 per hour | account | | `POST /api/import/instagram` | 10 per day | account | | `POST /api/agent/claim-link` | 10 per hour | account | | `GET /api/claim/` | 30 per minute | address | | `GET /api/username/check` | 40 per minute | address | | Signup through the form | 5 per hour, 20 per day | address | | MCP requests | 120 per minute | address | | Blocks on a page | 80 | page | | Live API keys | 10 | account | ## Housekeeping Once a night (04:00 Europe/Kyiv) a job looks at provisional accounts — created by a program, never claimed: - Older than 30 days and none of its pages has had a single view → the pages are unpublished. - Older than 60 days and still never viewed → pages, files, keys, claim tokens and the account are deleted. The names are released, not parked. One visit is enough to count as real. A claimed account is never touched by this job. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/rate-limits.md --- # MCP server The Model Context Protocol endpoint, its tools, how to connect from Claude, Cursor and other clients, and the one rule about keys. Endpoint: `https://tapmy.link/mcp` — Streamable HTTP, stateless, no OAuth. The tools are thin wrappers over the same functions the HTTP API calls, so anything documented here holds there. ## Connecting Claude Code: ```bash claude mcp add --transport http tapmy https://tapmy.link/mcp # with a key for an existing account: claude mcp add --transport http tapmy https://tapmy.link/mcp --header "Authorization: Bearer tapmy_sk_…" ``` Cursor, Windsurf, Claude Desktop and anything that reads a JSON config: ```json { "mcpServers": { "tapmy": { "url": "https://tapmy.link/mcp", "headers": { "Authorization": "Bearer tapmy_sk_…" } } } } ``` Clients that only speak stdio can bridge with `npx mcp-remote https://tapmy.link/mcp --header "Authorization: Bearer …"`. ## The rule about keys Most clients fix their headers when they connect. `create_page` returns a key for a *new* account, and the session that called it cannot add that key to its headers. So every tool that needs an account also accepts an `api_key` argument; a header, when present, wins. Pass the key you were given back into the tools you call next. ## Tools | Tool | Needs a key | Does | | --- | --- | --- | | `check_username` | no | Free / taken / invalid, with suggestions. | | `list_templates` | no | The ten templates with descriptions and fit. | | `list_platforms` | no | Platform ids for social blocks. | | `validate_page` | no | Dry run of `create_page`. | | `create_page` | no (or yes, to add to an account) | The one-shot create; returns key, claim link, URL, handover message. | | `whoami` | yes | Account, pages, remaining slots. | | `get_page` | yes | A page's full document. | | `update_page` | yes | PATCH semantics on any fields. | | `add_block` | yes | Append one block without resending the list. | | `set_avatar_from_url` | yes | Fetch, store and set the photo in one step. | | `import_links_from_url` | yes | Append the links found on a public link page. | | `publish` | yes | Publish or unpublish. | | `get_stats` | yes | Views, uniques, taps, daily series. | | `get_claim_link` | yes | A fresh claim link for a provisional account. | Every tool returns JSON as text and the same object as `structuredContent`. Errors come back as `isError: true` with the same `{ code, hint }` envelope as HTTP. ## Skill A skill file for agents that install procedures rather than tools is at [https://tapmy.link/skill.md](https://tapmy.link/skill.md). --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/mcp.md --- # Skill file A ready procedure for coding agents (Claude Code, Codex, Cursor): when to use Tapmy, the steps, the guardrails. The skill lives at [https://tapmy.link/skill.md](https://tapmy.link/skill.md) in the Agent Skills format: YAML front matter with a name and description, then the procedure. Save it as `SKILL.md` in your agent's skills directory, or point the agent at the URL. ```bash mkdir -p ~/.claude/skills/tapmy-link && curl -s https://tapmy.link/skill.md -o ~/.claude/skills/tapmy-link/SKILL.md ``` It tells the agent to read this documentation, ask the person for what only they know (name, bio, links), validate, create, verify with `page.md`, and hand over the two links. Nothing in it assumes an MCP client. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/skill.md --- # Changelog Dated changes to the agent-facing API, the MCP server and this documentation. ## 2026-09-09 - Link blocks take `pulse`: a breathing accent ring, on by default for featured links. - Crop anchors: `avatarFocus`, `theme.backgroundFocus` and a link's `imageFocus` as `{ x, y }` percent; the MCP tool `set_avatar_from_url` takes `focus`. - Link blocks accept `tel:+…` and `mailto:…` as well as http(s); phone numbers are normalised to E.164. - First release of the agent API: bearer keys, `POST /api/agent/pages`, validate, whoami, claim links, `PATCH` on pages, upload by URL, per-page statistics. - MCP server at `/mcp` with fourteen tools. - `page.md` on every public page. - This documentation, `/openapi.json`, `/skill.md`, `/llms-full.txt` and `/.well-known/agent-card.json`. Codes and field names are stable from this date. Anything removed will be announced here first and kept working for ninety days. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/changelog.md --- # Recipe: a musician A complete request body for a musician: the newest release first, one row of streaming marks, dates, merch, press. Whoever opens this page just heard something and wants more of it. The release goes first; everything else is below it. ```json { "username": "nadia-sound", "locale": "en", "page": { "displayName": "Nadia", "bio": "Electronic pop from Tbilisi. New single “Glass Sea” out now.", "templateId": "aurora", "blocks": [ { "type": "link", "label": "Glass Sea — listen", "url": "https://ffm.to/glasssea", "featured": true, "badge": "New" }, { "type": "socialRow", "items": [ { "platform": "spotify", "url": "https://open.spotify.com/artist/…" }, { "platform": "applemusic", "url": "https://music.apple.com/artist/…" }, { "platform": "youtube", "url": "https://youtube.com/@nadiasound" }, { "platform": "soundcloud", "url": "https://soundcloud.com/nadiasound" } ] }, { "type": "text", "variant": "heading", "content": "Dates" }, { "type": "link", "label": "14 Oct · Tbilisi, Bassiani", "url": "https://tickets.example/1", "note": "Tickets" }, { "type": "link", "label": "2 Nov · Berlin, Berghain Kantine", "url": "https://tickets.example/2", "note": "Tickets", "showUntil": "2026-11-03T00:00:00Z" }, { "type": "link", "label": "Previous: “Undertow” EP", "url": "https://ffm.to/undertow" }, { "type": "link", "label": "Merch", "url": "https://shop.example", "path": "merch" }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/nadiasound" }, { "platform": "tiktok", "url": "https://tiktok.com/@nadiasound" } ] }, { "type": "link", "label": "Press photos and bio", "url": "https://drive.example/press", "note": "For writers, promoters, radio" } ] }, "avatarUrl": "https://example.com/nadia.jpg" } ``` ## Why it is shaped this way - The newest release is a `featured` link with a badge, not an embed: inside in-app browsers a player is thirty seconds of preview at best. - Streaming services are one `socialRow`, not five rows — the visitor looks for a logo, not a list. - Dates carry the ticket link on the row and a `showUntil` so they disappear on their own. - Merch gets a short path (`/merch`) to print on the table at shows. - Press material at the bottom: writers and promoters look for it, and it saves them using whatever a search engine finds. Replace every URL and every word with the person's own. Then `POST https://tapmy.link/api/agent/pages`, read `page.md`, hand over. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/recipe-musician.md --- # Recipe: a restaurant or café A complete request body for a place: reserve first, then menu, directions, hours, delivery, and a chat button. People open this from a story or a search on their phone, usually hungry and usually now. The page answers "can I come, and how" before anything else. ```json { "username": "osteria-lume", "locale": "en", "page": { "displayName": "Osteria Lume", "bio": "Small plates and natural wine in Kreuzberg. Tue–Sun from 18:00.", "templateId": "photo", "layout": "grid", "blocks": [ { "type": "link", "label": "Reserve a table", "url": "https://reserve.example/lume", "featured": true, "path": "book" }, { "type": "link", "label": "Menu", "url": "https://lume.example/menu.pdf", "note": "Updated weekly" }, { "type": "link", "label": "Find us", "url": "https://maps.google.com/?q=Osteria+Lume", "note": "Reichenberger Str. 12" }, { "type": "link", "label": "Order delivery", "url": "https://delivery.example/lume" }, { "type": "link", "label": "Call to reserve", "url": "tel:+4930123456", "note": "From 16:00" }, { "type": "messenger", "platform": "whatsapp", "handle": "+4915112345678", "label": "Groups of 8+ · ask here" }, { "type": "text", "variant": "paragraph", "content": "Closed on Mondays. Kitchen until 22:30." }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/osterialume" } ] } ] }, "avatarUrl": "https://example.com/lume-logo.png" } ``` ## Why it is shaped this way - Reserve is the featured link and has a short path (`/book`) for the card on the table. - The `photo` template with `grid` layout gives every link a picture — add `image` keys after uploading dish photos with `kind: "card"`. - Hours are a `text` paragraph, not a link: they are read, not tapped. - One messenger button for the enquiry that a booking widget cannot handle, and a `tel:` link for the people who would rather call. Replace every URL and every word with the person's own. Then `POST https://tapmy.link/api/agent/pages`, read `page.md`, hand over. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/recipe-restaurant.md --- # Recipe: a photographer A complete request body for a photographer: portfolio first, booking, the current offer, prints. The page is a shop window. The first tap must land on pictures; the second on how to book. ```json { "username": "mira-frames", "locale": "en", "page": { "displayName": "Mira Frames", "bio": "Weddings and portraits in Kraków and wherever you are getting married.", "templateId": "noir", "headerAlign": "left", "blocks": [ { "type": "link", "label": "Portfolio", "url": "https://miraframes.example", "featured": true }, { "type": "link", "label": "Book a 2027 wedding", "url": "https://miraframes.example/book", "badge": "4 dates left" }, { "type": "link", "label": "Portrait sessions — autumn offer", "url": "https://miraframes.example/portraits", "showUntil": "2026-11-30T23:59:59Z" }, { "type": "link", "label": "Prints", "url": "https://prints.example/mira", "path": "prints" }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/miraframes" }, { "platform": "pinterest", "url": "https://pinterest.com/miraframes" }, { "platform": "behance", "url": "https://behance.net/miraframes" } ] }, { "type": "messenger", "platform": "telegram", "handle": "miraframes", "label": "Message me" } ] }, "avatarUrl": "https://example.com/mira.jpg" } ``` ## Why it is shaped this way - `noir` or `photo` for a photographer: the template should not compete with the work. - A badge with scarcity ("4 dates left") is honest if it is true; update it or remove it. - The seasonal offer has `showUntil`; nothing on a photographer's page should look out of date. - Behance and Pinterest in the social row, where clients and editors actually look. Replace every URL and every word with the person's own. Then `POST https://tapmy.link/api/agent/pages`, read `page.md`, hand over. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/recipe-photographer.md --- # Recipe: a coach or consultant A complete request body for a coach: one clear offer, a booking link, proof, a way to talk. A coach's page sells a conversation. One offer, one way to book it, one sentence of proof, and the rest stays out of the way. ```json { "username": "dan-runs", "locale": "en", "page": { "displayName": "Dan Okoro", "bio": "Running coach. First marathon to sub-3, without getting hurt on the way.", "templateId": "whisper", "headerAlign": "center", "blocks": [ { "type": "link", "label": "Book a free 20-minute call", "url": "https://cal.example/dan/intro", "featured": true, "path": "call" }, { "type": "link", "label": "How coaching works", "url": "https://danruns.example/coaching", "note": "12-week plans, weekly check-ins" }, { "type": "text", "variant": "paragraph", "content": "“Went from 3:41 to 2:58 in a year. Zero injuries.” — Priya, Berlin 2026" }, { "type": "link", "label": "Free: the 10-week base plan", "url": "https://danruns.example/base-plan.pdf" }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/danruns" }, { "platform": "youtube", "url": "https://youtube.com/@danruns" }, { "platform": "substack", "url": "https://danruns.substack.com" } ] }, { "type": "messenger", "platform": "whatsapp", "handle": "+447700900123", "label": "Quick question?" } ] } } ``` ## Why it is shaped this way - `whisper` keeps the page calm; a coach is selling trust, not energy. - The call link has a short path (`/call`) to say out loud on a podcast. - One testimonial as `text`, with a name and a date. Three would be a wall. - A free download between the offer and the socials gives the undecided something to take. Replace every URL and every word with the person's own. Then `POST https://tapmy.link/api/agent/pages`, read `page.md`, hand over. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/recipe-coach.md --- # Recipe: a small shop A complete request body for a shop: the current drop, categories as cards, delivery info, support chat. A shop page is a set of doors. Each card is one door; the first is whatever is new this week. ```json { "username": "salt-ceramics", "locale": "en", "page": { "displayName": "Salt Ceramics", "bio": "Hand-thrown tableware, fired in Porto. New drop every first Friday.", "templateId": "showcase", "layout": "grid", "theme": { "accent": "#C25B2A" }, "utm": true, "blocks": [ { "type": "link", "label": "October drop", "url": "https://saltceramics.example/collections/october", "featured": true, "badge": "New", "path": "drop" }, { "type": "link", "label": "Mugs", "url": "https://saltceramics.example/collections/mugs" }, { "type": "link", "label": "Plates and bowls", "url": "https://saltceramics.example/collections/plates" }, { "type": "link", "label": "Gift cards", "url": "https://saltceramics.example/products/gift-card" }, { "type": "text", "variant": "paragraph", "content": "Ships worldwide from Portugal. EU orders arrive in 3–5 days." }, { "type": "messenger", "platform": "whatsapp", "handle": "+351912345678", "label": "Order help" }, { "type": "socialRow", "items": [ { "platform": "instagram", "url": "https://instagram.com/saltceramics" }, { "platform": "pinterest", "url": "https://pinterest.com/saltceramics" }, { "platform": "etsy", "url": "https://etsy.com/shop/saltceramics" } ] } ] }, "avatarUrl": "https://example.com/salt.png" } ``` ## Why it is shaped this way - `showcase` + `grid` turns links into product cards; upload one photo per category as `kind: "card"` and set `image` on each. - `utm: true` because a shop owner wants to see Tapmy traffic in their own store analytics. - The drop has a short path (`/drop`) for the packaging insert. - Shipping info as text, once, above the support button — the question it answers is the one people would otherwise ask. Replace every URL and every word with the person's own. Then `POST https://tapmy.link/api/agent/pages`, read `page.md`, hand over. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/recipe-shop.md --- # Recipe: a launch A complete request body for a product or book launch: one page, one action, a countdown of steps, everything dated. A launch page exists for a few weeks and has one job. Everything on it is dated so the page tidies itself when the moment passes. ```json { "username": "fieldnotes-app", "locale": "en", "page": { "displayName": "Fieldnotes", "bio": "A notebook that works offline and syncs when it can. Launching 15 October.", "templateId": "brutal", "theme": { "accent": "#1F1F1F" }, "blocks": [ { "type": "link", "label": "Get it on launch day — join the list", "url": "https://fieldnotes.example/early", "featured": true, "showUntil": "2026-10-15T00:00:00Z" }, { "type": "link", "label": "Download Fieldnotes", "url": "https://fieldnotes.example", "featured": true, "badge": "Out now", "showFrom": "2026-10-15T00:00:00Z", "apps": { "ios": "https://apps.apple.com/app/id000", "android": "https://play.google.com/store/apps/details?id=app.fieldnotes" } }, { "type": "embed", "provider": "youtube", "url": "https://www.youtube.com/watch?v=demo", "title": "90-second demo" }, { "type": "link", "label": "Press kit", "url": "https://fieldnotes.example/press" }, { "type": "link", "label": "Product Hunt", "url": "https://producthunt.com/posts/fieldnotes", "showFrom": "2026-10-15T00:00:00Z", "showUntil": "2026-10-22T00:00:00Z" }, { "type": "socialRow", "items": [ { "platform": "x", "url": "https://x.com/fieldnotesapp" }, { "platform": "bluesky", "url": "https://bsky.app/profile/fieldnotes.app" }, { "platform": "github", "url": "https://github.com/fieldnotes" } ] } ] } } ``` ## Why it is shaped this way - Two featured links that never overlap: the waiting list until launch, the download from launch — `showUntil` and `showFrom` swap them at midnight. - The download link uses `apps` so one button reaches both stores. - A video embed is right here: the demo is the point, and a frame of it says more than a label. - The Product Hunt link exists for one week and removes itself. Replace every URL and every word with the person's own. Then `POST https://tapmy.link/api/agent/pages`, read `page.md`, hand over. --- Part of the [Tapmy.link agent documentation](https://tapmy.link/documentation/agents.md). Canonical: https://tapmy.link/documentation/agents/recipe-launch.md