Kaminari Ad

Release notes

Notable changes to the Kaminari Ad platform.

October 2026 — Working banner controls are told apart from painted ones

A new system tag in the behavior category, severity low: creative_interactive, for a creative where some buttons or links do not open the landing, such as carousel arrows that switch slides. Once the landing is captured, the creative's buttons and links under a grid of points are clicked, once each, until one of them opens nothing. Every page load those clicks start is stopped inside the browser until the check ends, so none of them opens the click-through URL. Tracking requests the creative sends on its own when clicked still go out. A banner that opens the landing wherever it is clicked does not get the tag.

The tag is informational and is meant for combo rules: none_of: [creative_interactive] lets a fake-interface verdict stand only for banners whose arrows and buttons are not working controls. See the Detection catalog.

October 2026 — Bulk scans take a campaign, and country codes ignore case

POST /api/v1/scans/bulk now accepts every optional field of POST /api/v1/scans: campaign_id and run_id, ad_discovery with max_discovered_ads and ad_formats, on top of the fields it already took. With a campaign_id, every country's scan joins the campaign's run and can raise alerts, so a multi-country check no longer has to be split into one call per country to get them. See Scans API and Alerts.

country_code and country_codes are now case-insensitive: us is accepted, stored and returned as US. Scans submitted in lowercase before this change have been converted, so they now match country filters and country-scoped policy rules.

October 2026 — Test up to 20 rules against 50 scans in one call

POST /api/v1/custom-rules/test-batch previews unsaved detection rules against stored scans: up to 20 rules and 50 scans per call, one result cell per rule and scan. A cell that could not run carries an error_code — for example deadline_exceeded when the call ran out of time, so you can send just those cells again — and never fails the rest of the call. One batch per organization runs at a time. The MCP server exposes it as test_custom_rules_batch. See Rule testing API.

October 2026 — Landings the AI refuses to analyse are tagged

A new system tag in the llm category, severity medium: ai_prohibited_content. It marks a scan whose landing the AI content analysis refused to process because its safety filter flagged the page as prohibited content. These scans still finish as partial, since they have no AI verdict, but the reason is now visible on the scan and usable in policies, alerts and search. See Detection catalog.

September 2026 — Route alerts by scan label, and one Save for a campaign

A shared campaign can now send each partner only its own alerts. Turn on Route by label in a campaign's Notifications section, pick a scan label key such as dspName, and give each partner destination a label rule: alerts go to the campaign's usual destinations plus every destination whose rule matches the scan's value. The API takes routing_label_key on the campaign overrides endpoint and returns each destination's rule as route_label_key / route_label_values. See Alerts.

A campaign's notification settings are now part of the campaign form: one Save stores both, on the new-campaign page too, and the API accepts an optional notifications block on campaign create and update. See Campaigns.

The Alert Notifications page is reorganised for large setups: campaign routing collapses to the three most recent changes with a search, and destinations get compact rows, channel tabs, delivery health, and a side panel per destination. See Account & settings screens.

September 2026 — Ad discovery can check only the formats you pick

Ad discovery campaigns and scans accept a new optional ad_formats: any of banner, video and pop (pop-unders and tab-unders). Pick pop alone to check a publisher's pops without screenshotting, clicking or paying for every banner on the page. Leave it out, or tick every format in the campaign form, and a scan checks everything, exactly as before. See Creating scans and Campaigns.

September 2026 — Back-button hijacks and autoplaying sound are tagged

Two new system tags in the behavior category, both severity medium: back_button, for a landing that fills the browser history with copies of itself and intercepts the Back button to push another offer, and autosound, for a landing that tries to play sound on open without the visitor pressing play. Neither is visible on a screenshot, so until now a moderation prompt could not catch them.

Both are read from the landing's own behaviour while it loads — nothing is clicked on the page to trigger them — and attach to the scan like any other system tag, so they show on the scan card and can be used in policies, custom rules and alerts. See the Detection catalog.

September 2026 — Ad discovery finds the same ads on every phone width

Ad discovery recognised a full-width mobile unit only where its rendered pixel width happened to fall near a fixed standard size, so a 360 px-wide phone reported a banner that a 393 px-wide phone (iPhone, Galaxy A54, Redmi Note 12) did not. The detector now recognises a unit by its height and by the fact that it spans the column, on any device. Separately, a full-screen ad frame layered over a page no longer hides the banners under it: the overlay and each banner are reported as their own ads. Expect slightly more discovered ads on 384–412 px phones and on pages that serve an interstitial. What still legitimately differs between devices is explained in Ad discovery across devices.

September 2026 — Your own click domains can be left out of detection

Campaigns and scans accept a new optional ignore_first_n_domains (0 to 5, default 0). It drops the first N unique domains of the redirect chain, counted from the entry point, from detection and tagging — the click and tracking domains your own direct links start on, which until now were rated like any other hop and produced tags and alerts about your infrastructure rather than about the offer.

Domains fold to their registrable form, so www.example.com and example.com consume one slot between them, and any other request to a skipped domain is excluded too. The default of 0 leaves detection byte-for-byte as it was, and the full redirect chain is still captured and returned either way — only what the checkers are given narrows.

Set it on a campaign and every scan of every run inherits it, discovered ads included; the value is frozen onto each scan at creation, so a re-check skips what the original scan skipped. See Campaigns and Creating scans.

September 2026 — Bank top-ups credit what arrived, and corrections are their own ledger entry

A confirmed bank transfer now credits the amount that actually landed on the account minus the invoice's VAT, instead of the amount the proforma billed. For a payment that matches the proforma nothing changes; an underpayment or a wire that arrived minus the bank fee now credits the sum received.

GET /api/v1/billing/history can therefore return a new type value, invoice_payment_adjustment: the signed difference posted when we correct what was recorded for an already-settled invoice. The ledger stays append-only — a correction never rewrites the original invoice_settlement row, so summing amount_micros over the history still reproduces your balance. If you branch on type, treat an unrecognised value as a balance motion of its signed amount rather than rejecting it. See Billing.

September 2026 — Ad Discovery names the ad network, and keeps its domain

network on a discovered ad is now the ad network's name — ExoClick, PropellerAds, TrafficStars — resolved from the ad server's domain, and it is empty when we cannot name the network. The domain itself moves to the new network_host field, which is populated whenever an ad server was attributable at all, so an unrecognised network shows you its domain rather than nothing.

Two kinds of value stop appearing as networks: infrastructure that is not an ad network (tag managers, analytics, measurement, DMPs) and the publisher's own creative CDN. Those ads are still found, still scanned and still billed exactly as before — only the label is withheld.

If you read network programmatically, expect names where you used to get hostnames, and read network_host for the domain. Scans created before this release are unchanged: they keep the hostname in network and have an empty network_host, so aggregate by network only over scans created after it. network and network_host appear on GET /api/v1/scans/{id}, the scan list, and the ad blocks of an ad-discovery report. See Scans.

September 2026 — Ad Discovery campaigns choose how many ads to check

max_discovered_ads (1 to 25) is now a field on ad-discovery campaigns and on POST /api/v1/scans with ad_discovery: true. It sets how many ad blocks we look for on a page before stopping; omit it and the platform default of 12 applies, exactly as before. A large publisher whose inventory the default was truncating can now cover it in one run.

Each ad found is scanned and billed as a separate check, so a page scanned at 25 can cost 26 checks. Values outside 1 to 25, or the field without ad_discovery, are rejected with 422 rather than clamped. Existing campaigns and scans are unchanged. See Scans API.

September 2026 — Default policy set for new campaigns

An organization can mark one owned policy set as the default. New campaigns that omit policy_set_id bind that set; JSON null still creates an unbound campaign that does not generate alerts. Existing campaigns are unchanged.

POST /api/v1/policy-sets/{id}/set-default with { "is_default": true } or false needs policies.manage and only works on a set you own. List and get responses include is_default; for a foreign public set the flag is always false for the viewer. See Custom policies.

September 2026 — Rules can match what a page's sub-resources contain

rule_type: "regexp_request_body" runs your pattern against the contents of the scripts, fetch / XHR responses and iframe documents a page loaded, rather than against their URLs. Same config as regexp_request_url (a non-empty pattern up to 4,096 characters, optional flags of "" or "i") and the same fixed target: "page". See Detection rules.

It exists because address-based matching has stopped being enough. The malvertising kits we see now rotate every filename on every visit, so a regexp_request_url pattern matches the names from the day it was written and nothing after. The code inside those files changes far more slowly than their names.

Two limits worth knowing before you write one:

  • Contents are captured for the scripts, fetch / XHR responses and iframe documents only — never images, video, fonts or stylesheets — up to 400 resources, 128 KB each and 8 MB per scan. A resource over a limit is recorded by URL, but its content is not searched.
  • Captured contents live for one day. Testing a body rule against an older scan returns a non-match because there is nothing left to read, so test against a recent scan.

August 2026 — Publishing a policy set is now reversible

POST /api/v1/policy-sets/{id}/unpublish returns a policy set your organization owns to private. It is the inverse of request-approval and covers both directions the old contract was missing: withdrawing a publication request that is still awaiting moderation, and taking an already-approved set back out of the shared catalog. It needs policies.manage, answers 204, and is idempotent — calling it on a set that is already private is a no-op rather than an error. See Custom policies.

Until now the only way out of the public catalog was to ask an administrator to reject the set, and that worked only while it was still pending.

Unpublishing does not detach campaigns. Campaigns in other organizations that were already bound to the set keep running against it; going private blocks new attachments instead.

August 2026 — Proxy targeting is discoverable, and its errors are machine-readable

The values accepted in a scan's proxy block now have a documented endpoint. GET /api/v1/proxy/targeting?country_code=US returns the regions, cities, and ISPs we accept for a country, ordered by pool size, with an ETag for cheap revalidation. Anything it lists is accepted by POST /api/v1/scans. See Proxy targeting.

Previously the only way to learn the accepted set was to guess, because the values come from our upstream network provider and cannot be a fixed enum in the schema.

Two things to know when you integrate:

  • Pass proxy_type explicitly if you scan on mobile. Residential and mobile are separate pools with separate catalogues — in the US that is 1500+ ISPs against roughly a dozen carriers.
  • refreshed_at tells you when we last checked with the provider and moves on every check. Use the ETag to detect actual changes.

Behaviour change for integrators. When a scan is rejected because a field's value is not one we accept, detail is now the array of {loc, msg, type, ctx} objects our OpenAPI schema has always declared for 422, instead of a single string. This covers proxy.region, proxy.city, proxy.isp, country_code and emulator_id. You can now tell which field was refused and retry without just that one. The top-level code field is unchanged, and errors that are not about a specific field — quota exhausted, campaign archived — still return detail as a string.

We also added 53 countries that our network covers but our country list was missing, mostly small territories, plus Turkmenistan. Countries the provider has stopped serving no longer appear in the country list.

August 2026 — An archived tag is retired everywhere

Archiving a tag now takes it out of service across the whole platform instead of only hiding it from the tag catalogue and the pickers. A retired tag is no longer assigned to new scans, no longer raises alerts, and no longer appears on scan detail, in scan or alert filters, in public reports, or in webhook payloads. Combo rules that use it as an input can no longer fire, and the rule editor's warning says so.

Your own archived custom tags still appear under Archived in the tag catalogue so you can restore them. Restoring returns the tag to service immediately, but does not retroactively re-tag the scans processed while it was archived.

For integrators:

  • GET /api/v1/tag-definitions?include_archived=true now returns only the archived tags your organization owns. Archived platform tags are never returned, and GET /api/v1/tag-definitions/{slug} returns 404 for one.
  • GET /api/v1/scans/{scan_id}/tags omits tags whose definition is archived, so a scan's tag list can shrink without the scan changing. The assignment is not deleted — un-archiving brings it back.
  • GET /api/v1/alerts omits alerts whose tag is archived, and total matches. Alerts on a tag you merely cannot see are still listed with the tag identity redacted, as before.
  • Creating or updating a policy set silently drops rules that point at an archived tag your organization can use, instead of rejecting the whole request. A slug that does not exist — or that belongs to another organization — still returns 422 with Unknown tag(s).

August 2026 — Campaign alert routing takes a fixed set of modes

The mode field on per-campaign alert routing is now a declared enumeration rather than a free-form string. It has always accepted exactly three values — inherit, override, silence — but they lived in prose, so a client could send something else and only learn it was wrong from a 422 at runtime.

For integrators:

  • PUT /api/v1/alert-notifications/campaigns/{campaign_id}/overrides and its GET twin now publish the three values in the OpenAPI schema, so generated clients constrain the field at compile time.
  • Accepted values are unchanged. If you already send inherit, override or silence, nothing changes. Anything else still returns 422, now as a standard schema-validation error listing the allowed values instead of the notifications.invalid_override_mode code.
  • destination_ids continues to be accepted only when mode is override; sending it with any other mode returns 422 with notifications.invalid_override_combination. Note that override with an empty list routes nowhere, which is effectively silence.

The contact and Request-a-Demo forms moved from /api/v1/contact and /api/v1/demo-inquiries to /api/forms/contact and /api/forms/demo-inquiries, and no longer appear in the Swagger document. They were never part of the REST product surface — they are anonymous intake endpoints for our own marketing site, and listing them alongside the API invited the assumption that integrators depend on them. The old paths still work for now and will be removed in a later release; if you somehow depend on them, switch to /api/forms/*.

August 2026 — Regex rules for network request URLs

Detection rules can now match a regular expression against every captured network and subresource request URL. Existing URL stopword, regex, and domain rules still inspect only the main-frame redirect chain, so their results do not change.

Kaminari Ad includes a public high-severity iOS Browser Exploit Attempt tag backed by an editable global rule for known exploit-kit request signatures.

For integrators:

  • Use rule_type: "regexp_request_url", target: "page", and config: {"pattern": "...", "flags": "i"} when creating or testing a request-URL rule. pattern must be non-empty and no longer than 4,096 characters. flags may be omitted or set to "" / "i"; unsupported keys, flags, and invalid expressions return 422.
  • Fresh scans evaluate up to 5,000 captured request URLs. Rule tests and rechecks reconstruct the persisted initiator tree, which contains the main-frame hops and up to 200 subrequests with selected resource types omitted, so historical evaluation is best-effort.
  • Regex rules for one scan surface now share a single evaluation budget, and platform-wide rules are evaluated first. If the budget runs out, your own rules are the ones skipped for that scan. The budget is sized well above a healthy full pass, so this is a guard against a runaway expression rather than a routine limit — keep patterns anchored and avoid nested quantifiers over overlapping character classes.

August 2026 — Alert filters, policy set column, and counts that agree

The alerts inbox filters by date range, policy set, tag, and country on top of campaign, and the table shows which policy set raised each alert. The four status cards are now the status filter — click one to narrow the list.

The cards used to count the last 30 days org-wide while the table counted all time with your filters applied, so the two disagreed. Both now read the same filters, so the four cards add up to the total under the table, and Select all acts on exactly that set rather than a wider one.

For integrators:

  • GET /api/v1/alerts accepts policy_set_id, tag, country_code (each comma-separated, matching any value), plus date_from, date_to and timezone. Dates are whole calendar days, both ends inclusive, interpreted in timezone (IANA name, UTC by default) and applied to the alert's creation date. Each bound is independent: omitting both lists all time, date_from alone means everything since that day, and date_to alone means everything up to it.
  • GET /api/v1/alerts/stats accepts the same filters except status, which it buckets by. With identical filters its four counts sum to the total from GET /api/v1/alerts.
  • Changed default: GET /api/v1/alerts/stats without date_from / date_to now counts all time, matching the list endpoint. It previously counted only the last 30 days, which is what stopped the counts from adding up. If you relied on the old window, pass the dates explicitly: ?date_from=<30 days ago>&date_to=<today>.
  • AlertResponse carries a new nullable policy_set_name; null when the alert predates policy-set attribution or the set was deleted.
  • POST /api/v1/alerts/bulk-status accepts the matching filter_policy_set_ids, filter_tag_slugs, filter_country_codes, filter_date_from, filter_date_to and filter_timezone alongside all_matching. As before, filter_* is rejected together with an explicit ids list. filter_timezone is required whenever either date bound is given — defaulting it would move the window against the list the selection was made from. The three collection filters reject an empty list rather than reading it as "no filter", since here that would widen what changes rather than what you look at.
  • A date_from later than date_to, or a closed range over 366 days, is rejected with 422 on both endpoints. A one-sided range carries no span cap; it is already bounded by all time.

August 2026 — Restart many campaigns in one go

Campaign and group lists have row checkboxes and a Run selected action. Select a page, or everything behind the current filters, and confirm against a preview of how many campaigns and scans the restart creates. Each selected campaign gets an ordinary new run, so quota and queueing behave exactly as they do for the per-row Run button.

Archived campaigns cannot run, so they are excluded from both the preview and the restart, and the archived list offers no selection.

Bulk run is a dashboard feature; there is no /api/v1 equivalent. API clients loop over POST /api/v1/campaigns/{id}/run, which is the same underlying operation.

August 2026 — Search and date filters for campaigns

The campaigns hub and the group page filter by name and by Created / Last Run date range, on top of the existing archived toggle and group selector. Groups now show a Last Run column — the newest run across the group's campaigns.

For integrators:

  • GET /api/v1/campaigns and GET /api/v1/campaign-groups accept q, created_from, created_to, last_run_from, last_run_to and timezone. Dates are whole calendar days, both ends inclusive, interpreted in timezone (IANA name, UTC by default).
  • "Last run" is the newest run of the campaign (for a group, of any of its campaigns), so setting either last-run bound excludes anything that has never run.
  • CampaignGroupResponse carries a new nullable last_run_at; null means no campaign in the group has ever run. CampaignResponse already had the field and now reports the same aggregate everywhere — list, filter and GET /api/v1/campaigns/{id} no longer disagree.
  • A *_from later than its *_to is rejected with 422 (created or last_run named in the message) instead of returning an empty page.
  • All parameters are optional; omitting them keeps the previous behaviour.

August 2026 — See a capture before the scan finishes

New webhook event scanning.screenshot.captured, emitted the moment the crawl stores a screenshot — minutes before scanning.scan.completed, which waits for classification. Subscribe to it when you show captures to moderators.

For integrators:

  • Subscribable like any other event (GET /api/v1/webhooks/event-types), same signed envelope, same retry policy.
  • The payload carries screenshot_url, creative_screenshot_url, page_title, report_url, and public_report_url, so no follow-up GET /api/v1/scans/{scan_id} is needed.
  • Emitted in the transaction that ends the crawl, so it is always queued before scanning.scan.completed for the same scan — see Webhooks.

August 2026 — Webhooks survive an outage

Webhook delivery no longer discards an event on the first unfriendly response, and an endpoint no longer switches itself off within seconds of a blip.

For integrators:

  • Every non-2xx response is retried now, 4xx included. A 404 from a load balancer mid-reconfiguration used to drop the event after a single attempt.
  • Retries continue for 24 hours from the moment the event occurred, with backoff growing to 1 hour. While an endpoint is down the events keep buffering and one 2xx drains the whole backlog automatically.
  • An endpoint is switched off only after 24 hours without a single 2xx. Everyone in your org who can manage webhooks gets a warning email after the first hour and a second email if it is switched off.
  • Undelivered events stay replayable for 7 days after they occurred, then they are deleted.
  • Endpoint responses carry two new health fields, failing_since and paused_until — see Webhooks.

July 2026 — Check from a publisher page (referrer)

Scans and campaigns accept an optional referrer: the publisher page a check is performed from. For ad_tag and vast_tag the browser commits its document on that URL, so the creative is embedded exactly as it would be in a real placement; for url and ad_discovery it travels as the Referer of the page request.

For integrators:

  • referrer is additive and optional on POST /api/v1/scans, POST /api/v1/scans/bulk, and the campaign create/update bodies, and it is returned on the scan and campaign responses.
  • It is rejected with 422 when it carries credentials, points at a host that is not publicly routable, or exceeds 2048 characters once normalised — see Creating scans.
  • Scans submitted directly to POST /api/v1/scans do not inherit a campaign's referrer; only scans created by a campaign run do.

July 2026 — Combo rules gained a match scope

Combo rules gained a Match scope setting. Conditions are still counted across the whole scan by default; the new per-link scope requires them all to hold on one URL, so the aggregate tag only fires for — and is attached to — a link that satisfies the rule on its own. The setting is opt-in per rule: existing rules keep evaluating exactly as before. A rule built only from link-less tags (AI, behaviour) has no link to attach to and should stay on the whole scan.

July 2026 — Repeats and retries

Campaigns and scans can now be repeated and retried.

Repeats scan the same URL, geo, and device up to 20 times per run, either in a fresh browser with a new IP each time or in one shared browser that carries cookies across the repeats — the second mode is how you reach a creative that only misbehaves on a returning visitor. Repeats are separate scans and bill as such; the campaign form estimates the run size before you save.

Retries re-crawl a scan that failed for a technical reason (dead proxy, timeout, browser crash) up to 5 more times on the same scan record, so they cost nothing extra.

Available in the dashboard, on POST /api/v1/scans, /scans/bulk and campaign create/update, and through the MCP tools. See Campaigns and the Scans API.

June 2026 — Documentation site launches

The Kaminari Ad documentation site is live at kaminari.ad/docs — a unified help center and developer reference covering the dashboard, the REST API (app.kaminari.ad/api/v1), webhooks, OAuth 2.0, and the MCP server, with a screen-by-screen UI Reference.

Future platform changes will be posted here as dated entries.

← All documentation