# Keepp Page

> Use when a user wants to build or manage their Keepp page — adding, arranging, or removing blocks (links, cards, products, bookings, forms, maps, YouTube videos, menus, tickers, profile, socials, headers, text), scheduling a block to appear later, changing the layout, or restyling the theme — through the Keepp Agent API. Requires a Keepp Pro API key (keepp_live_…).

- Skill: `thinslatelabs/keepp-page` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thinslatelabs/keepp-page`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thinslatelabs/keepp-page/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: thinslatelabs (https://skillmd.com/u/thinslatelabs)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/thinslatelabs/keepp-page

---


# 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:

1. `GET /api/v1/page`
2. Modify the `blocks` array you received — change one, add one, reorder, remove one
3. `PUT` the **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:

```json
{ "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:

1. **Ask them for a URL.** Anything already public works — their own site, a
   social post, a CDN, a Drive/Dropbox link set to public.
2. **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, at `PUT` time, 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:

```json
{ "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

