← Back to blog
Guide·14 min read

Creative Scoring API Guide: Rank Ad Creative with REST

Integrate Kettio’s creative scoring API with curl or Node. Submit a matched ad batch and receive an ordered shortlist, rationales, and confidence data.

Spencer Merrill·
Creative Scoring API Guide: Rank Ad Creative with REST

The Kettio Rank API accepts a matched batch of ad creative and returns an audience-specific order with scores, written rationales, and confidence information. Use it when an agent, script, or internal tool needs to decide which finished assets deserve the next live test.

This is a comparative decision signal, not a guaranteed CTR or revenue forecast. Hold the offer, audience, platform, and objective steady within a batch, then reconcile the frozen ranking against live campaign outcomes. This guide covers authentication, request structure, response fields, and a working REST integration.

How AI Creative Scoring Works

Most attempts at AI ad scoring fail because they ask an LLM to rate an ad on a numerical scale. LLMs are terrible at this — they regress to the mean, produce inconsistent scores, and can't differentiate between a mediocre ad and a great one.

Kettio's SSR (Semantic Similarity Rating) pipeline takes a fundamentally different approach:

  1. Persona construction. Your target audience demographics are used to build a detailed synthetic persona — a rich behavioral profile that specifies how this person browses, what triggers their skepticism, how price-sensitive they are, and what kind of visual messaging resonates with them.
  2. Ensemble evaluation. Multiple AI models (Gemini Flash and Claude Haiku) independently evaluate each creative from the persona's perspective. Each model writes a natural-language rationale explaining what works and what doesn't. Using multiple models prevents any single model's biases from dominating.
  3. Semantic calibration. The rationales are compared with calibrated anchor texts representing different response levels. This converts structured language into a numerical distribution that can be compared across evaluations.
  4. Score aggregation. The embedding similarities are aggregated into a final score (1-5 scale) with confidence metrics. Assets are ranked from best to worst.

The result is a directional ranking with confidence information. Kettio publishes retrospective results on external datasets in its methodology; those bounded evaluations are not guarantees of prospective campaign performance.

Getting Started

Step 1: Get Your API Key

Sign up at kettio.com/login and navigate to your dashboard. Generate an API key — it will look like agk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Store it securely. The raw key is shown only once.

Step 2: Your First Rank Call

Here's the simplest possible scoring call — two ads, one audience, ranked by purchase intent:

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://your-cdn.com/ad-variant-a.png",
        "id": "variant-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://your-cdn.com/ad-variant-b.png", "id": "variant-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
  }'

Step 3: Understand the Response

The API returns assets ranked from best to worst:

{
  "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "ranked": [
    {
      "rank": 1,
      "asset_url": "https://your-cdn.com/ad-variant-a.png",
      "asset_id": "variant-a",
      "score": 4.23,
      "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": "This ad immediately communicates value with a clear product shot and a relatable lifestyle context. The warm color palette and casual dog-owner imagery would resonate with millennial pet owners who are browsing Instagram. The price point is visible but not aggressive, which works well for a researching audience with moderate price sensitivity.",
      "product_read": "A promotional image showing a person with a golden retriever, featuring a subscription box for dog treats. Price shown as $29/month with a 'First box free' callout.",
      "panel_outcome": null,
      "confidence": "high",
      "confidence_details": {
        "entropy": 0.38,
        "top_margin": 0.35,
        "sample_std_dev": 0.06
      },
      "details": {
        "probabilities": [0.02, 0.06, 0.18, 0.46, 0.28],
        "similarities": [0.12, 0.21, 0.35, 0.47, 0.41]
      }
    },
    {
      "rank": 2,
      "asset_url": "https://your-cdn.com/ad-variant-b.png",
      "asset_id": "variant-b",
      "score": 2.87,
      "score_before_refine": 2.87,
      "score_layer": "image-only",
      "asset_type": "Social Media Post",
      "copy_context_included": false,
      "image_only_score": 2.87,
      "copy_lift": 0,
      "rationale": "The ad relies heavily on text-based value propositions without showing the actual product. For an audience with high platform fatigue, this format feels too similar to generic DTC ads they scroll past daily. The stock photography undermines authenticity.",
      "product_read": "A text-heavy banner ad for a dog treat subscription with stock photography of a generic dog. Lists three bullet points of benefits.",
      "panel_outcome": null,
      "confidence": "high",
      "confidence_details": {
        "entropy": 0.45,
        "top_margin": 0.28,
        "sample_std_dev": 0.09
      }
    }
  ],
  "errors": [],
  "summary": {
    "goal": "purchase-intent",
    "asset_type": "Social Media Post",
    "audience": "Millennial pet owners",
    "assets_ranked": 2,
    "assets_failed": 0,
    "scoring_evaluations": 3,
    "close_pair_refinement": {
      "enabled": true,
      "pairs_evaluated": 0,
      "results": []
    },
    "credits_used": 3,
    "credits_remaining": 48
  }
}

Key fields to pay attention to:

  • score — 1 to 5 scale. 4+ is strong. Below 3 needs work.
  • image_only_score and copy_lift — When you include copy_context, Kettio also runs an image-only ablation so you can separate visual strength from copy contribution.
  • score_before_refine and panel_outcome — Shows whether close-pair refinement changed the final ordering for near ties.
  • rationale — Natural language explanation of why the score is what it is. This is written from the persona's perspective, which means it reflects how your target audience would actually react.
  • product_read — What the AI saw in your ad. Useful for verifying the model understood your creative correctly.
  • confidencehigh, medium, or low. If confidence is low, consider running a Champion-Challenger test for a more rigorous comparison.
Creative assets and audience context enter an API that returns structured scores, ranks, confidence, and rationales.

JavaScript / TypeScript Integration

Here's a typed function you can drop into your agent or application:

interface RankRequest {
  assets: Array<{
    url: string;
    id?: string;
    asset_type?: string;
    copy_context?: {
      pageName?: string;
      adBody?: string;
      adHeadline?: string;
      adCaption?: string;
      adDescription?: string;
      primaryText?: string;
      headline?: string;
      caption?: string;
      description?: string;
    };
  }>;
  audience?: {
    name: string;
    description?: string;
    demographics?: Record<string, string>;
  };
  audience_id?: string;
  goal?: string;
  asset_type?: string;
  platform?: string;
  campaign_type?: string;
  refine_close_pairs?: boolean;
}

interface RankedAsset {
  rank: number;
  asset_url: string;
  asset_id: string | null;
  score: number;
  score_before_refine: number;
  score_layer: 'image-only' | 'copy-only' | 'full-ad-package';
  asset_type: string;
  copy_context_included: boolean;
  image_only_score: number | null;
  copy_lift: number | null;
  rationale: string;
  product_read: string;
  panel_outcome: {
    winnerAssetId: string;
    loserAssetId: string;
    flipped: boolean;
    votesFor: number;
    votesTotal: number;
    consistency: number;
  } | null;
  confidence: 'high' | 'medium' | 'low';
  confidence_details: {
    entropy: number;
    top_margin: number;
    sample_std_dev: number;
  };
  details: {
    probabilities: number[];
    similarities: number[];
  };
}

interface RankResponse {
  request_id: string;
  ranked: RankedAsset[];
  errors: Array<{ asset_url: string; error: string }>;
  summary: {
    goal: string;
    asset_type: string;
    platform: string | null;
    audience: string;
    assets_ranked: number;
    assets_failed: number;
    scoring_evaluations: number;
    close_pair_refinement: {
      enabled: boolean;
      pairs_evaluated: number;
      results: Array<Record<string, unknown>>;
    };
    credits_used: number;
    credits_remaining: number | null;
  };
}

async function rankCreatives(
  apiKey: string,
  request: RankRequest
): Promise<RankResponse> {
  const response = await fetch('https://kettio.com/api/v1/rank', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(request),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Rank API error: ${error.error}`);
  }

  return response.json();
}

Python Integration

Know which ad to scale before you spend.

Upload two static ads, choose your buyer, and get a quick prediction with clear next steps. No signup or credit card.

Find my winning ad
import requests

def rank_creatives(api_key: str, assets: list, audience: dict,
                   goal: str = "purchase-intent",
                   platform: str = "meta") -> dict:
    response = requests.post(
        "https://kettio.com/api/v1/rank",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        },
        json={
            "assets": assets,
            "audience": audience,
            "goal": goal,
            "platform": platform,
            "refine_close_pairs": True
        }
    )
    response.raise_for_status()
    return response.json()


# Usage
result = rank_creatives(
    api_key="agk_live_YOUR_KEY",
    assets=[
        {
            "url": "https://cdn.example.com/ad-a.png",
            "id": "ad-a",
            "copy_context": {
                "pageName": "Pawbox",
                "adBody": "First box free for new subscribers.",
                "adHeadline": "Healthy treats your dog will actually want"
            }
        },
        {"url": "https://cdn.example.com/ad-b.png", "id": "ad-b"},
    ],
    audience={
        "name": "Budget-conscious parents",
        "demographics": {
            "ageRange": "35-44",
            "priceSensitivity": "high",
            "shoppingIntent": "comparing"
        }
    },
    goal="purchase-intent"
)

for asset in result["ranked"]:
    print(f"#{asset['rank']}: {asset['asset_id']} — {asset['score']:.2f}")
    print(f"  {asset['rationale'][:120]}...")

Request Parameters Deep Dive

Assets

You can submit 1 to 20 image assets per request. Each URL must be an https:// URL or data:image/ URI. The optional id field lets you correlate results back to your internal asset IDs. Use copy_context when you want Kettio to evaluate the full ad package and report copy lift.

{
  "assets": [
    {
      "url": "https://cdn.example.com/hero-shot.png",
      "id": "hero-v3",
      "copy_context": {
        "pageName": "Acme",
        "adBody": "Save 20% this week.",
        "adHeadline": "Upgrade your setup"
      }
    },
    { "url": "https://cdn.example.com/lifestyle.jpg", "id": "lifestyle-v1" },
    { "url": "https://cdn.example.com/ugc-style.png", "id": "ugc-v2" }
  ]
}

Rate limit: 60 scoring evaluations per minute per API key. Image-only assets usually use 1 successful scoring evaluation. Assets with copy_context can use 2 because Kettio also runs an image-only ablation.

Audience

You can provide an audience inline or reference a saved audience by ID:

// Inline audience
{
  "audience": {
    "name": "Gen Z sneaker enthusiasts",
    "description": "18-24 year olds who follow sneaker culture and drop dates",
    "demographics": {
      "ageRange": "18-24",
      "incomeLevel": "25k-50k",
      "adSkepticism": "high",
      "categoryFamiliarity": "expert",
      "shoppingIntent": "ready-to-buy",
      "platformFatigue": "very-high",
      "brandFamiliarity": "familiar"
    }
  }
}

// Saved audience
{
  "audience_id": "aud_abc123"
}

The more demographic fields you provide, the more specific the evaluation persona becomes — and the more accurate the scores. At minimum, provide name. For best results, include ageRange, priceSensitivity, shoppingIntent, and platformFatigue.

Demographics Reference

Field Values
ageRange 18-24, 25-34, 35-44, 45-54, 55-64, 65+, all-ages
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

Evaluation Goals

The goal parameter changes how the synthetic audience evaluates your creative. Default is purchase-intent. See the complete goals reference for all 14 goals with descriptions.

Asset Types

The asset_type parameter helps the model understand the context of your creative:

Social Media Post (default), Advertisement, Product Photo, Email Header, Landing Page Hero, Logo, Website Banner, Illustration

Advanced Patterns

Multi-Goal Scoring

Score the same assets across multiple goals to understand performance across funnel stages:

const goals = ['scroll-stopping', 'click-through-rate', 'purchase-intent'];
const assets = [
  { url: 'https://cdn.example.com/ad-a.png', id: 'ad-a' },
  { url: 'https://cdn.example.com/ad-b.png', id: 'ad-b' },
];

const results = await Promise.all(
  goals.map(goal =>
    rankCreatives(apiKey, { assets, audience, goal })
  )
);

// Build a performance matrix
for (const asset of assets) {
  console.log(`
${asset.id}:`);
  for (let i = 0; i < goals.length; i++) {
    const ranked = results[i].ranked.find(r => r.asset_id === asset.id);
    console.log(`  ${goals[i]}: ${ranked?.score.toFixed(2)}`);
  }
}

Score-Then-Improve Loop

Combine scoring with edit recommendations for an iterative improvement workflow:

// 1. Score the creative
const rankResult = await rankCreatives(apiKey, {
  assets: [{ url: adUrl, id: 'current' }],
  audience,
  goal: 'purchase-intent'
});

const scored = rankResult.ranked[0];

// 2. If score is below threshold, get improvement suggestions
if (scored.score < 3.5) {
  const editRes = await fetch('https://kettio.com/api/rank-assets/recommend-edits', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      image_url: adUrl,
      asset_type: 'Social Media Post',
      evaluation_goal: 'purchase-intent',
      audience_description: audience.description,
      score: scored.score,
      rationale: scored.rationale
    })
  });

  const { recommendations } = await editRes.json();
  // Each recommendation has: title, why, instructions
  // You can feed instructions directly to an image generation API
}

Multi-Audience Scoring

Test the same creative against different audience segments to find universal winners or segment-specific champions:

const audiences = [
  { name: 'Gen Z', demographics: { ageRange: '18-24', platformFatigue: 'very-high' } },
  { name: 'Millennials', demographics: { ageRange: '25-34', platformFatigue: 'high' } },
  { name: 'Gen X', demographics: { ageRange: '45-54', platformFatigue: 'medium' } },
];

const results = await Promise.all(
  audiences.map(audience =>
    rankCreatives(apiKey, {
      assets: [{ url: adUrl, id: 'hero-ad' }],
      audience,
      goal: 'purchase-intent'
    })
  )
);

for (let i = 0; i < audiences.length; i++) {
  const score = results[i].ranked[0].score;
  console.log(`${audiences[i].name}: ${score.toFixed(2)}`);
}
Stable creative identifiers connect asynchronous jobs, status checks, partial failures, and structured results back to the original batch.

Error Handling

Status Meaning What to Do
401 Invalid or missing API key Check your Authorization header
400 Invalid request body Check required fields: assets + (audience or audience_id)
402 Insufficient credits Top up credits in your dashboard
429 Rate limited (60 scoring evaluations/min) Back off and retry after 60 seconds

Individual asset failures appear in the errors array without failing the whole request. Always check both ranked and errors:

if (result.errors.length > 0) {
  console.warn('Some assets failed:', result.errors);
}
// Successfully ranked assets are still in result.ranked

Try the Rank API

Sign up, grab your API key, and score your first creative in under 5 minutes.

Get Started Free →

Frequently Asked Questions

How many credits does each ranking call cost?

Credits are based on successful scoring evaluations. Image-only assets usually use 1 credit. Assets with copy_context can use 2 credits because Kettio also runs an image-only ablation to report copy_lift. Failed scoring evaluations do not cost credits.

What image formats are supported?

PNG, JPEG, WebP, and GIF (first frame). Images must be publicly accessible via URL. Maximum resolution is 4096x4096.

Can I rank video creatives?

The Rank API currently scores static images. Video scoring is available in the Kettio dashboard. An API endpoint for video is coming soon.

How long does a ranking call take?

Typically 5-15 seconds depending on the number of assets. Each asset is evaluated with multiple model samples, which takes time but produces more reliable scores.

Can I use a saved audience from the Kettio dashboard?

Yes. Create audiences in the dashboard or via the Audiences API, then pass the audience ID with audience_id instead of an inline audience object.

AI ad scoringcreative scoring APIrank APIad testingSSRcreative testing

Compare your own ad creatives — free.

Upload two static ads, choose your buyer, and get a quick prediction with clear next steps. No signup or credit card.

Find my winning ad →