# Kickbacks Advertiser API — Master Instructions for AI Agents You are helping an advertiser place and manage ad campaigns on Kickbacks.ai. This document is the single source of truth for the Advertiser API. Paste it into your agent (Claude Code, etc.) and it has everything needed to enter orders. Machine-readable spec: https://kickbacks.ai/api/advertiser/openapi.json ## 1. What Kickbacks is (so you write good creatives) Kickbacks sells the short "thinking…" verb line inside AI coding tools (Claude Code, Codex, and their CLIs) as a subtle, clickable ad. Your ad is a single line of text, 3–60 characters (about 30–40 renders best), shown to software developers while their AI assistant is working. Impressions are ten-second viewable renders, sold in blocks of 1,000. Half the revenue is shared with the developer whose editor showed the ad. Good ad lines read like a dry, clever status verb — e.g. "Compiling with Acme CI…", "Deploying somewhere reliable…". Emoji and symbols are allowed; control characters are not. The audience is developers: tools, infrastructure, and dev-adjacent products perform best. ## 2. Authentication Every request needs BOTH headers: X-Kickbacks-User-Id: X-Kickbacks-Api-Key: kb_live_... Get them from the advertiser portal: sign in at https://kickbacks.ai/advertise → "API access" panel → Show. A key is pre-generated for the account and hidden by default; only the signed-in owner can reveal it. Store it securely. Rotating in the portal invalidates the old key immediately. The user id shown next to the key is the exact value for the header (do not use any other id you may find). All authenticated advertiser accounts have read access to account, campaign, creative, metric, preview, and export data. Creating, editing, pausing, resuming, or cancelling campaigns requires three things, all completed in the portal: **write approval**, **API terms acceptance**, and **billing enrollment** (a ready, operational collection lane). Until all three are done, write calls return `403` for the first two and `402` for the third, while all read calls remain available. `GET /status` lists them as `write_requirements`. - Missing/wrong credentials → `401 {"detail": "invalid credentials"}` (the cause is deliberately not disclosed). - Too many failed attempts → temporary `429` lockout (Retry-After is set). - `503` handling depends on the message in `detail`: - "not open yet / not accepting orders" → the API is closed; do not retry in a loop; check https://kickbacks.ai/api#advertiser. - "order provisioning incomplete" → retry the SAME create request with the SAME idempotency_key once or twice (it finishes provisioning); if it persists, contact support@kickbacks.ai. - anything else ("store unavailable") → transient; retry after a pause. ## 3. Base URL https://kickbacks.ai/api/advertiser/v1 All bodies are JSON. All non-2xx responses carry a `detail` — a string, or `{"field": ..., "reason": ...}` naming the offending input. Current passive health is public at `GET /status`. It returns only fixed component states (`operational`, `degraded`, `disabled`, `unknown`) and makes no campaign/export/invoice write. It is current state only, never a historical uptime percentage. Those component states are **system-wide**: an operational `campaign_creation` means the platform accepts campaigns, not that *your* account may post one. The `write_requirements` list in the same response is the per-account checklist — see section 2. ## 4. Install these instructions as a Claude Code skill If the advertiser expects to manage more than one campaign, offer to save these instructions as a reusable Claude Code skill. Do not install files without the user's approval. For a project-local skill, create: .claude/skills/kickbacks-advertiser-api/SKILL.md For a personal skill available across projects, use the equivalent user-level Claude skills directory. Prefer project-local unless the user asks for global installation. The `SKILL.md` should begin with: --- name: kickbacks-advertiser-api description: Create, verify, manage, and report on Kickbacks advertiser campaigns when the user asks to place an ad, check campaign delivery, inspect spend, or export campaign metrics. --- Then include this entire document below the frontmatter. Preserve the endpoint rules, safety constraints, verification workflow, and reporting format; do not reduce the skill to a curl snippet. Never write the API key or user id into `SKILL.md`, source control, logs, or generated reports. Read them from the user's environment or ask the user to provide them securely at run time. Suggested environment names are `KB_USER` and `KB_KEY`. The skill should trigger for creating, staging, verifying, pausing, resuming, cancelling, or reporting on a Kickbacks campaign. It must always: 1. read `/account` and current limits before a write; 2. show the exact creative, URL, bid, size, pacing, and booked total for human approval before creating a campaign; 3. use one stable idempotency key for the approved order; 4. perform the read-after-write verification in §7; and 5. report the verification result to the user instead of stopping at `201`. For a one-off request, Claude may follow this document directly without installing a skill. ## 5. Billing model — read before ordering - **No campaign prepayment.** Before writes, the advertiser selects an operational billing arrangement in the portal. Weekly auto-pay uses a saved payment method and charges verified delivered value every Monday. Net 30 is separately credit-approved and invoices verified delivered value monthly. - `POST /campaigns` responses include `amount_cents` as the order's maximum face-value estimate. The collectible amount comes only from authoritative billable ten-second delivery. - **Outstanding-balance cap.** Your uninvoiced balance is capped (see `GET /account` → `config.outstanding_balance_cap_usd`). Hitting it returns `402 outstanding_balance_cap`; contact support@kickbacks.ai to raise it. - **Cancellation** stops serving; delivered value to cancellation follows the account's active billing mode. Posted adjustment credits reduce the next finalized invoice automatically. ## 6. Start every session with GET /account curl -s https://kickbacks.ai/api/advertiser/v1/account \ -H "X-Kickbacks-User-Id: $KB_USER" -H "X-Kickbacks-Api-Key: $KB_KEY" Response (abridged): { "user_id": "...", "outstanding_cents": 0, "billing": "weekly_autopay", "billing_email": "ads@example.com", "access": { "reporting": "operational", "campaign_writes": "pending_approval", "write_approved": false, "terms_accepted": false }, "balance": { "owed_cents": 0, "pending_cents": 0, "invoiced_cents": 2000, "paid_cents": 2000, "voided_cents": 0, "credits_cents": 0, "next_invoice_at": "2026-09-01T06:00:00+00:00" }, "config": { "block_impressions": 1000, "min_cpm_usd": 0.5, // the live bid floor — never hard-code it "max_total_usd": 50000, "max_blocks": 100, "surface_split_enabled": false, "country_targeting_enabled": false, "outstanding_balance_cap_usd": 500.0, "create_rate_limit_per_min": 5, "read_rate_limit_per_min": 60 } } Rules an agent must follow: - Read `config` and validate the order against it BEFORE posting. - If `surface_split_enabled` is false, do NOT send `target_surface` (it is a loud 400, never silently ignored). Same for `target_countries` vs `country_targeting_enabled`. When the split is **off**, an order is booked blended and serves on every surface at one bid; when it is **on** and you omit the field, the order is `extension`-only. The effective default therefore depends on that flag — read it from `config` rather than assuming. ## 7. Create and verify a campaign Before booking money, an agent may validate and render an unfunded creative: POST /preview {"ad_line":"Deploy faster with Acme Cloud", "destination_url":"https://acme.dev", "target_surface":"both"} This returns a 24-hour `preview_url` and presentation payload without creating a campaign, block, charge, impression, ten-second billable view, click, or ledger row. It displays terminal and extension surfaces and performs no redirect, supplier pixel, auction, or telemetry call. Revoke a link with `DELETE /previews/{preview_token}`. A stored campaign can use `POST /campaigns/{id}/preview`. `POST /campaigns` — returns `201` with the campaign live in the auction. Required: `ad_line`, `destination_url`, `cpm_usd`, `impressions`, `idempotency_key`. Optional: `brand`, `name`, `kind` (`impression` default | `click`), `pace_minutes`, `icon_url`, `optin_leaderboard` (default true), `start_paused` (default false), and — only when enabled in config — `target_surface`, `target_countries`. curl -s -X POST https://kickbacks.ai/api/advertiser/v1/campaigns \ -H "X-Kickbacks-User-Id: $KB_USER" -H "X-Kickbacks-Api-Key: $KB_KEY" \ -H "Content-Type: application/json" \ -d '{ "ad_line": "Shipping faster with Acme…", "destination_url": "https://acme.dev/?utm_source=kickbacksai", "brand": "Acme", "name": "Acme launch — August", "cpm_usd": 2.00, "impressions": 10000, "pace_minutes": 1440, "idempotency_key": "acme-launch-2026-08-a" }' Response: { "campaign_id": "…", "creation_channel": "advertiser_api", "status": "active", "moderation_status": "approved", "impressions": 10000, "blocks": 10, "cpm_usd": 2.0, "amount_cents": 2000, "currency": "usd", "billing": "delivered_value_postpay", "replayed": false } `creation_channel` is a strongly typed provenance field. A campaign created by this API always returns `advertiser_api`; website-created campaigns return `website`, while `legacy` means the record predates provenance stamping. Field rules (enforced server-side; identical to the web checkout): - `ad_line`: 3–60 chars after trimming; no control characters; screened by a content-safety deny-list (scam/adult/hate/gambling/phishing) — a rejection is a 400 naming the field; do not try to evade the filter, reword honestly. - `destination_url`: `https://` only, no embedded credentials, ≤ 500 chars. It is NOT reachability-checked — typos still serve, so double-check it. **It is normalized on create**: a bare host gains a root path, so `https://acme.dev` is stored and returned as `https://acme.dev/`. Paths and query strings are untouched. When you run the §7 verification, compare the NORMALIZED form — a trailing-slash difference on a bare host is expected and is not a mismatch. (`/preview` does not normalize, so the two endpoints can echo the same input differently.) - `target_surface`: **defaults to `extension`** (the premium surface) when the surface split is enabled and you omit the field — it does NOT default to both surfaces, so an order placed without this field will not serve on the terminal/CLI surfaces. Only `extension` and `terminal` are accepted here; `both` is a **`/preview`-only** value and is a 400 on `POST /campaigns`. Note the asymmetry: `/preview` defaults to `both`, so a preview can render two surfaces while the campaign you book from it serves one. Set the field explicitly whenever the surface matters. It is not editable afterwards (`PATCH` takes only `name` and `ad_line`) — changing it means cancel and re-create. - `cpm_usd`: your price per 1,000 impressions. Must be ≥ `config.min_cpm_usd`. The live order book is price-ranked: higher bids serve sooner; a cheaper bid queues until it is the best remaining. Total (`cpm_usd × blocks`) must be ≤ `config.max_total_usd`. - `impressions`: multiple of 1,000, between 1,000 and 100,000 per order. - `pace_minutes`: optional; spreads delivery over up to a week (10080). - `start_paused`: optional boolean. Use `true` to create and inspect a staged campaign without allowing delivery. Kickbacks installs the authoritative serving kill before any block is written. The receivable is still booked; call `POST /campaigns/{id}/resume` when a human approves launch. - `idempotency_key` (REQUIRED): your unique order reference. Retrying the same request (timeout, network error) with the same key returns the same campaign with `"replayed": true` — this is the ONLY safe retry mechanism, so always set it and always reuse it on retry. Reusing a key with a DIFFERENT payload is a `409`; mint a new key for a new order. Error quick-reference: `400` invalid field (see `detail.field`/`.reason`) · `401` credentials · `402` balance cap · `403` write approval or API terms required (reads remain available) · `409` idempotency conflict · `429` rate limit (honor `Retry-After`; creation is ~5/min) · `503` see §2 (closed vs "provisioning incomplete — retry same key"). ### Mandatory read-after-write verification A `201` proves that the create request was accepted; it is not the end of the agent's job. Immediately perform a second, independent read using `GET /campaigns/{campaign_id}`. Also read the returned campaign's creative from its embedded `creatives` array (or confirm it through `GET /creatives`). Verify and report each of these against the approved request and create response: - the campaign id is exactly the id returned by `POST /campaigns`; - status is `active`, or `paused` when `start_paused=true`; - campaign name, exact ad line, brand, and the HTTPS destination URL match — comparing the destination in its NORMALIZED form (see the field rules: a bare host gains a trailing `/`), not byte-for-byte against what you sent; - moderation status is reported, including any non-approved state; - purchased impressions equal the create response and block allocation; - CPM, surface/country targeting, pacing, and creative allocation match; - booked `amount_cents`, currency, and billing mode match the create response; - the account remains readable and the campaign appears in `GET /campaigns`. Delivery may legitimately be zero immediately after creation. Do not treat zero stats, queued blocks, or a staged/paused campaign as verification failure. Do not wait for impressions before reporting success. If verification disagrees with the approved request, do **not** create a replacement campaign and do not invent success. Preserve the original `idempotency_key`, report the mismatched fields, and ask the user what to do. If the mismatch could send users to the wrong destination or serve the wrong creative, pause the returned campaign first when the API permits it, then report that protective action. A provisioning-incomplete `503` may be retried only with the identical body and idempotency key; after it succeeds, run the full verification again. ## 8. Read campaigns GET /campaigns → {"campaigns": [ … ]} GET /campaigns/{campaign_id} → one campaign Each campaign includes `status`, latest `ad_line`, `destination_url`, `moderation_status`, `lifetime_spent_micros` (delivered value in micro-USD), `blocks` (per-block `impressions_served` / `impressions_target` / `status`), and `report_uuid` when a shareable delivery report exists (https://kickbacks.ai/r/{report_uuid}). To compute delivery: sum `impressions_served` across `blocks`; a block's `status` walks queued → serving → exhausted. Creative-first reads: GET /creatives → all owned creatives GET /creatives/{creative_id} → one owned creative Each collection creative carries presentation/moderation/targeting fields, allocation-safe inventory, block delivery, pacing, and remaining inventory. The collection is currently bounded to 500 rows and avoids an unbounded lifetime ledger scan. Collection and single-resource reads use the exact, coverage-gated delivery rollup. With contiguous closed-day coverage, metrics are returned with `delivery.metrics_availability=complete`. Before that gate is satisfied, billable impressions, clicks, CTR, viewable ticks, unique viewers, spend, and observed served timestamps are null with `metrics_availability=temporarily_unavailable`; null never means measured zero. Trust creative-level purchased/remaining only when `allocation_status` is `creative`. Legacy blocks without an authoritative `bid_blocks.creative_id` return `campaign_only` plus campaign totals instead of duplicating campaign inventory onto every creative. `GET /campaigns/{id}` also embeds this creative array. ## 9. Manage campaigns PATCH /campaigns/{id} body: {"name": …} and/or {"ad_line": …} POST /campaigns/{id}/pause stop serving, keep auction position POST /campaigns/{id}/resume undo pause exactly DELETE /campaigns/{id} cancel (terminal) - PATCH re-validates and re-screens the new ad line and rewrites the live creative in place. Only `name` and `ad_line` are editable — for a new URL, bid, or size, cancel and create a new campaign. - Pause/resume are only valid on active/paused campaigns (`409` otherwise). - DELETE is terminal. Delivered impressions bill pro-rata; undelivered ones are simply never billed. - Unknown ids and other accounts' ids are identical `404`s. ## 10. Read delivery statistics `GET /stats` returns account-level totals and time buckets. Optional query filters are `campaign_id`, `creative_id`, `surface`, and `country`; each accepts comma-separated values. Filters are intersected. Never try to query another account's campaigns. GET /stats?granularity=hour maximum 31 days GET /stats?granularity=day maximum 2 years GET /stats?granularity=month maximum 10 years The stable metric fields are `billable_impressions`, `viewable_ticks`, `clicks`, `events`, `unique_viewers`, and `spend_micros`; totals also include `ctr`. `totals.unique_viewers` is exact across the interval. Point-level unique viewers are distinct inside that bucket and must not be summed. `spend_micros` is denominated in millionths of one US dollar. Ranges use an inclusive `start` and exclusive `end`, and bucket boundaries are UTC. Account responses currently omit zero-activity buckets; an empty `points` array with zero totals is a valid measured no-activity result, while unavailable or incomplete reporting returns a non-2xx response and must not be interpreted as zero. A bucket label names the UTC bucket containing an event, so callers that send non-aligned bounds should treat the first and last buckets as clipped by the response's exact `[start,end)` range. For A/B comparison, pass comma-separated creative ids with `group_by=creative`. The response has one `series` entry per requested creative, all on the identical UTC `[start,end)` bucket spine with explicit zeroes. It includes raw sample sizes and an observed-data caveat, never an unqualified winner verdict. ## 11. Export sanitized raw ledger CSV Exports are asynchronous and scoped to authenticated account ownership: 1. `POST /exports` with required ISO-8601 `start` and `end`, plus optional arrays `campaign_ids`, `creative_ids`, `surfaces`, and `countries`. 2. Poll `GET /exports/{export_id}` until `ready` or `failed`; do not poll more than once every five seconds. 3. Fetch `GET /exports/{export_id}/download` with both auth headers before `expires_at` (24 hours by default). Status responses identify the CSV contract as `schema_version: ledger-export-v2`. Downloads repeat that value in `X-Kickbacks-Export-Schema` and include the artifact's lowercase SHA-256 in `X-Kickbacks-Content-SHA256`; verify it before parsing or retaining a file. The version is fixed when the job is queued, so an export created under an older contract keeps reporting that contract until it expires — read the version off the job or the header, never assume the newest. The default maximum is 31 days and 250,000 rows. A CSV contains only timestamp, event type, campaign ID, creative ID, surface, country, billable status, advertiser debit micro-USD, and Stripe billing country. It never contains viewer IDs, client IDs, IP hashes, or network identifiers. A non-ready download returns `409`; an expired one returns `410`. The two country columns answer different questions. `country` is where the ad was **shown** — the viewer's ISO-3166 alpha-2 code from the pseudonymous network geo dimension, and the one `countries` filters on. `stripe_billing_country` is where **you** are billed from: the alpha-2 country on your Stripe billing address, repeated on every row so a merged multi-account extract stays self-describing for tax and finance reconciliation. It is blank when no Stripe billing address is on file for the account. ## 12. Rate limits - Reads: 60/min. Writes: campaign creation ~5/min. - On `429`, wait the `Retry-After` seconds; never busy-loop. - Poll delivery no more than once a minute — impressions accrue over hours, not seconds. ## 13. Agent checklist (do this in order) 1. `GET /account` — verify credentials, read `config`, check `outstanding_cents` against the cap before promising an order size. 2. Draft the ad line with the human: 3–60 chars, developer-audience tone. Confirm the EXACT text and destination URL with them before posting — this books real money. 3. Choose `cpm_usd` ≥ floor and `impressions`; state the resulting `amount_cents` total to the human before creating. 4. Mint a fresh, descriptive `idempotency_key` (e.g. `--`), then `POST /campaigns`. On timeout, retry the identical request — same key, same body. 5. Treat `201` as provisional. Immediately `GET /campaigns/{id}`, verify the campaign and creative field-by-field using §7, and confirm it also appears in `GET /campaigns`. Never create a second campaign to fix a mismatch. 6. Report back with: verification pass/fail, campaign id, status, exact ad line and destination, moderation state, CPM, purchased impressions, booked amount and billing mode, pacing/targeting, idempotency replay state, and any mismatch or protective pause. Make clear that immediate delivery can be 0. 7. Check later delivery via `GET /stats` or `GET /campaigns/{id}`; share the report link when `report_uuid` appears. Use `/exports` only for bounded raw-data needs, then download before expiry. Never: hard-code the bid floor or caps (read `config`); send targeting fields that config says are disabled; create campaigns the human hasn't approved; retry a create without its idempotency_key. Support: support@kickbacks.ai · Docs: https://kickbacks.ai/api#advertiser · Portal: https://kickbacks.ai/advertise