Keepp Agent API

Build and run your Keepp page from an AI app, or from your own code. Connect an MCP app like Claude or ChatGPT, or call the HTTP API with a key.

Connect Keepp to Claude, ChatGPT or any MCP app

Keepp runs a remote MCP server. Add it to an app that supports MCP connectors, sign in with your Keepp account, and ask for what you want in the chat.

https://api.keepp.link/mcp

Connecting

  1. In your app’s connector settings, add a custom connector with the URL above.
  2. Sign in to Keepp in the window that opens, with an email code or Google. If you’re new, your account is created as you sign in.
  3. Review what the app is asking to do and approve it.

Things you can ask for

  • “Make me a page for my pottery studio”
  • “Add my online course for 49 CAD and put it near the top”
  • “Swap the photo on my consultation booking for this one”
  • “Connect Stripe so I can take payments”
  • “Make the page darker and use a serif font for headings”

Good to know

  • Building or changing your page from an app needs Pro. If you’re on Free, the app can hand you a link to upgrade.
  • Keepp asks the app to check with you before it creates a page or changes a product, price or booking. Your handle can’t be changed once it’s created.
  • Connecting Stripe, Google Calendar or Zoom opens their own sign-in page. Those passwords never pass through the chat.
  • Images you ask for are copied onto Keepp, so your page never depends on another site.
  • To disconnect, remove the connector in your app.

HTTP API

For your own code. 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.

GET /api/v1/capabilities

Every block type, its fields and what each needs to publish — plus guidance on composing a page. Public: no key required.

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": "#C2185B" },
  "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, so 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, so 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, placeUrl
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[]
chatfulltitle*, greeting, icon message-circle|mail|headphones|sparkles|heart|users: singleton, Influencer. A round chat button on the page; who answers is set in Chat settings

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

showcase—Shows 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, so 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, so 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, because 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.