Building a Keepp Page
What Keepp is
Keepp gives a creator or small business one page at keepp.link/theirhandle that replaces a whole toolkit: link-in-bio, storefront, booking page, and forms on a single URL they own.
A page is an ordered list of blocks on a two-column grid. Each block is full width (a row to itself) or half (pairs side-by-side with the next half block). List order is visual order — the array you send is the page, top to bottom.
Authentication
Every request needs the owner's API key as a bearer token:
Authorization: Bearer keepp_live_…
Keys are generated by a Pro account in the Keepp dashboard under "AI Agent". If you don't have one, ask the user. Base URL: https://api.keepp.link. Rate limit: 60 requests/minute.
The three endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/page |
Read the whole page: version, url, publishedAt, blocks, theme. |
| PUT | /api/v1/page |
Replace the whole page. Send blocks, optional theme, optional version. |
| GET | /api/v1/catalog |
List the products and bookingUnits this page can reference. |
PUT replaces everything — read first
PUT is not a patch. Any block you leave out is deleted. There is no undo.
So the loop is always:
GET /api/v1/page- Modify the
blocksarray you received — change one, add one, reorder, remove one PUTthe entire array back
Never build a blocks array from scratch for a page that already exists, unless the user explicitly asked to replace the whole thing. If you received 25 blocks and you're adding one, send 26.
The response tells you what happened, so you can check yourself:
{ "ok": true, "data": {
"version": 13,
"publishedAt": "2026-07-21T10:00:00.000Z",
"blockCount": { "before": 25, "after": 26 } } }
If after is lower than you intended, you dropped blocks — say so and restore them from your earlier GET.
A full page is roughly 6,000 output tokens, which exceeds the default max_tokens on many API clients (often 4096). If your response is cut off mid-array the request fails as malformed JSON with a 400. Raise your output limit rather than sending a shortened page.
Ids
Every block has an id (a UUID). Keep the ids of blocks you received — they're what the page's per-block share links point at. Omit id on a genuinely new block and the server mints one.
Concurrency
Pass the version from your GET to make the write fail rather than overwrite someone editing at the same moment — you'll get 409 VERSION_CONFLICT, then re-GET and reapply. Omit version and your write always wins.
What a page can contain, and how to build one well
Do not guess at block shapes, and do not work from a list written here. Both the block vocabulary and the guidance on composing a page are published, and both are derived from the product itself rather than maintained by hand:
GET https://keepp.link/api/v1/capabilities (public, no key needed)
It returns two things:
blocks— every block type, the fields each one takes, which are required to publish, and the sizes, alignments and shapes it accepts. Generated from the same definitions the editor and the publisher use, so it cannot fall behind the product.guidance— what to get right (where money is taken, what a schedule needs, what deleting costs) and how to build a page somebody would be pleased to send to a stranger.
Read it before building a page. An earlier version of this file carried its own copy of all of that, and the copy drifted from the product — which is why it now lives in one place.
Images
Send a public https:// image URL in mediaUrls, imageUrl, or iconUrl and the server fetches and stores it. Paths already starting with /uploads/ are stored — send those back unchanged.
A URL that can't be fetched, isn't an image, is too large, or resolves to a private address returns 422 INVALID_IMAGE.
There is no upload endpoint — a public URL is the only way in. A local file, a path on the user's machine, or an image you generated in this session cannot be sent directly. When the user has no URL for the image they want, say so and offer the two ways forward rather than stalling:
- Ask them for a URL. Anything already public works — their own site, a social post, a CDN, a Drive/Dropbox link set to public.
- Put it somewhere public first. Any image or file host that hands back a
direct
https://link to the image itself will do; the server only needs to fetch it once, atPUTtime, after which the image lives in Keepp and the temporary copy can go.
The link must resolve to the image, not to a viewer page wrapped around it — a
share page returns HTML and fails as 422 INVALID_IMAGE.
Errors
Success: { "ok": true, "data": { … } }. Error: { "ok": false, "error": { "code": "…", "message": "…" } }.
| Code | HTTP | Meaning |
|---|---|---|
NO_API_TOKEN / INVALID_API_TOKEN |
401 | Missing, revoked, or wrong key. Ask the user for a valid one. |
PLAN_REQUIRED |
403 | The account isn't on Pro. |
RATE_LIMITED |
429 | Over 60 requests/minute. Slow down and retry. |
INVALID_INPUT |
400 | A block is malformed, or the page broke a rule (block cap, two singletons). |
INVALID_IMAGE |
422 | An image URL couldn't be fetched or processed. |
NOT_FOUND |
404 | A productId or bookingUnitId doesn't belong to this account. |
VERSION_CONFLICT |
409 | You sent a version and the page moved. Re-GET and reapply. |
Errors caused by one block carry blockIndex — its position in the array you sent:
{ "ok": false, "error": {
"code": "INVALID_INPUT", "message": "Card price is required.", "blockIndex": 3 } }
Fix that block and resend the whole array. A rejected PUT changes nothing, so the page is exactly as it was.
More context
- Developer docs: https://keepp.link/developers
- Machine index: https://keepp.link/llms.txt
- Managing your page with an AI agent: https://keepp.link/blog/how-to/14-managing-your-page-with-an-ai-agent