Keepp Agent API

Build and manage your Keepp page programmatically — every block, the layout, and the theme. Available on Pro. Generate a key from your dashboard under AI Agent.

What this API manages

Your whole page: links, cards, profile, socials, headers, text, forms, maps, YouTube videos, menus, tickers, scheduling, and the theme. Products and bookable units are read-only — the API can place them on your page and arrange them, but it can’t create one or change a price. Those stay in your dashboard.

Authentication

Send your key as a bearer token. Base URL https://api.keepp.link.

Authorization: Bearer keepp_live_…

Endpoints

GET /api/v1/page

Read your whole page — version, url, publishedAt, blocks, theme.

PUT /api/v1/page

Replace your whole page. Any block you leave out is removed.

GET /api/v1/catalog

List the products and bookable units your blocks can reference.

PUT replaces — read first

PUT is not a patch. Always GET the page, modify the array you received, and send all of it back. If you get 25 blocks and add one, send 26. The response returns blockCount before and after so you can confirm nothing was dropped.

A page is a list of blocks

Blocks sit on a two-column grid. A full block takes a row; two half blocks pair side-by-side. Array order is the order on the page.

PUT https://api.keepp.link/api/v1/page
{
  "version": 12,
  "theme": { "roundedness": "soft", "brandColor": "#d7494c" },
  "blocks": [
    { "id": "2f9c…", "type": "profile-circle", "size": "full",
      "name": "Studio Marlow", "about": "Hand-thrown ceramics." },
    { "id": "7a41…", "type": "header", "size": "full", "text": "Shop", "showInNav": true },
    { "type": "product", "size": "half", "productId": "8f2c…" },
    { "type": "card", "size": "half", "kind": "promo-code",
      "title": "20% off your first order", "code": "FIRST20" }
  ]
}

Block envelope

typeenumThe block type — see the table below.
idstring?UUID. Keep the ids you received; omit on a new block and one is minted. Ids back your per-block share links.
sizeenum?"full" or "half". Most types only allow "full"; defaults per type.
alignenum?"left" | "center" | "right", on the types that support it. Useful for a lone half block.
styleobject?{ bg, text } hex overrides. Dropped on types that can't render them.
shapeenum?"pill" | "card", on the types that support it. Unlike style, a shape a type can't render is rejected, not dropped — leave it out entirely on the others.
scheduleobject?{ from, until, tz } — show the block only inside a window. Accepted on every type.

Scheduling

Any block can be given a window, and it only appears inside it:

"schedule": { "from": "2026-08-01T09:00", "until": "2026-08-14T23:59", "tz": "Asia/Kolkata" }
tzstring*An IANA zone — “Europe/London”, not “GMT+1”.
fromstring?Local wall-clock YYYY-MM-DDTHH:mm — no seconds, no offset, no Z.
untilstring?Same format, and must be after from.

At least one of from / until is required. Times are deliberately local rather than UTC, so 9am Friday stays 9am across a daylight-saving change. The window is evaluated server-side when the page is read, so a block that hasn’t started isn’t in the HTML at all — it can’t be found in view-source. Scheduled blocks still count toward the 100-block limit.

Block types

linkhalf, fullshape pill|card (default pill) — title*, url*, imageUrl* (card only), icon / iconUrl (pill only)
cardhalf, fullshape card|pill (default card) — kind*, title* (optional for showcase only), mediaUrls, description, additionalInfo, ctaLabel, aspect wide|square|tall, icon — plus a per-kind field (below)
headerfulltext*, showInNav — set showInNav to build nav tabs. variant: "plain" | "eyebrow" | "divider"
subheaderfulltext*, variant: "plain" | "eyebrow" | "divider"
textfulltext*
profile-circlefullname, about, imageUrl
profile-squarefullname, about, imageUrl
social-iconsfullicons* — [{ id, platform, url }]
profile-ctafulllabel*, url — singleton
navfullNo fields — singleton. Tabs come from headers with showInNav.
spacerfullNo fields.
formfullshape pill|card (default pill) — preset* ("lead" | "feedback" | "form"), title*, fields*, description (shown on the card), triggerLabel, submitLabel, successMessage, verification
bookinghalf, fullshape card|pill (default card) — bookingUnitId*, aspect wide|square|tall — all other display fields are server-filled
producthalf, fullshape card|pill (default card) — productId*, aspect wide|square|tall — all other display fields are server-filled
maphalf, fullshape card only — embedSrc*, title, address, hours
youtubehalf, fullshape card only — videoId*, title, description
tickerfulltext* (max 300), direction rtl|ltr, speed slow|normal|fast, pauseOnHover
menuhalf, fullshape pill|card (default pill) — title*, subtitle, imageUrl, triggerLabel, items[]

A menu item is { name, description, price, tags[] }. Keep descriptions to 50 words, up to 100 items, and no more than 10 distinct tags per menu.

* required. profile-cta and nav are singletons — one each per page.

Form fields

A form takes a fields array of { id, label, type, required }, where id is a UUID you generate. Submissions land in your dashboard.

typeenum*short_text, long_text, email, phone, single_select, multi_select, product_interest, social_handles, rating
optionsarray?Required by the select types.
isIdentifierboolExactly one field must set this, and only an email or phone field may — it's how a submission is attributed to a person.
verificationobject?On the block: { email: true } makes the submitter confirm their address before the submission counts.

Sending no identifier, or two, rejects the whole PUT. It’s the most common reason a hand-built form block fails.

Card kinds

showcaseShows something off. The only kind where title isn't required. Opens the detail dialog on tap; optional ctaUrl adds a CTA button on the card face.
for-saleprice*Free text price — "₹2,400", "From $40". Opens the detail dialog on tap.
affiliate-linkctaUrl*The affiliate destination, used for the card's CTA button. Opens the detail dialog on tap.
promo-codecode*The code's chip copies it directly when tapped; tapping the rest of the card opens the detail dialog, where the code is also available.

Use a card for something sold elsewhere. For something bought on your page through Stripe, use a product block.

Products & bookings

Call GET /api/v1/catalog and send only the reference — { "type": "product", "productId": "…" }. The server fills title, price, images and buyability from the record, so any values you send for those are overwritten. This is deliberate: a key can never publish a price the seller didn’t set.

Images

Put a public https image URL in mediaUrls, imageUrl, or iconUrl and it’s fetched and stored for you. Paths already starting with /uploads/ are yours already — send them back unchanged.

Theme

roundednessenum?"flat" | "soft" | "round".
brandColorstring?3- or 6-digit hex.
textColorstring?3- or 6-digit hex.
fontPairingenum?"editorial" | "bold" | "classic" | "modern" | "soft" | "handwritten" | "minimal" — sets the page's display/body font pair.
backgroundobject?{ type: "color" | "gradient" | "image" } with color, or from/to/angle, or imageUrl + optional veil/position.
background.veilenum?"none" | "light" | "strong" — scrims an image background so text stays readable.
background.positionenum?"left top" … "right bottom" — the nine CSS keyword positions.

Unrecognised theme keys and invalid values are silently dropped, not rejected — a typo in fontPairing leaves your fonts unchanged with no error. Re-read the page if you need to confirm a theme change landed.

Limits

  • Up to 100 blocks per page.
  • Images up to 10 MB each (JPEG/PNG/WebP/GIF), from a direct public https URL.
  • 60 requests per minute per key.
  • Writes publish to your live page immediately — there is no draft state.
  • A full page is roughly 6k tokens. If you’re generating one with an LLM, raise its output limit — a truncated body fails as malformed JSON.

Errors

  • 401 NO_API_TOKEN / INVALID_API_TOKEN — missing or revoked key.
  • 403 PLAN_REQUIRED — the key’s business is not on Pro.
  • 400 INVALID_INPUT — a block is malformed, or the page broke a rule.
  • 422 INVALID_IMAGE — an image URL couldn’t be fetched, was the wrong type/size, redirected, or resolved to a non-public address.
  • 404 NOT_FOUND — a productId or bookingUnitId isn’t yours.
  • 409 VERSION_CONFLICT — you sent a version and the page moved. Re-read and reapply.
  • 429 RATE_LIMITED — more than 60 requests in a minute.

Errors caused by one block include blockIndex, its position in the array you sent. A rejected write changes nothing, so your page is exactly as it was.

Using an LLM? Hand it the Keepp skill or the llms.txt index — both teach it how to structure a good page, not just call the API.