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/XHRresponses 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_typeexplicitly 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_attells you when we last checked with the provider and moves on every check. Use theETagto 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=truenow returns only the archived tags your organization owns. Archived platform tags are never returned, andGET /api/v1/tag-definitions/{slug}returns 404 for one.GET /api/v1/scans/{scan_id}/tagsomits 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/alertsomits alerts whose tag is archived, andtotalmatches. 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}/overridesand itsGETtwin 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,overrideorsilence, nothing changes. Anything else still returns 422, now as a standard schema-validation error listing the allowed values instead of thenotifications.invalid_override_modecode. destination_idscontinues to be accepted only whenmodeisoverride; sending it with any other mode returns 422 withnotifications.invalid_override_combination. Note thatoverridewith an empty list routes nowhere, which is effectivelysilence.
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", andconfig: {"pattern": "...", "flags": "i"}when creating or testing a request-URL rule.patternmust be non-empty and no longer than 4,096 characters.flagsmay 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/alertsacceptspolicy_set_id,tag,country_code(each comma-separated, matching any value), plusdate_from,date_toandtimezone. Dates are whole calendar days, both ends inclusive, interpreted intimezone(IANA name, UTC by default) and applied to the alert's creation date. Each bound is independent: omitting both lists all time,date_fromalone means everything since that day, anddate_toalone means everything up to it.GET /api/v1/alerts/statsaccepts the same filters exceptstatus, which it buckets by. With identical filters its four counts sum to thetotalfromGET /api/v1/alerts.- Changed default:
GET /api/v1/alerts/statswithoutdate_from/date_tonow 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>. AlertResponsecarries a new nullablepolicy_set_name;nullwhen the alert predates policy-set attribution or the set was deleted.POST /api/v1/alerts/bulk-statusaccepts the matchingfilter_policy_set_ids,filter_tag_slugs,filter_country_codes,filter_date_from,filter_date_toandfilter_timezonealongsideall_matching. As before,filter_*is rejected together with an explicitidslist.filter_timezoneis 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_fromlater thandate_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/campaignsandGET /api/v1/campaign-groupsacceptq,created_from,created_to,last_run_from,last_run_toandtimezone. Dates are whole calendar days, both ends inclusive, interpreted intimezone(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.
CampaignGroupResponsecarries a new nullablelast_run_at;nullmeans no campaign in the group has ever run.CampaignResponsealready had the field and now reports the same aggregate everywhere — list, filter andGET /api/v1/campaigns/{id}no longer disagree.- A
*_fromlater than its*_tois rejected with 422 (createdorlast_runnamed 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, andpublic_report_url, so no follow-upGET /api/v1/scans/{scan_id}is needed. - Emitted in the transaction that ends the crawl, so it is always queued
before
scanning.scan.completedfor 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-
2xxresponse is retried now,4xxincluded. A404from 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
2xxdrains 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_sinceandpaused_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:
referreris additive and optional onPOST /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
422when 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/scansdo 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.