# Campaign filters (/guides/campaign-filters)

<Callout type="info">

Create/update returns `422` if a value does not match the format or is not in the API allowlist. Do not guess country names or free-form locale strings.

</Callout>

## Allowed values [#allowed-values]

| Field | Type | Max items | Format |
|-------|------|-----------|--------|
| `filters.geolocation.countries` | `string[]` | 256 | ISO 3166-1 alpha-2, **uppercase** |
| `filters.browser_language.languages` | `string[]` | 255 | IETF language tag, **all lowercase** (`xx` or `xx-yy`) |
| `filters.state.states` | `string[]` | 100 | Place name (see below) |
| `filters.city.cities` | `string[]` | 100 | Place name (see below) |
| `pages.offers[].segmentation.country` | `string[]` | 256 | Same as `filters.geolocation.countries` |
| `filters.device.devices` | `string[]` | — | `desktop`, `smartphone`, `tablet`, `unknown` |
| `filters.os.os` | `string[]` | — | Lowercase detector name (see [OS](#os-filtersos)) |
| `filters.browser.browsers` | `string[]` | — | Lowercase detector name (see [Browser](#browser-filtersbrowser)) |
| `filters.isp.isps` | `string[]` | — | Substring of the GeoIP ISP name |
| `filters.blacklist` / `filters.whitelist` | `string[]` | — | Exact IP string |

### Country codes [#country-codes]

Used in `filters.geolocation.countries` and `pages.offers[].segmentation.country`.

| Rule | Value |
|------|-------|
| Format | Exactly 2 uppercase letters ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)) |
| Examples | `"BR"`, `"US"`, `"PT"` |
| Invalid | `"br"`, `"Brazil"`, `"BRA"`, `"brasil"` |

### Browser language codes [#browser-language-codes]

Used in `filters.browser_language.languages`.

| Rule | Value |
|------|-------|
| Format | Lowercase [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag): `xx` or `xx-yy` (hyphen, not underscore) |
| Examples | `"pt-br"`, `"en"`, `"es"` |
| Invalid | `"pt-BR"`, `"portuguese"`, `"pt_BR"` |

### State and city names [#state-and-city-names]

Used in `filters.state.states` and `filters.city.cities`.

| Rule | Value |
|------|-------|
| Length | 1–100 characters (trimmed) |
| Allowed chars | Unicode letters, spaces, `'`, `-`, `.` |
| Must start with | A letter |
| Examples | `"São Paulo"`, `"California"`, `"St. Louis"` |
| Invalid | `"123"`, `""`, `"City@Name"` |

Free-text GeoIP place names — not ISO codes.

## URL query params (`filters.query`) [#url-query-params-filtersquery]

Cloakup matches the query string after `?` in the visitor URL against `filters.query`.

You always send **two objects with the same keys**:

| Object | What it stores |
|--------|----------------|
| `params` | Param **name → values** to match (always a `string[]`, never a plain string) |
| `rules` | Param **name → how to compare** (`equals`, `contains`, `starts_with`, `ends_with`) |

`"asiudhasudhasd": "equals"` does **not** set the value. It only says: compare the URL param named `asiudhasudhasd` with **exact match**. The value lives in `params` as an array: `"asiudhasudhasd": ["123u1basj"]`.

Visitor URL:

```text
https://track.example.com/slug?asiudhasudhasd=123u1basj
```

Matching config:

```json
"query": {
  "allow": true,
  "remove_params": false,
  "condition": "some",
  "params": {
    "asiudhasudhasd": ["123u1basj"]
  },
  "rules": {
    "asiudhasudhasd": "equals"
  }
}
```

Every key in `params` needs the same key in `rules`. Empty `params: {}` and `rules: {}` means **no URL param filtering**.

| Field | Type | Description |
|-------|------|-------------|
| `params` | `Record<string, string[]>` | Param name → list of values in the URL |
| `rules` | `Record<string, string>` | Per-param comparison. Not the value itself |
| `condition` | `"some"` \| `"every"` | `some` = at least one param must match; `every` = all must match |
| `allow` | `boolean` | `true` = allow when params match; `false` = block when params match |
| `remove_params` | `boolean` | Strip matched params from the URL before redirect |

### Match rules [#match-rules]

The value in `rules` is the operator. The value in `params` is what you compare against.

| Rule | Meaning | `params` value | Matches | Does not match |
|------|---------|----------------|---------|----------------|
| `equals` | Exact value | `["google"]` | `?utm_source=google` | `?utm_source=Google`, `?utm_source=googleads` |
| `contains` | Substring anywhere | `["google"]` | `?utm_source=googleads` | `?utm_source=facebook` |
| `starts_with` | Prefix | `["Cjw"]` | `?gclid=CjwKEAjabc` | `?gclid=xxCjw` |
| `ends_with` | Suffix | `["-br"]` | `?utm_campaign=promo-br` | `?utm_campaign=br-promo` |

### Wildcard (`*`) [#wildcard-]

`"*"` is a special **value** in `params` (not a rule, not a glob). If that key is present in the URL, the checker accepts **any** value and skips `equals` / `contains` / `starts_with` / `ends_with`.

| URL | Config | Result |
|-----|--------|--------|
| `?fbclid=IwAR123abc` | `"fbclid": ["*"]` | Match — key exists |
| `?fbclid=` | `"fbclid": ["*"]` | Match — key exists, even empty |
| `?utm_source=google` | `"fbclid": ["*"]` | No match — `fbclid` missing |
| `?utm_source=googleads` | `"utm_source": ["goo*"]` | No match — `"goo*"` is a literal string, not a pattern |

You still send a `rules` entry for that key (use `"equals"`). The operator is ignored when `"*"` is in the values list.

```json
"params": { "fbclid": ["*"] },
"rules": { "fbclid": "equals" }
```

Do not mix `"*"` with other values on the same key (`["*", "google"]`). `"*"` already matches any value, so the extra entries never run.

### Examples [#examples]

<Accordions type="single" collapsible defaultValue="custom-token-equals">
<Accordion title="Custom token (equals)" id="custom-token-equals">

URL: `?asiudhasudhasd=123u1basj`

    The visitor must send this exact token. Common for a secret query key you append to the ad URL.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "asiudhasudhasd": ["123u1basj"]
      },
      "rules": {
        "asiudhasudhasd": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="utm_source = google" id="utm-source-google">

URL: `?utm_source=google`

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "utm_source": ["google"]
      },
      "rules": {
        "utm_source": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="Several params together" id="several-params-together">

URL: `?utm_source=google&asiudhasudhasd=123u1basj&utm_campaign=black-friday`

    `condition: "some"` — at least **one** of these params must match.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "utm_source": ["google"],
        "asiudhasudhasd": ["123u1basj"],
        "utm_campaign": ["black-friday"]
      },
      "rules": {
        "utm_source": "equals",
        "asiudhasudhasd": "equals",
        "utm_campaign": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="All params must match (every)" id="all-params-must-match-every">

Same keys as above, but the visitor must match **all** of them.

    URL that matches: `?utm_source=google&asiudhasudhasd=123u1basj`

    URL that does **not** match: `?utm_source=google` (missing token)

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "every",
      "params": {
        "utm_source": ["google"],
        "asiudhasudhasd": ["123u1basj"]
      },
      "rules": {
        "utm_source": "equals",
        "asiudhasudhasd": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="One param, several allowed values" id="one-param-several-allowed-values">

URL: `?utm_source=google` **or** `?utm_source=facebook`

    Values are always an array. Do not send `"utm_source": "google"`.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "utm_source": ["google", "facebook"]
      },
      "rules": {
        "utm_source": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="contains" id="contains">

URL: `?utm_content=summer-promo-br` matches because the value **contains** `promo`.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "utm_content": ["promo"]
      },
      "rules": {
        "utm_content": "contains"
      }
    }
    ```

</Accordion>
<Accordion title="starts_with" id="starts-with">

URL: `?gclid=CjwKEAjabc123` matches because the value **starts with** `Cjw`.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "gclid": ["Cjw"]
      },
      "rules": {
        "gclid": "starts_with"
      }
    }
    ```

</Accordion>
<Accordion title="ends_with" id="ends-with">

URL: `?utm_campaign=sales-br` matches because the value **ends with** `-br`.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "utm_campaign": ["-br"]
      },
      "rules": {
        "utm_campaign": "ends_with"
      }
    }
    ```

</Accordion>
<Accordion title="Wildcard — key must exist, any value" id="wildcard-key-must-exist-any-value">

URL that matches: `?fbclid=IwAR123abc` (any value)

    URL that matches: `?fbclid=` (empty value still has the key)

    URL that does **not** match: `?utm_source=google` (`fbclid` missing)

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "fbclid": ["*"]
      },
      "rules": {
        "fbclid": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="Wildcard on a custom token" id="wildcard-on-a-custom-token">

URL that matches: `?asiudhasudhasd=anything`

    URL that does **not** match: no `asiudhasudhasd` in the query

    Use this when you only care that the secret param **name** is present, not the value.

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "some",
      "params": {
        "asiudhasudhasd": ["*"]
      },
      "rules": {
        "asiudhasudhasd": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="Wildcard + exact param (every)" id="wildcard-exact-param-every">

Visitor must have `fbclid` (any value) **and** `utm_source=google`.

    URL that matches: `?fbclid=IwAR123&utm_source=google`

    URL that does **not** match: `?utm_source=google` (missing `fbclid`)

    URL that does **not** match: `?fbclid=IwAR123&utm_source=facebook`

    ```json
    "query": {
      "allow": true,
      "remove_params": false,
      "condition": "every",
      "params": {
        "fbclid": ["*"],
        "utm_source": ["google"]
      },
      "rules": {
        "fbclid": "equals",
        "utm_source": "equals"
      }
    }
    ```

</Accordion>
<Accordion title="Strip params after match" id="strip-params-after-match">

Match `utm_source=google`, then remove it from the URL before redirect so the destination does not see it.

    ```json
    "query": {
      "allow": true,
      "remove_params": true,
      "condition": "some",
      "params": {
        "utm_source": ["google"]
      },
      "rules": {
        "utm_source": "equals"
      }
    }
    ```

</Accordion>
</Accordions>

<Callout type="success">

Send `"utm_source": ["google"]`, never `"utm_source": "google"`. A plain string returns `422`.

</Callout>

## Referer (`filters.referer`) [#referer-filtersreferer]

`filters.referer.domains` is a list of hostnames. Send the hostname only (no `https://`, no path). `www.` is stripped at match time.

`*` in a referer domain matches **that subdomain slot**. Use `*.facebook.com` to allow every subdomain of `facebook.com`.

| Config | Matches | Does not match |
|--------|---------|----------------|
| `"facebook.com"` | `https://facebook.com/foo`, `https://www.facebook.com` (`www.` is stripped) | `https://m.facebook.com` |
| `"*.facebook.com"` | `https://m.facebook.com`, `https://l.facebook.com` | `https://facebook.com` (no subdomain) |

`*.facebook.com` does **not** match the apex host `facebook.com`. Add both entries if you need apex + subdomains.

`block_null: true` blocks visitors with no referer (direct access).

```json
"referer": {
  "allow": true,
  "block_null": true,
  "domains": ["facebook.com", "*.facebook.com", "instagram.com"]
}
```

<Accordions type="single" collapsible>
<Accordion title="Exact referer host" id="exact-referer-host">

Allow traffic that came from `facebook.com` only (not `m.facebook.com`).

    ```json
    "referer": {
      "allow": true,
      "block_null": true,
      "domains": ["facebook.com"]
    }
    ```

</Accordion>
<Accordion title="All subdomains (wildcard)" id="all-subdomains-wildcard">

Allow `m.facebook.com`, `l.facebook.com`, `lm.facebook.com`, and any other subdomain.

    Does **not** match `facebook.com` itself. Add both if you need apex + subdomains.

    ```json
    "referer": {
      "allow": true,
      "block_null": true,
      "domains": ["facebook.com", "*.facebook.com"]
    }
    ```

</Accordion>
<Accordion title="Several sources" id="several-sources">

```json
    "referer": {
      "allow": true,
      "block_null": true,
      "domains": [
        "facebook.com",
        "*.facebook.com",
        "instagram.com",
        "*.instagram.com",
        "google.com"
      ]
    }
    ```

</Accordion>
</Accordions>

## Device (`filters.device`) [#device-filtersdevice]

Empty `devices: []` means no device filtering.

`allow: true` = only listed devices pass. `allow: false` = listed devices are blocked.

| Value | What it matches |
|-------|-----------------|
| `smartphone` | Phones. Phablet is treated as smartphone |
| `tablet` | Tablets |
| `desktop` | Desktops and laptops |
| `unknown` | Detector could not classify the device |

```json
"device": {
  "allow": true,
  "devices": ["smartphone", "tablet"]
}
```

<Accordions type="single" collapsible>
<Accordion title="Mobile only" id="mobile-only">

Desktop visitors are blocked.

    ```json
    "device": {
      "allow": true,
      "devices": ["smartphone"]
    }
    ```

</Accordion>
<Accordion title="Block desktop" id="block-desktop">

```json
    "device": {
      "allow": false,
      "devices": ["desktop"]
    }
    ```

</Accordion>
<Accordion title="All classified devices" id="all-classified-devices">

```json
    "device": {
      "allow": true,
      "devices": ["smartphone", "tablet", "desktop"]
    }
    ```

</Accordion>
</Accordions>

## OS (`filters.os`) [#os-filtersos]

Empty `os: []` means no OS filtering. Names are **lowercase** as the device detector reports them.

| Value | Matches |
|-------|---------|
| `android` | Android |
| `ios` | iOS |
| `windows` | Windows |
| `mac` | macOS |
| `gnu/linux` | Generic Linux desktop. Send this, not `"linux"` |
| `ubuntu` | Ubuntu |
| `unknown` | Detector could not classify the OS |
| `other` | Any OS **not** in `windows`, `mac`, `gnu/linux`, `android`, `ios`, `ubuntu` |

```json
"os": {
  "allow": true,
  "os": ["android", "ios"]
}
```

<Accordions type="single" collapsible>
<Accordion title="Android and iOS" id="android-and-ios">

```json
    "os": {
      "allow": true,
      "os": ["android", "ios"]
    }
    ```

</Accordion>
<Accordion title="Desktop OSes" id="desktop-oses">

```json
    "os": {
      "allow": true,
      "os": ["windows", "mac", "gnu/linux", "ubuntu"]
    }
    ```

</Accordion>
<Accordion title="Catch-all with other" id="catch-all-with-other">

Include `"other"` to also match uncommon OS names (Chrome OS, Tizen, and so on).

    ```json
    "os": {
      "allow": true,
      "os": ["android", "ios", "windows", "mac", "other"]
    }
    ```

</Accordion>
</Accordions>

## Browser (`filters.browser`) [#browser-filtersbrowser]

Empty `browsers: []` means no browser filtering. Match is **case-insensitive exact** on the detector name.

Common values: `chrome`, `chrome mobile`, `chrome mobile ios`, `chrome webview`, `safari`, `mobile safari`, `firefox`, `microsoft edge`, `opera`, `samsung browser`, `miui browser`, `android browser`, `internet explorer`, `facebook`, `instagram app`, `whatsapp`, `baidu spark`.

```json
"browser": {
  "allow": true,
  "browsers": ["chrome", "chrome mobile", "safari", "mobile safari"]
}
```

<Accordions type="single" collapsible>
<Accordion title="Chrome and Safari (desktop + mobile)" id="chrome-and-safari-desktop-mobile">

```json
    "browser": {
      "allow": true,
      "browsers": [
        "chrome",
        "chrome mobile",
        "chrome mobile ios",
        "safari",
        "mobile safari"
      ]
    }
    ```

</Accordion>
<Accordion title="Block in-app browsers" id="block-in-app-browsers">

```json
    "browser": {
      "allow": false,
      "browsers": ["facebook", "instagram app", "whatsapp"]
    }
    ```

</Accordion>
<Accordion title="Firefox only" id="firefox-only">

```json
    "browser": {
      "allow": true,
      "browsers": ["firefox"]
    }
    ```

</Accordion>
</Accordions>

## ISP (`filters.isp`) [#isp-filtersisp]

Empty `isps: []` means no ISP filtering. Match is a **case-insensitive substring** of the GeoIP ISP string — `"claro"` matches `Claro S.A.`.

Use names as they appear in GeoIP (carriers, not brand slogans).

```json
"isp": {
  "allow": true,
  "isps": ["Claro", "Vivo", "Oi", "TIM"]
}
```

<Accordions type="single" collapsible>
<Accordion title="Allow Brazilian carriers" id="allow-brazilian-carriers">

```json
    "isp": {
      "allow": true,
      "isps": ["Claro", "Vivo", "Oi", "TIM"]
    }
    ```

</Accordion>
<Accordion title="Block datacenter / hosting ISPs" id="block-datacenter-hosting-isps">

```json
    "isp": {
      "allow": false,
      "isps": ["Amazon", "Google", "DigitalOcean", "OVH", "Hetzner"]
    }
    ```

</Accordion>
<Accordion title="Single ISP" id="single-isp">

```json
    "isp": {
      "allow": true,
      "isps": ["Comcast"]
    }
    ```

</Accordion>
</Accordions>

## IP lists (`filters.blacklist` / `filters.whitelist`) [#ip-lists-filtersblacklist--filterswhitelist]

Both are arrays of **exact IP strings** (no CIDR). Empty `[]` means no IP list.

| Field | Effect |
|-------|--------|
| `blacklist` | That IP is blocked |
| `whitelist` | That IP **always passes**, including when other filters would deny. `GET /v1/campaigns/:id` returns `is_test_mode: true` if your IP is in this list |

```json
"blacklist": ["203.0.113.10", "198.51.100.22"],
"whitelist": ["192.0.2.80"]
```

<Accordions type="single" collapsible>
<Accordion title="Block specific IPs" id="block-specific-ips">

```json
    "blacklist": ["203.0.113.10", "198.51.100.22"]
    ```

</Accordion>
<Accordion title="Test from your own IP" id="test-from-your-own-ip">

Add your IP to `whitelist` so you always see the offer while testing.

    ```json
    "whitelist": ["192.0.2.80"]
    ```

</Accordion>
<Accordion title="Both lists" id="both-lists">

`192.0.2.80` still passes because whitelist wins.

    ```json
    "blacklist": ["203.0.113.10"],
    "whitelist": ["192.0.2.80"]
    ```

</Accordion>
</Accordions>

## Required filter keys [#required-filter-keys]

On create/update, these top-level keys under `filters` **must** be present:

`geolocation`, `referer`, `query`, `domain`, `browser`, `browser_language`, `isp`, `os`, `user_agent`, `blacklist`

Optional keys (defaults apply): ad-network flags, `deny_always`, `bots`, `proxy`, `adspy`, `cloakup_ai`, `state`, `city`, `network_type`, `device`, `whitelist`.

Full JSON Schema: [Schemas → CampaignFilters](/reference/schemas#campaignfilters).
