Carvr Public API Reference (Beta)

Bearer-token HTTP API reference for the Carvr public API — authentication, endpoints, and code examples for automating Figma-to-Klaviyo template creation.

Beta. This API is available today for anyone with a Carvr account, but endpoints and response shapes may still change before a stable release is announced.

What This API Does

The Carvr public API runs the same pipeline as the web app and the Figma plugin from your own code or automation. Connect brands, check your credits, and send an email design to be sliced, compressed and saved as a template in the brand’s Klaviyo, Omnisend or Mailchimp account. Call it from any HTTP client.

Getting an API Token

  1. Go to your Profile and find API Tokens for AI Tools.
  2. Give it a name and, optionally, scope it to one or more brands.
  3. Copy the token immediately — it's shown once and can't be retrieved again.

Only account owners and admins can create or revoke tokens. Anyone on the account can see that tokens exist, but not their raw values.

Authentication

Send the token as a bearer token on every request:

Authorization: Bearer carvr_live_...

Requests without a valid token return 401. A request for a brand the token isn't scoped to returns 403. These are real HTTP status codes — unlike the rest of the Carvr backend (which always returns 200 with a success flag in the body), this API follows normal REST conventions since it's meant for external tools.

Endpoints

GET /api-list-brands

Lists the brands your token can access. Each brand's espProvider (klaviyo, omnisend or mailchimp, or null for a brand with no connection) is the ESP it sends to, and what slice-and-upload's espProvider has to match.

curl "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/api-list-brands" \
  -H "Authorization: Bearer carvr_live_..."

POST /api-create-brand

Creates a brand from the ESP API key your system already stores for a client, so brands can be added programmatically instead of on the Brands page. Klaviyo, Omnisend and Mailchimp are all supported, and the key is checked with the ESP before anything is created.

Klaviyo (the default): send a Klaviyo private API key (pk_...), the API-only alternative to the interactive OAuth connect flow. It needs the accounts:read scope (full-access keys have it), and the brand is named and URL'd from the Klaviyo account automatically. Pass name / websiteUrl only to override that.

curl -X POST "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/api-create-brand" \
  -H "Authorization: Bearer carvr_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "apiKey": "pk_..." }'

Omnisend or Mailchimp: send that ESP's API key with espProvider set to omnisend or mailchimp. The brand is named from that account: Omnisend's brand name and website, or Mailchimp's account name and the domain of its connected store or site when it has one. Pass name / websiteUrl only to override that. A website sent without https:// gets it added. Send a Mailchimp key whole, including the server suffix at the end (-us21 or similar). The brand works in the web app and the Figma plugin straight away, the same as one added on the Brands page.

curl -X POST "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/api-create-brand" \
  -H "Authorization: Bearer carvr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "apiKey": "...-us21",
    "espProvider": "mailchimp"
  }'

Returns 201 with the new brand: use its id as brandId in template calls, and its espProvider says which ESP it sends to. Safe to retry: POSTing a key that's already on one of your account's brands returns that existing brand with 200 and created: false instead of a duplicate. A name another of your brands already uses gets " (2)" added. A missing field or a key the ESP rejects is 400, the ESP throttling the check is 429, and the ESP being unreachable is 502; nothing is created in any of these, so fix the input or retry. Brand-scoped tokens can't call this endpoint (403): creating brands takes an unscoped token, since it grows the very brand list a scoped token is limited to.

GET /api-get-account-status

Returns your plan tier and remaining upload credits for the current billing period.

curl "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/api-get-account-status" \
  -H "Authorization: Bearer carvr_live_..."

POST /api-create-klaviyo-template

For scripts that slice designs themselves. Uploads sections you’ve already cropped to Klaviyo and creates a template, for one credit. It needs a brand connected to Klaviyo, either through the one-click connection or a legacy private API key. Omnisend and Mailchimp brands get a 400; send their designs through slice-and-upload instead, which is also the way to use Carvr’s own slicing and compression.

curl -X POST "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/api-create-klaviyo-template" \
  -H "Authorization: Bearer carvr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "brandId": "your-brand-id",
    "templateName": "My template",
    "width": 600,
    "height": 1200,
    "positions": [33.3],
    "verticalLines": [
      { "position": 50, "sectionTopPercent": 33.3, "sectionBottomPercent": 100 }
    ],
    "sections": [
      { "imageBase64": "...", "contentType": "image/png", "altText": "Hero" },
      { "imageBase64": "...", "contentType": "image/png", "altText": "Footer" }
    ]
  }'

sections.length must equal positions.length + 1, and each section's imageBase64 must decode to PNG or JPEG bytes (checked before a credit is spent). The JSON body is capped at 20MB (413); a body that isn't valid JSON is 400 validation_error. On success you get back a templateId you can open directly in Klaviyo. The first section stands in as the email’s picture in your upload history.

verticalLines is optional. Describe your column layout with it and the saved editor state keeps the columns, so opening the template from your upload history restores them instead of full-width rows. It only affects the editor state: sections are still uploaded to Klaviyo exactly as the full-width rows you send. Each line's sectionTopPercent/sectionBottomPercent must be the bounds of a row cut by positions, and position must be strictly between 0 and 100; anything else is a 400 before a credit is spent.

Each section's altText is optional — leave it out (or send an empty string) and Carvr generates it for you with the same AI-vision pipeline the Figma plugin uses, in parallel with that section's upload so it costs no extra time. Supply your own if you want exact control instead.

Each section can also carry a linkAddress; a section without one is left unlinked. Pass an idempotencyKey to make retries safe: a retry of a request that already finished returns the same templateId with deduped: true instead of creating a second template. If the account has Auto-add HTML footer switched on for the brand's ESP and the brand has footer HTML saved, the footer is added as the last block of the template.

POST /api-slice-and-upload

The whole pipeline in one call: send a full, uncropped design image plus a brandId, and Carvr slices it, generates alt text, and uploads it to the brand's connected ESP, with Carvr’s slicing and image compression. Bills one credit.

curl -X POST "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/api-slice-and-upload" \
  -H "Authorization: Bearer carvr_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "brandId": "your-brand-id",
    "imageBase64": "...",
    "contentType": "image/png",
    "templateName": "My template",
    "espProvider": "klaviyo"
  }'

espProvider defaults to klaviyo and must match a connection the brand actually has (klaviyo, omnisend, or mailchimp).templateName, idempotencyKey (same retry-safety as create-template), and ctaLink (applied to every section's image block) are all optional. contentType must be image/png or image/jpeg (image/jpg is accepted as an alias) and must match what the bytes actually are. The image can be up to 25,000 px tall and 30 megapixels, and the JSON body is capped at 20MB. Send it at the width you want in the ESP (1200 px for a 600 px email): every section keeps it.

On success you get back templateId, sectionCount, imageId (the row in your upload history, null if saving it failed), algorithm (ai or baseline: which slicer produced the cuts) and credits (your credit state after the charge). A warning field is present only when the template was created but saving it to your history failed. A retry with the same idempotencyKey replays the original result with deduped: true (and no algorithm); if the original is still running you get a 409 with a Retry-After header instead.

To change where the cuts went, open the email from your upload history in Carvr (imageId), adjust the cuts and update the template there.

Connecting Claude (MCP)

Carvr runs an MCP (Model Context Protocol) server, so Claude can list your brands and upload email designs to their ESP for you. Its tools call the endpoints above, so billing and rate limits work exactly as they do over HTTP. create-brand stays HTTP only, so an ESP key never passes through a chat.

Claude on the web, desktop, mobile or Cowork

  1. In Claude, open Customize, then Connectors, and click Add custom connector.
  2. Name it Carvr and paste the server URL. Leave the OAuth fields empty.
    NameCarvr
    URLhttps://mcp.carvr.ai
  3. Click Connect, log in to Carvr if asked, pick the account, and click Allow.

On a Team or Enterprise plan, an Owner adds the connector once in Organization settings, then each person clicks Connect. For Figma designs, turn on Claude's own Figma connector too. Claude then hands Carvr an export link of the frame, so the image never has to pass through the chat.

Claude Code

claude mcp add --transport http carvr https://mcp.carvr.ai

Then run /mcp in Claude Code, pick carvr, and log in the same way.

Scripts and other MCP clients

A client that can send a header can skip the login and use a token from your Profile against the server directly.

{
  "mcpServers": {
    "carvr": {
      "url": "https://nvowpqdlcusmkbyrtyhr.supabase.co/functions/v1/mcp-server",
      "headers": {
        "Authorization": "Bearer carvr_live_..."
      }
    }
  }
}

Tools

  • list_brands and get_account_status, the brands this connection can upload to and the credits left.
  • slice_and_upload, the whole pipeline in one call. One credit, and a repeat of the same image, brand and template name never charges twice.
  • get_upload_link, a one-time Carvr page where the person drops an image Claude can't link to, such as one attached to the chat. It lasts 2 hours.

slice_and_upload takes one of imageUrl (an export link from Claude’s Figma connector), uploadId (from get_upload_link) or imageBase64. Base64 is only for small images from a script, because a model can't reliably retype a large one. Designs can be up to 15 MB, 25,000 px tall and 30 megapixels, and Claude sends them 1200 px wide, as the Figma plugin does.

Every Claude connection shows in Profile under API Tokens, named after the person who connected it. Revoking it there disconnects Claude, and Claude asks to connect again. Errors from an endpoint come back as tool results with isError: true and the same JSON body the HTTP API returns.

Error Responses

Every error follows the same shape:

{ "success": false, "error": { "type": "validation_error", "message": "..." } }
  • 401 auth_error: missing, invalid, or revoked token.
  • 403 auth_error: token isn't scoped to the requested brand.
  • 400 validation_error: missing/malformed fields, a body that isn't valid JSON, or an image that isn't PNG/JPEG, is over 25,000 px tall or is over 30 megapixels.
  • 410 gone: /api-analyze-email-design was retired on 9 October 2026. Use slice-and-upload.
  • 402 limit_reached: plan's upload credits are exhausted. The body also carries error.details.credits (the same credit state as account-status) and error.status: 402.
  • 409 conflict (slice-and-upload): the idempotencyKey is still being processed (retry after Retry-After seconds) or was first used by another account.
  • 413 validation_error: the request body is over the 20MB cap, or (slice-and-upload) the cropped sections are too large to return in one call; send a smaller image. Also returned when a single section is over Klaviyo's 5MB per-image limit; slice it smaller.
  • 429: rate limit exceeded for this token.
  • 502 server_error: the ESP or the crop service failed on Carvr's side (not your input). Any credit charged for the request was refunded; retry, with the same idempotencyKey if you sent one. ESP auth and throttling problems keep their own status: 401/403 auth_error for a rejected brand key, 429 rate_limit_error when the ESP is throttling uploads, and 429 daily_limit_error when Klaviyo's 100 images/day limit is reached on that account (the message says when it resets).
  • 504 timeout: slice-and-upload ran out of time; nothing was charged, or the charge was refunded. Retry with the same idempotencyKey.

Rate Limits

Each token is limited to 60 requests per minute. Over that you get a 429 rate_limit_error: back off and retry after a short delay.

Try It

Frequently Asked Questions

Is the Carvr API different from the Carvr MCP server?

They're the same backend, two ways to call it. This page documents a plain bearer-token HTTP API you can call from any language or tool. The Carvr MCP server (see Connecting Claude below, or the Upload to Klaviyo With Claude page) sits over these same endpoints for AI assistants like Claude, which sign in with your Carvr login instead of a token. Billing and rate limits are the same either way.

Does calling the API use my normal upload credits?

Yes. An API-created template counts the same as a plugin or web app upload against your plan's monthly credit allowance — there is no separate quota.

Can one token access every brand on my account?

By default, yes — a token created with no brands selected can act on any brand your account owns. You can scope a token to specific brands when you create it in Profile → API Tokens, which is recommended for tokens handed to a third-party tool.

What happens if I lose my token?

Tokens are shown once, at creation, and are stored hashed — Carvr cannot show it to you again. Revoke the lost token from Profile → API Tokens and create a new one.

Is this API stable enough to build on?

It is early — endpoints, response shapes, and rate limits may still change before a stable v1 is announced. Treat it as a preview: useful for internal automations and experimentation today, not yet guaranteed to be backward compatible.