# API reference Source: https://docs.kettio.com/api-reference/introduction Generated from the Kettio OpenAPI 3.1 contract. Every page in this section is generated directly from [`rank-api.json`](https://kettio.com/docs/api/rank-api.openapi.json), the OpenAPI 3.1 contract the API is built against. Request fields, response schemas, and status codes here cannot drift from the implementation. **Base URL** — `https://kettio.com` **Authentication** — `Authorization: Bearer agk_live_YOUR_KEY` on every request. See [Authentication](/quickstart/authentication). ## Endpoints `POST /api/v1/rank` — score and rank up to 20 assets against one audience. `POST /api/v1/pairwise` — five-voter blinded panel on explicit matchups. Code samples here are copyable but not executable in the browser. Kettio's `/api/v1` routes do not expose CORS, and requests are not proxied — so run them from your own server or terminal with a real key rather than expecting a "send" button. ## Reading the reference The reference is the contract. It will tell you what a field *is*, but not when to use it. For that, start with the guides: Goals, asset types, copy context, and refinement behavior. What a score is, and the four things it is not. # Compare pairs Source: https://docs.kettio.com/api-reference/pairwise openapi/rank-api.json POST /api/v1/pairwise Run five-voter Haiku pairwise comparisons on explicit matchups. # Rank assets Source: https://docs.kettio.com/api-reference/rank openapi/rank-api.json POST /api/v1/rank Score and rank a batch of ad creative assets against one audience. # Changelog Source: https://docs.kettio.com/changelog/overview Changes to the Kettio API and documentation. **Documentation moved to docs.kettio.com.** The developer reference now lives here, generated from the same OpenAPI 3.1 contract the API is built against. Request and response schemas come straight from the spec, so the reference cannot drift from the implementation. Current surface: * `POST /api/v1/rank` — batch scoring and ranking, up to 20 assets * `POST /api/v1/pairwise` — five-voter blinded panel on explicit matchups * MCP server — asset workflow tools for Claude Desktop, Codex, and stdio clients ## Versioning The Rank API is versioned in the URL path (`/api/v1/`). Additive changes — new optional request fields, new response fields, new enum values for `goal` or `asset_type` — ship within `v1` without a version bump. Parse responses tolerantly. New fields can appear on `ranked[]` and `summary` objects within the current version, so a strict schema check that rejects unknown keys will break on an additive release. Breaking changes would ship under a new path prefix. ## Staying current The machine-readable spec is the source of truth for the contract: * [Rank API reference](/api-reference/rank) — generated from OpenAPI 3.1 * [Pairwise API reference](/api-reference/pairwise) — generated from OpenAPI 3.1 Questions or integration issues: [spencer@kettio.com](mailto:spencer@kettio.com). # Audiences Source: https://docs.kettio.com/concepts/audiences Define who the creative is being scored for, inline or from a saved audience. Every scoring request needs an audience. It is what the persona is built from, and it conditions every score in the response. Send exactly one of `audience` or `audience_id` — not both. ## Inline audiences Define the audience in the request. Only `name` is required. ```json theme={null} { "audience": { "name": "Millennial pet owners", "description": "Dog owners aged 25-34, mid-income, active on Instagram", "demographics": { "ageRange": "25-34", "incomeLevel": "50k-75k", "priceSensitivity": "moderate", "shoppingIntent": "researching", "platformFatigue": "high" } } } ``` Inline is the right choice for one-off runs and for agents generating audiences on the fly. ## Saved audiences Reference an audience already created in your Kettio account: ```json theme={null} { "audience_id": "aud_7c2b0d7e" } ``` A `404` means the ID does not exist or is not owned by the authenticated account. Saved audiences are the better choice when you are comparing results over time. An inline audience that drifts between runs quietly invalidates the comparison, because scores are conditioned on the audience. ## Demographics All demographics fields are optional, and sensible defaults are applied. They shape the persona's attitude and behavior rather than filtering anything. | Field | Accepted values | | --------------------- | ------------------------------------------------------------------- | | `ageRange` | `18-24`, `25-34`, `35-44`, `45-54`, `55-64`, `65+` | | `incomeLevel` | `under-25k`, `25k-50k`, `50k-75k`, `75k-100k`, `100k-150k`, `150k+` | | `adSkepticism` | `very-low`, `low`, `moderate`, `high`, `very-high` | | `trustBaseline` | `very-low`, `low`, `moderate`, `high`, `very-high` | | `priceSensitivity` | `very-low`, `low`, `moderate`, `high`, `very-high` | | `categoryFamiliarity` | `none`, `casual`, `knowledgeable`, `expert` | | `shoppingIntent` | `browsing`, `researching`, `comparing`, `ready-to-buy` | | `urgency` | `none`, `low`, `moderate`, `high`, `urgent` | | `brandFamiliarity` | `unaware`, `heard-of`, `familiar`, `loyal` | | `platformFatigue` | `low`, `medium`, `high`, `very-high` | ## Writing a description that does work `description` is free text and it carries real weight — it is the part of the audience the persona reads directly. "Women considering GLP-1 support who need credible purchase proof" Names the buying situation and the objection to overcome. "Our target customers" Carries no information the persona can act on. Describe the purchase situation, not the demographic profile — the structured `demographics` fields already cover the profile. Changing the audience between runs changes what is being measured. If you are comparing two sets of creative, hold the audience fixed across both. # Reading confidence Source: https://docs.kettio.com/concepts/confidence Deciding when a gap between two scores is a real difference. Every ranked asset carries a `confidence` label and a `confidence_details` object. The label is a summary; the details are what you should actually build logic against. ## The fields | Field | What it measures | | ------------------------ | ------------------------------------------------------------------------------------------ | | `entropy` | Spread of the underlying response distribution. Higher means the persona was less decided. | | `top_margin` | Distance between the leading response bucket and the next one. | | `sample_std_dev` | Standard deviation across the three repetitions. | | `repetition_count` | Repetitions behind the score (3). | | `generator_sample_count` | Generator samples behind the score (6). | `sample_std_dev` is the most directly useful of these. It tells you how much the pipeline disagreed with *itself* on the same asset. A score whose repetitions ranged widely is a soft score regardless of where it landed in the ordering. ## The 0.30 rule Kettio treats any gap smaller than **0.30** as a close pair. That is the threshold at which close-pair refinement is willing to run a pairwise panel, and it is a reasonable line for your own logic too. ```javascript theme={null} const CLOSE_PAIR_GAP = 0.30; const [first, second] = ranked; const gap = first.score - second.score; if (gap < CLOSE_PAIR_GAP) { // Not a winner. Escalate or report both. } ``` A gap under 0.30 is **not a ranking**. Presenting rank 1 as the winner when it beat rank 2 by 0.08 is presenting noise as a decision. Either escalate the pair to [`/api/v1/pairwise`](/guides/pairwise-api) or surface both as jointly leading. ## When refinement already ran If you left `refine_close_pairs` at its default, Kettio has already done some of this for you. It finds close pairs, sorts them by how close they are, and refines at most **6 pairs** per request. Check these fields before deciding whether you still need to escalate: | Field | Tells you | | ----------------------------------------------- | ---------------------------------------------------------------------------- | | `score_before_refine` | The raw SSR score, prior to adjustment | | `panel_outcome` | Non-null when a panel ran on this asset — includes whether the order flipped | | `summary.close_pair_refinement.pairs_evaluated` | How many pairs were refined | A `panel_outcome` showing a flip is informative: the SSR ordering and the pairwise panel disagreed, and the panel won. A pair that was close but sat outside the top 6 was never refined at all — the gap is still soft even though nothing in the response flags it. ## A usable decision rule `summary.assets_failed > 0` means the ordering is incomplete. Above it, treat rank 1 as leading. Below it, treat the top assets as tied. If `sample_std_dev` on either asset is large relative to the gap, the gap is inside the noise even if it clears 0.30. Send the pair to the Pairwise API, or report both and let a human choose. The failure mode to design against is false precision. A ranking API will always return an order — including when the assets are indistinguishable. The confidence fields exist so your interface can say "these two are tied" instead of inventing a winner. # Limits and billing Source: https://docs.kettio.com/concepts/limits-and-billing Rate limits and credits are both counted in scoring evaluations, not assets. The single most important thing to internalize: **Kettio bills and rate-limits by scoring evaluation, not by asset.** An agent that budgets by asset count will hit `429` earlier than it expects. ## Rank API limits | Limit | Value | | --------------------- | ----------------------------------------------------- | | Maximum assets | 20 per request | | Concurrency | Up to 4 assets evaluated at a time inside the request | | Rate limit | 60 scoring evaluations per minute per API key | | Close-pair refinement | At most 6 pairs per request | ## What an evaluation costs | Asset shape | Evaluations | | ---------------------------------------- | -------------------------------------------- | | Image only | 1 | | With `copy_context` (ablation succeeded) | 2 — full ad package plus image-only ablation | | With `copy_context` (ablation failed) | 1 | | Failed asset | 0 — failures are not billed | Each evaluation contains three paired-model repetitions (six generator samples), but credits are deducted per **successful scoring evaluation** — not per repetition and not per model call. A 20-asset batch where every asset carries copy context can cost up to 40 evaluations. That is two thirds of your per-minute allowance in a single request. ## Reading usage from the response ```json theme={null} { "summary": { "assets_ranked": 1, "assets_failed": 0, "scoring_evaluations": 2, "repetitions_per_evaluation": 3, "generator_samples_per_evaluation": 6, "credits_used": 2, "credits_remaining": 48 } } ``` Track `scoring_evaluations` and `credits_remaining` rather than counting requests. ## Pairwise billing The Pairwise API is accounted separately and reports `pairwise_evaluations` on its own. | Result | Billed | | -------------------------- | ---------- | | Completed five-voter panel | 1 credit | | `partial` | Not billed | | `error` | Not billed | Close-pair refinement inside the Rank API is reported under `summary.close_pair_refinement`; Rank API credit and rate accounting is based on scoring evaluations. ## Handling 429 `429` responses carry a `Retry-After` header. Honour it instead of retrying on a fixed interval — there is a worked backoff implementation in the [agent loop guide](/guides/agent-loop#rate-limit-backoff). ## Estimating before you send ```javascript theme={null} function estimateEvaluations(assets) { return assets.reduce( (total, asset) => total + (asset.copy_context ? 2 : 1), 0, ); } ``` This is an upper bound — failed assets and failed ablations both come in under it. # How scoring works Source: https://docs.kettio.com/concepts/scoring Repetitions, evaluations, score layers, and the image-only ablation. Kettio scores creative with an SSR (Semantic Similarity Rating) pipeline. An asset is presented to a persona built from the audience you supplied, and the resulting response distribution is converted into a score. ## Repetitions and evaluations The unit that matters for both accuracy and billing is the **scoring evaluation**. One scoring evaluation runs **three independent repetitions**. Each repetition scores a two-model pair, and the three pair scores are averaged. That means a single evaluation makes six generator samples while consuming one ranking credit. ``` 1 scoring evaluation ├── repetition 1 ──┐ ├── repetition 2 ──┼── averaged → score └── repetition 3 ──┘ (2 generator samples each → 6 total) ``` You can see all of this in the response: | Field | Meaning | | ------------------------------------------- | ------------------------------------------------ | | `confidence_details.repetition_count` | Repetitions behind this score (3) | | `confidence_details.generator_sample_count` | Generator samples behind this score (6) | | `details.repetitionScores` | The individual repetition scores | | `details.repetitionScoreRange` | Spread between the highest and lowest repetition | Credits are deducted per **successful scoring evaluation** — not per repetition, not per model call. Three repetitions cost the same as one. ## Score layers An asset is scored at one of two layers, reported as `score_layer`: The default. The persona sees the creative with no ad copy attached. Used when you send `copy_context`. The persona sees the creative *and* the words. ## The image-only ablation and copy lift When you attach `copy_context`, Kettio does not just score the full package. It also re-scores the same creative with the copy stripped out, then reports the difference. | Field | Meaning | | ------------------ | ------------------------------------------------------- | | `score` | Score at the layer named in `score_layer` | | `image_only_score` | Score for the same creative with the copy removed | | `copy_lift` | `score` minus `image_only_score` — what the words added | This is why a copy-context asset can cost two evaluations instead of one: the ablation is a second evaluation. If the ablation fails, `image_only_score` and `copy_lift` are absent and the asset costs one. `copy_lift` is a within-batch, within-audience quantity. A positive lift means the copy helped *this* creative for *this* persona in *this* run. It is not a general claim that the copy is good, and it is not a CTR estimate. ## What a score is not This is the part worth being precise about, because it determines whether you build something useful or something misleading. A score of 4.24 does not mean 4.24% of anything, and does not map to a purchase rate. Scores are calibrated relative to the assets in the submitted batch. Two assets that scored 4.1 in different batches did not necessarily perform equally. Changing `goal` or `audience` changes what is being measured. Hold both fixed for any comparison you intend to act on. Kettio does not predict CPA, ROAS, or causal lift. It is a prioritization signal for deciding what to test, not a substitute for testing. ## Related Deciding when a gap between two scores is real. How evaluations translate into credits and rate limits. # Agent loop Source: https://docs.kettio.com/guides/agent-loop Combine the MCP server and the REST API into a generate → score → decide cycle. There are two ways to reach the same scoring pipeline, and the right one depends on where your agent runs. Takes local file paths and uploads them for you. Best for repo-local and interactive work. Takes fetchable URLs. Best for volume, scheduled jobs, and services with no client attached. Both paths hit the same model. The difference is transport and where the files live — MCP handles the upload step, REST expects you to have already published the assets. ## The cycle ```mermaid theme={null} flowchart TD A[Collect or edit creatives] --> B{Where does the agent run?} B -->|In a repo, via MCP| C[rank_assets
local paths] B -->|As a service| D[Publish to https:// URLs] D --> E[POST /api/v1/rank] C --> F{Read the result} E --> F F -->|copy lift low| G[Edit copy, re-rank] F -->|gap under 0.30| H[POST /api/v1/pairwise] F -->|clear winner| I[Launch] G --> A H --> F ``` Gather the files you want scored. The agent works from its own filesystem — Kettio's tools take local paths directly. See the [tool reference](/mcp/tools). Via MCP, call `rank_assets` with local paths. Via REST, publish to durable `https://` URLs — or inline a `data:image/` payload for small images — then call `POST /api/v1/rank`. Up to 20 assets either way. Use the ordering, `copy_lift` to see whether the words are earning their place, `panel_outcome` to see where refinement changed the order, and `summary.scoring_evaluations` to track spend. ## Decision rules worth encoding These are the checks worth writing into the agent rather than leaving to a model's judgment: `summary.assets_failed > 0` means the ordering you are reading is incomplete. Either re-run the failed assets or mark the result provisional. ```javascript theme={null} if (summary.assets_failed > 0) throw new Error("incomplete batch"); ``` A small gap between rank 1 and rank 2 is not a decision. Either escalate to [`/api/v1/pairwise`](/guides/pairwise-api) or report both. See [Reading confidence](/concepts/confidence). An asset with `copy_context` can cost twice what a bare image costs. An agent that budgets by asset count will hit `429` earlier than it expects. See [Limits and billing](/concepts/limits-and-billing). Scores are relative within a batch and conditioned on the audience. Changing the audience between runs and comparing the numbers is not a valid comparison. ## Rate limit backoff The Rank API allows 60 scoring evaluations per minute per key and returns `Retry-After` on `429`. Honour it rather than retrying on a fixed interval: ```javascript theme={null} async function rankWithBackoff(body, attempt = 0) { const response = await fetch("https://kettio.com/api/v1/rank", { method: "POST", headers: { Authorization: `Bearer ${process.env.KETTIO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (response.status === 429 && attempt < 4) { const retryAfterSeconds = Number(response.headers.get("Retry-After") ?? 5); await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000)); return rankWithBackoff(body, attempt + 1); } return response.json(); } ``` ## Related Connect Claude Desktop, Codex, or any stdio MCP client. Goals, asset types, copy context, and refinement behavior. # Pairwise API Source: https://docs.kettio.com/guides/pairwise-api Run a five-voter blinded panel on explicit head-to-head matchups. `POST /api/v1/pairwise` compares creatives directly instead of scoring them independently. Use it when you already know which matchups need a decision. Every pair gets exactly five blinded Claude Haiku votes, with presentation order balanced 3/2 to control for position bias. The response returns every vote and every raw response, so the result is auditable rather than a black-box verdict. This is **not** the same mechanism as `refine_close_pairs` on the Rank API. Refinement is automatic and fires only on near-ties that Kettio detects. The Pairwise API runs a full panel on exactly the pairs you name, whatever their scores. ## Request ```bash theme={null} curl -X POST https://kettio.com/api/v1/pairwise \ -H "Authorization: Bearer agk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "pairs": [ { "pair_id": "creative-a-vs-b", "a": { "id": "creative-a", "image_url": "https://cdn.example.com/ad-a.png" }, "b": { "id": "creative-b", "image_url": "https://cdn.example.com/ad-b.png" } } ], "audience": { "name": "GLP-1 shoppers", "description": "Women considering GLP-1 support who need credible purchase proof" }, "goal": "purchase-intent", "platform": "facebook" }' ``` Send 1 to 10 pairs per request. The endpoint uses the same inline-or-saved audience contract as `/api/v1/rank`, so an `audience_id` works here too. Full schemas are in the [Pairwise API reference](/api-reference/pairwise). ## Outcomes and billing | Result | Billed | | -------------------------- | --------------------------------- | | Completed five-voter panel | 1 `pairwise_evaluation`, 1 credit | | `partial` | Not billed | | `error` | Not billed | Pairwise usage is reported separately from Rank API usage. The Rank API's rate limit and credit accounting are based on scoring evaluations; `pairwise_evaluations` are counted on their own. ## Use it as a controlled cascade The reason to keep the two endpoints separate is that it lets you run a clean experiment. The pattern: Call `/api/v1/rank` with `refine_close_pairs: false` so the SSR scores stay frozen. Choose which pairs to send to `/api/v1/pairwise` — and choose your consensus threshold — up front, not after seeing results. Replace the SSR winner only when the panel clears the threshold you preregistered. Picking the pairs or the threshold *after* reading outcomes turns this into a post-hoc fit. If you intend to report the result as evidence, fix both before the first outcome lands. ## Choosing between the two You have a batch and need an ordering. You do not know in advance which comparisons matter. You have two specific creatives and need to know which one wins, with the votes on record. # Rank API Source: https://docs.kettio.com/guides/rank-api Score and rank batches of ad creative before spending media budget. `POST /api/v1/rank` scores and ranks a batch of creatives against one audience. It handles image-only ranking, full ad packages with copy context, platform framing, saved or inline audiences, and pairwise refinement for near ties. This page covers behavior and interpretation. Field types, schemas, and response shapes live in the [Rank API reference](/api-reference/rank), generated from the OpenAPI 3.1 contract. ## Choosing a goal `goal` determines what dimension the assets are scored on. It defaults to `purchase-intent`, which is the most developed head. | Goal | Best for | | -------------------------------------- | ------------------------- | | `purchase-intent` | Product ads, DTC creative | | `click-through-rate` | Ads optimized for clicks | | `conversion-potential` | Landing pages, lead gen | | `trustworthiness` | Any ad — perceived trust | | `scroll-stopping` | Feed-based social ads | | `engagement` | Social content, community | | `brand-recognition` | Brand awareness | | `emotional-resonance` | Storytelling creative | | `open-rate` | Email headers | | `logo-professionalism` | Logo evaluation | | `logo-versatility` | Logo evaluation | | `product-page-conversion` | Product detail pages | | `landing-page-conversion` | Landing pages | Scores from different goals are not comparable to each other, and scores are only comparable **within one batch**. Do not build a cross-batch leaderboard out of raw scores. ## Framing the asset `asset_type` changes the system prompt so the simulated persona evaluates the asset in the right mental context. It defaults to `Social Media Post`. | Asset type | Context | | ---------------------------------------- | ------------------ | | `Social Media Post` | Feed-based content | | `Advertisement` | Paid ads | | `Product Photo` | Product imagery | | `Email Header` | Email marketing | | `Landing Page Hero` | Hero sections | | `Logo` | Brand marks | | `Website Banner` | Display ads | | `Illustration` | Custom art | You can set `asset_type` at the top level and override it per asset. Optionally add delivery context with `platform` (`meta`, `instagram`, `facebook`, `tiktok`, `linkedin`, `google`), or `campaign_type` when `platform` is omitted. ## Copy context Attaching `copy_context` to an asset switches it from image-only scoring to full ad package scoring, and triggers an image-only ablation alongside it. The response gains `image_only_score` and `copy_lift`. Preferred keys are `pageName`, `adBody`, `adHeadline`, `adCaption`, and `adDescription`. Common aliases are normalized automatically: | Alias | Normalized to | | ----------------------------- | --------------- | | `businessName`, `displayName` | `pageName` | | `primaryText`, `body`, `copy` | `adBody` | | `headline` | `adHeadline` | | `caption` | `adCaption` | | `description` | `adDescription` | An asset with `copy_context` uses **two** scoring evaluations when the ablation succeeds — one for `full-ad-package`, one for `image-only`. Check `score_layer` on each ranked asset to see which layer produced the final score. ## Batch uploads and refinement Send up to 20 assets per request. Kettio evaluates up to 4 concurrently inside the request and returns a single sorted `ranked` array. By default (`refine_close_pairs: true`) Kettio checks successful assets for close pairs. When two scores fall within the refinement threshold, a pairwise panel can run and adjust the final order if it disagrees with the initial SSR ranking. | Field | What it tells you | | ------------------------------- | ---------------------------------------------------------------- | | `score_before_refine` | The raw SSR score before any close-pair adjustment | | `panel_outcome` | Winner, loser, whether the order flipped, votes, and consistency | | `summary.close_pair_refinement` | Whether refinement ran, how many pairs, and the raw results | Set `refine_close_pairs: false` when you want raw SSR-only ordering — for example, when you are running a controlled experiment and need the SSR scores frozen. Refinement is automatic and threshold-driven. When you already know which specific matchups matter, use the [Pairwise API](/guides/pairwise-api) instead — it runs a full five-voter panel on exactly the pairs you name. ## Handling partial failures A batch does not fail as a unit. Assets that could not be fetched or scored appear in `errors`, and the rest still rank. Always read `summary.assets_failed` before acting on `ranked` — a "winner" chosen from a batch where half the assets failed is not a winner. ```javascript theme={null} const { ranked, errors, summary } = await rankResponse.json(); if (summary.assets_failed > 0) { console.warn(`${summary.assets_failed} assets failed`, errors); } const [winner] = ranked; ``` ## Related Inline vs saved audiences, and the demographics that shape the persona. Rate limits and credit accounting, both measured in scoring evaluations. # Kettio for developers Source: https://docs.kettio.com/introduction Score and rank ad creative against a simulated audience before you spend media budget. Kettio scores ad creative against a simulated audience and returns a ranking. You send image URLs and optional ad copy, you name the audience you are buying, and you get back an ordered list with a rationale for each asset. The intended use is pre-flight triage: deciding which creatives are worth putting spend behind, and which pairs are close enough that they need a direct comparison. Kettio scores are **relative within the batch you submit**. They are not conversion probabilities, and they are not forecasts of CPA, ROAS, or causal lift. Treat them as a prioritization signal, not a prediction. ## Two surfaces Install into Claude Code, Codex, or Claude Desktop and rank creative straight from local files. Two commands and a key. Batch scoring over HTTP. Use this for volume: pipelines, scheduled jobs, and anything that runs without a client attached. Both reach the same scoring pipeline. The difference is where your files live: `rank_assets` over MCP takes **local paths** and uploads them for you, while the REST API expects assets already published at fetchable URLs. See [Agent loop](/guides/agent-loop) for choosing between them. ## Endpoints | Endpoint | What it does | | --------------------------------------- | ------------------------------------------------ | | [`POST /api/v1/rank`](/guides/rank-api) | Score and rank 1–20 assets against one audience. | \| [`POST /api/v1/pairwise`](/guides/pairwise-api) | Run a five-voter blinded panel on explicit head-to-head matchups. | Base URL is `https://kettio.com` for every endpoint. Authentication is a bearer token on every request — see [Authentication](/quickstart/authentication). ## What the score means Every asset is scored by Kettio's SSR pipeline against a persona built from the audience you supply. Each scoring evaluation runs three independent repetitions and averages them, so a single score already carries a spread you can inspect through `confidence_details`. When you include `copy_context`, Kettio scores the full ad package **and** runs an image-only ablation, so the response also tells you what the copy contributed (`copy_lift`). Repetitions, evaluations, copy lift, and the image-only ablation. When a gap is real and when two assets are effectively tied. ## Start here Open [Dashboard → API Keys](https://kettio.com/dashboard?section=api-keys). The raw key is shown once. Using a coding agent? [Set up your agent](/quickstart/agent-setup) — install, rank local files, done. Calling over HTTP? [First ranking](/quickstart/first-ranking) walks through a working request and its response. Read [Limits and billing](/concepts/limits-and-billing) before you scale up — billing is counted in scoring evaluations, not assets. # MCP installation Source: https://docs.kettio.com/mcp/installation Connect Claude Desktop, Codex, or any stdio MCP client to Kettio. The Kettio MCP server gives an AI client tools for working with brand assets: finding local files, reading brand context, generating and editing images, removing backgrounds, and opening previews. The server includes `rank_assets`, which scores local files directly — an MCP client does not need to touch the REST API to rank creative. See [Set up your agent](/quickstart/agent-setup) for the fastest path. ## Requirements * Node.js 18 or newer * A Kettio API key from [Dashboard → API Keys](https://kettio.com/dashboard?section=api-keys) ## Connect your client The server is distributed as a hosted tarball, so `npx` fetches it directly — there is nothing to clone and nothing to build. ```bash theme={null} claude mcp add kettio \ --transport stdio \ -e KETTIO_API_URL=https://kettio.com \ -e KETTIO_API_KEY=your-api-key-here \ -- npx -y --package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz kettio-mcp ``` Run `claude mcp list` and confirm `kettio` is connected. ```bash theme={null} codex mcp add kettio \ --env KETTIO_API_URL=https://kettio.com \ --env KETTIO_API_KEY=your-api-key-here \ -- npx -y --package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz kettio-mcp ``` Add to your Claude Desktop config. On macOS that is `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json theme={null} { "mcpServers": { "kettio": { "command": "npx", "args": [ "-y", "--package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz", "kettio-mcp" ], "env": { "KETTIO_API_URL": "https://kettio.com", "KETTIO_API_KEY": "your-api-key-here" } } } } ``` Configure a stdio server with this command and two environment variables: ```bash theme={null} npx -y --package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz kettio-mcp ``` ```bash theme={null} KETTIO_API_URL=https://kettio.com KETTIO_API_KEY=your-api-key-here ``` ## Verify the setup Restart your MCP client after saving the configuration, then ask: > What Kettio tools do you have? You should see `rank_assets`, `rank_ad_batch`, `get_rank_run`, and `list_audiences`. Full schemas are in the [tool reference](/mcp/tools). Kettio displays a new key only once. Do not commit it to your repository or paste it into public issue threads. If you lose it, create a replacement from [Dashboard → API Keys](https://kettio.com/dashboard?section=api-keys). ## How it fits together ```mermaid theme={null} flowchart TD A[Agent
Claude Code · Codex · Desktop] -->|"Rank the ads in public/ads/"| B[MCP server] B -->|reads local files| C[Your repo
public/ads/] B -->|uploads + scores| D[Kettio API] D -->|scores · confidence · rationale| B B -->|ranked result| A ``` ## Next End-to-end install, plus the rules worth giving the agent. Every tool and its parameters. # MCP tools Source: https://docs.kettio.com/mcp/tools Reference for the tools exposed by the Kettio MCP server. Tools available once the [MCP server is connected](/mcp/installation). Schemas here match `@kettio/mcp-server` v3.0.0. Every ranking tool takes **local file paths** and handles the upload for you. Inside an MCP client there is no separate "publish to a URL" step — that only applies when you call the [REST API](/guides/rank-api) directly. ## Choosing a ranking tool Scores and returns the ranking in one call. Use it for interactive work where you want the answer immediately. Queues the work and returns a run ID straight away. Use it for large batches, or when blocking the agent for the whole scoring pass is not acceptable. Both reach the same scoring pipeline and bill identically. The difference is whether you wait. ## rank\_assets Runs Kettio's audience-fit model on 1–20 local ad creatives and returns the ranked winner, real Kettio scores, confidence, rationale, and close-pair panel outcome. | Parameter | Type | Required | Description | | ---------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `assets` | `object[]` | Yes | 1–20 creatives. Each needs `path`; optional `id` and `copyContext`. | | `assets[].path` | `string` | Yes | Local path, relative to the workspace or absolute. | | `assets[].id` | `string` | No | Stable label such as `ad-1`. | | `assets[].copyContext` | `object` | No | `pageName`, `adBody`, `adHeadline`, `adCaption`, `adDescription`. Enables full-package scoring and copy lift. | | `audience` | `string` | Yes | Concrete audience description — needs, objections, awareness, buying context. | | `audienceName` | `string` | No | Short label shown in the result. | | `goal` | `string` | No | Defaults to `purchase-intent`. | | `platform` | `string` | No | Placement context such as `facebook`, `instagram`, `tiktok`. | | `assetType` | `string` | No | Defaults to `Social Media Post`. | | `refineClosePairs` | `boolean` | No | Pairwise panel on close scores. Defaults to `true`. | ## rank\_ad\_batch Queues an asynchronous ranking run and returns a run ID immediately. Poll [`get_rank_run`](#get-rank-run) to collect the result. Takes the same parameters as `rank_assets`, plus: | Parameter | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------------- | | `audienceId` | `string` | No | ID of a saved audience, from `list_audiences`. Use instead of `audience`. | | `batchId` | `string` | No | Caller-owned label for this batch, echoed back on the run. | `audience` is required unless you supply `audienceId`. ## get\_rank\_run Polls one run created by `rank_ad_batch`. | Parameter | Type | Required | Description | | --------- | -------- | -------- | ----------------------------------- | | `runId` | `string` | Yes | Run ID returned by `rank_ad_batch`. | Results are withheld until the whole batch finishes. A poll that returns `queued` or `processing` carries no ranking — it is not a partial answer to report from. Poll about every three seconds. Polling is never billed. ## list\_audiences Lists saved audiences on the authenticated account, with their IDs. | Parameter | Type | Required | Description | | --------- | -------- | -------- | ----------------------------------------------- | | `limit` | `number` | No | Maximum to return. Defaults to 50, maximum 200. | Pass an ID as `audienceId` to `rank_ad_batch` to score against a saved audience instead of retyping a description. Creating, editing, and deleting audiences happens in the [Kettio dashboard](https://kettio.com/dashboard), not through this server. The MCP surface is read-only for audiences. ## Workspace resolution The server resolves relative paths against, in order: 1. `KETTIO_WORKSPACE_PATH` when set 2. `CLAUDE_PROJECT_DIR` when available (Claude Code) 3. The MCP process working directory Set `KETTIO_WORKSPACE_PATH` explicitly if your agent runs from a different directory than the repo holding the creatives. ## Related Install the ranking pipeline into a coding agent end to end. The generate → score → decide cycle, and when to drop to REST. # Set up your agent Source: https://docs.kettio.com/quickstart/agent-setup Install the Kettio ranking pipeline into Claude Code, Codex, or any MCP client. Install Kettio into a coding agent once, and it can rank your ad creative from local files for the rest of the project. Two commands and a key. Dashboard → API Keys. Shown once — copy it now. One command per client. No npm publish required. ## Install The server ships as a hosted tarball, so `npx` fetches it directly. Nothing to clone and nothing to build. ```bash theme={null} claude mcp add kettio \ --transport stdio \ -e KETTIO_API_URL=https://kettio.com \ -e KETTIO_API_KEY=your-api-key-here \ -- npx -y --package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz kettio-mcp ``` Confirm it connected: ```bash theme={null} claude mcp list ``` ```bash theme={null} codex mcp add kettio \ --env KETTIO_API_URL=https://kettio.com \ --env KETTIO_API_KEY=your-api-key-here \ -- npx -y --package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz kettio-mcp ``` Add to `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS: ```json theme={null} { "mcpServers": { "kettio": { "command": "npx", "args": [ "-y", "--package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz", "kettio-mcp" ], "env": { "KETTIO_API_URL": "https://kettio.com", "KETTIO_API_KEY": "your-api-key-here" } } } } ``` Restart Claude Desktop after saving. Configure a stdio server with this command and two environment variables: ```bash theme={null} npx -y --package=https://kettio.com/downloads/kettio-mcp-server-3.0.0.tgz kettio-mcp ``` ```bash theme={null} KETTIO_API_URL=https://kettio.com KETTIO_API_KEY=your-api-key-here ``` Requires Node.js 18 or newer. Keep the key in the client's environment — never paste it into chat, commit it, or put it in browser code. ## Rank your first batch Drop some creatives into the repo and ask in plain language: > Rank the ads in `public/ads/` for women 30-45 considering a first premium skincare purchase who > are skeptical of before/after claims. Goal is purchase intent, platform is Instagram. The agent calls `rank_assets` with the local paths, and Kettio handles the upload. You get back scores, confidence, a rationale per asset, and the close-pair panel outcome. Point it at **files**, not URLs. `rank_assets` uploads local paths for you — the publish-to-a-URL step only applies when you call the [REST API](/guides/rank-api) directly. ## Teach the agent the rules The tool descriptions tell an agent *how* to call Kettio. They do not tell it how to interpret the result, and the failure mode is predictable: agents report rank 1 as "the winner" when it beat rank 2 by noise. Save this as `AGENTS.md` (or `CLAUDE.md`) in the project: ```markdown AGENTS.md theme={null} ## Ranking ad creative with Kettio Use the `rank_assets` MCP tool for any question about which creative will perform better. Do not substitute your own numeric assessment of an image — you are not calibrated for it. ### Required inputs - `assets`: local paths, 1-20 per call - `audience`: describe the buying situation and the objection to overcome, not a demographic profile. "Women considering GLP-1 support who need credible purchase proof" is useful; "our target customers" is not. - `copyContext`: include it whenever the ad copy is known. It enables copy-lift analysis. ### Reading the result - Scores are relative WITHIN the batch. Never compare scores across batches, goals, or audiences. - A gap smaller than 0.30 between two assets is not a ranking. Report them as tied, or escalate the pair rather than declaring a winner. - Check `confidence` and the repetition spread before stating any result with certainty. - If assets failed, the ordering is incomplete. Say so instead of reporting a winner. ### What NOT to claim Kettio scores are a prioritization signal. They are not conversion probabilities and not forecasts of CPA, ROAS, or lift. Never present a score as a predicted performance number. ``` The 0.30 line is not arbitrary — it is the threshold at which Kettio itself treats two assets as a close pair and is willing to run a pairwise panel. See [Reading confidence](/concepts/confidence). ## Feed the agent the full docs Every page here is available as Markdown for agent ingestion. Append `.md` to any URL: ```bash theme={null} curl https://docs.kettio.com/guides/rank-api.md ``` There are also two aggregate files, generated automatically and always current: | File | Contents | | --------------------------------------------------------- | ------------------------------------- | | [`/llms.txt`](https://docs.kettio.com/llms.txt) | Index of every page with descriptions | | [`/llms-full.txt`](https://docs.kettio.com/llms-full.txt) | Full text of the entire documentation | Point an agent at `llms.txt` when it needs to look things up on demand, or paste `llms-full.txt` when it needs the whole contract in context up front. ## Going beyond the MCP server The MCP server is built for interactive and repo-local work. Switch to the REST API when you need volume, scheduling, or a service that runs without a client attached. `POST /api/v1/rank` — up to 20 assets per request, 60 evaluations per minute. The full generate → score → decide cycle with backoff and failure handling. ## Troubleshooting Restart the client after saving the config. Then ask "what Kettio tools do you have?" — you should get `rank_assets`, `rank_ad_batch`, `get_rank_run`, and `list_audiences`. For Claude Code, `claude mcp list` shows connection status directly. The server resolves relative paths against `KETTIO_WORKSPACE_PATH`, then `CLAUDE_PROJECT_DIR`, then the process working directory. If your agent runs from a different directory than the creatives, set `KETTIO_WORKSPACE_PATH` explicitly. The key is missing or invalid in the client environment. Keys are prefixed `agk_live_`. Generate a replacement from [Dashboard → API Keys](https://kettio.com/dashboard?section=api-keys) — the raw value is only shown at creation. The limit is 60 scoring evaluations per minute per key, and an asset with copy context can cost two. See [Limits and billing](/concepts/limits-and-billing). # Authentication Source: https://docs.kettio.com/quickstart/authentication Create an API key and authenticate every request with a bearer token. Every Kettio API request needs a bearer token: ```http theme={null} Authorization: Bearer agk_live_YOUR_KEY ``` The base URL is `https://kettio.com` for all endpoints. ## Create a key Go to [Dashboard → API Keys](https://kettio.com/dashboard?section=api-keys). Keys are prefixed `agk_live_`. Kettio displays the raw key **once**. Put it straight into your secret manager. This is a server-to-server API. Never embed a key in browser or mobile code, commit it to a repository, or paste it into a public issue thread. If you lose a key, create a replacement from the dashboard. ## Verify it works ```bash theme={null} curl -X POST https://kettio.com/api/v1/rank \ -H "Authorization: Bearer agk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "assets": [{ "url": "https://cdn.example.com/ad-a.png", "id": "ad-a" }], "audience": { "name": "Millennial pet owners" } }' ``` A `401` means the key is missing or invalid. Anything else means authentication succeeded — see [error codes](#error-codes) below for what the other statuses mean. ## Error codes | Status | Meaning | | ------ | ----------------------------------------------------- | | `401` | Missing or invalid API key | | `402` | Insufficient credits for this batch | | `404` | `audience_id` not found, or not owned by your account | | `422` | Validation error in the request body | | `429` | Rate limited — check the `Retry-After` header | Rate limiting and credits are counted in **scoring evaluations**, not assets. See [Limits and billing](/concepts/limits-and-billing). ## Using a key with MCP The MCP server reads the same key from an environment variable: ```bash theme={null} KETTIO_API_URL=https://kettio.com KETTIO_API_KEY=agk_live_YOUR_KEY ``` Full setup for each client is in [MCP installation](/mcp/installation). # Your first ranking Source: https://docs.kettio.com/quickstart/first-ranking Send a batch of creatives to the Rank API and read the response. This walks through one complete `POST /api/v1/rank` call: what to send, what comes back, and which fields actually drive a decision. ## Send the request ```bash cURL theme={null} curl -X POST https://kettio.com/api/v1/rank \ -H "Authorization: Bearer agk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "assets": [ { "url": "https://cdn.example.com/ad-a.png", "id": "ad-a", "copy_context": { "pageName": "Pawbox", "adBody": "First box free for new subscribers. Fresh treats picked for picky dogs.", "adHeadline": "Healthy treats your dog will actually want", "adCaption": "Cancel anytime" } }, { "url": "https://cdn.example.com/ad-b.png", "id": "ad-b" } ], "audience": { "name": "Millennial pet owners", "description": "Dog owners aged 25-34, mid-income, active on Instagram", "demographics": { "ageRange": "25-34", "incomeLevel": "50k-75k", "priceSensitivity": "moderate", "shoppingIntent": "researching", "platformFatigue": "high" } }, "goal": "purchase-intent", "platform": "meta", "refine_close_pairs": true }' ``` ```javascript Node.js theme={null} const response = await fetch("https://kettio.com/api/v1/rank", { method: "POST", headers: { Authorization: `Bearer ${process.env.KETTIO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ assets: [ { url: "https://cdn.example.com/ad-a.png", id: "ad-a", copy_context: { pageName: "Pawbox", adBody: "First box free for new subscribers. Fresh treats picked for picky dogs.", adHeadline: "Healthy treats your dog will actually want", adCaption: "Cancel anytime", }, }, { url: "https://cdn.example.com/ad-b.png", id: "ad-b" }, ], audience: { name: "Millennial pet owners", description: "Dog owners aged 25-34, mid-income, active on Instagram", demographics: { ageRange: "25-34", incomeLevel: "50k-75k", priceSensitivity: "moderate", shoppingIntent: "researching", platformFatigue: "high", }, }, goal: "purchase-intent", platform: "meta", refine_close_pairs: true, }), }); const { ranked, summary } = await response.json(); ``` ```python Python theme={null} import os, requests response = requests.post( "https://kettio.com/api/v1/rank", headers={"Authorization": f"Bearer {os.environ['KETTIO_API_KEY']}"}, json={ "assets": [ { "url": "https://cdn.example.com/ad-a.png", "id": "ad-a", "copy_context": { "pageName": "Pawbox", "adBody": "First box free for new subscribers. Fresh treats picked for picky dogs.", "adHeadline": "Healthy treats your dog will actually want", "adCaption": "Cancel anytime", }, }, {"url": "https://cdn.example.com/ad-b.png", "id": "ad-b"}, ], "audience": { "name": "Millennial pet owners", "description": "Dog owners aged 25-34, mid-income, active on Instagram", "demographics": { "ageRange": "25-34", "incomeLevel": "50k-75k", "priceSensitivity": "moderate", "shoppingIntent": "researching", "platformFatigue": "high", }, }, "goal": "purchase-intent", "platform": "meta", "refine_close_pairs": True, }, ) payload = response.json() ``` Only two things are strictly required: at least one asset with a `url`, and an audience — either inline (`audience`) or a saved one (`audience_id`). Everything else has a default. ## Read the response ```json theme={null} { "request_id": "1f4d884c-f9ef-4f36-a8e9-f7b6a5b63291", "ranked": [ { "rank": 1, "asset_url": "https://cdn.example.com/ad-a.png", "asset_id": "ad-a", "score": 4.24, "score_before_refine": 4.23, "score_layer": "full-ad-package", "asset_type": "Social Media Post", "copy_context_included": true, "image_only_score": 3.81, "copy_lift": 0.42, "rationale": "The creative quickly communicates the offer and removes purchase friction.", "product_read": "A product-focused social ad with subscription-box imagery and a clear first-box-free offer.", "confidence": "high", "panel_outcome": null, "confidence_details": { "entropy": 0.38, "top_margin": 0.35, "sample_std_dev": 0.06, "repetition_count": 3, "generator_sample_count": 6 } } ], "errors": [], "summary": { "goal": "purchase-intent", "assets_ranked": 1, "assets_failed": 0, "scoring_evaluations": 2, "credits_used": 2, "credits_remaining": 48 } } ``` ### The fields that matter `ranked` comes back sorted. `score` is the final value after any close-pair refinement; `score_before_refine` is the raw SSR score. Both are relative within this batch. Present only when you sent `copy_context` and the image-only ablation succeeded. `copy_lift` is the difference between the full-ad-package score and the image-only score — what the words added on top of the picture. `confidence` is a coarse label. `confidence_details` carries the numbers behind it, including `sample_std_dev` across repetitions. A large spread means the pipeline disagreed with itself and the ordering is soft. See [Reading confidence](/concepts/confidence). Non-null only when close-pair refinement ran a pairwise panel on this asset. It reports the winner, whether the panel flipped the original SSR order, the votes, and consistency. Per-asset failures land here rather than failing the whole request. A batch can succeed with `assets_failed > 0` — always check it. `summary.credits_used` is **2** in this example for **2** assets — but that is a coincidence. The asset with `copy_context` cost 2 evaluations (full package plus image-only ablation) and the bare image asset cost 1, minus failures. Budget against `scoring_evaluations`, never against asset count. See [Limits and billing](/concepts/limits-and-billing). ## Next Every request field, copy-context aliases, and batch refinement. When you need a direct five-voter comparison instead of a score.