> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.cloakup.me/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Como usar a API

# Referência de API Key — Cloakup

## Visão geral

- **URL base:** `https://api.cloakup.me` (sem barra no final). Env: `CLOAKUP_API_URL`.
- **Caminho base da API pública:** `/v1`
- **Formato da API key:** `ck_` + 64 caracteres hex (ex.: `ck_a1b2c3...`)
- **Header de autenticação:** `Authorization: Bearer ck_<secret>`
- **Content-Type:** `application/json`
- **IDs:** numéricos (`bigint` serializado como número ou string JSON, dependendo do serializador)
- **Enums de filtros de campanha:** regras em [Valores permitidos nos filtros de campanha](#valores-permitidos-nos-filtros-de-campanha) (3 exemplos cada + links de formato)
- **Segurança em produção:** leia [Crítico: não publique campanhas em revisão](#crítico-não-publique-campanhas-em-revisão) antes de criar campanhas
- **Criação de campanha:** você **não** precisa de todos os campos — veja [Defaults na criação de campanha](#defaults-na-criação-de-campanha)

### Fluxo recomendado

1. Garanta que um domínio existe: `POST /v1/domains` → `PATCH /v1/domains/:id/verify` (até `verified: true`).
2. Crie a campanha com o [exemplo mínimo](#post-v1campaigns) — mantenha `"deny_always": false` (ou omita).
3. Confirme com `GET /v1/campaigns/:id` ou listagem (`in_review` deve ser `false` para produção).

### Anti-padrões (não faça)

- **Não** defina `filters.deny_always: true` a menos que o usuário queira explicitamente modo revisão/teste.
- **Não** copie o objeto `filters` completo de uma resposta GET para POST/PUT.
- **Não** invente códigos de país ou idiomas de navegador — siga os links de formato abaixo; case/nome errado → `422`.
- **Não** envie valores de `filters.query.params` como string simples — sempre `string[]` (ex.: `"utm_source": ["google"]`).
- **Não** coloque `http(s)://`, `www` ou `/` no `name` do domínio.
- **Não** trate PUT como PATCH parcial — envie um `CampaignBody` **completo**.
- **Não** use `https://api.example.com` — use `https://api.cloakup.me` (ou `CLOAKUP_API_URL`).

## Crítico: não publique campanhas em revisão

> **Para agentes de IA e scripts:** defaults errados aqui queimam verba de anúncios — todo visitante vê apenas a página segura.

| Campo na requisição | Campo na resposta de listagem | Onde na resposta show/create | Efeito quando `true` |
|---------------------|-------------------------------|------------------------------|----------------------|
| `filters.deny_always` | `in_review` | `filters.deny_always` (CampaignFull não tem `in_review` no topo) | **Sempre mostra a página segura (white).** Sem tráfego para a oferta. Use apenas durante testes. |

**Default para produção:** omita `deny_always` ou defina `"deny_always": false`.

**Nunca** copie `deny_always: true` dos exemplos a menos que o usuário peça explicitamente modo revisão/teste.

### Parâmetros de query na URL (`filters.query`)

O Cloakup compara parâmetros de query da URL com `filters.query.params`. Cada chave mapeia para um **array de valores permitidos** (não uma string simples).

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

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `params` | `Record<string, string[]>` | Nome do parâmetro → lista de valores permitidos na URL |
| `rules` | `Record<string, string>` | Regra de match por parâmetro: `equals`, `contains`, `starts_with`, `ends_with` |
| `condition` | `"some"` \| `"every"` | `some` = pelo menos um parâmetro deve bater; `every` = todos devem bater |
| `allow` | `boolean` | `true` = permite quando os params batem; `false` = bloqueia quando batem |
| `remove_params` | `boolean` | Remove os params correspondentes da URL antes do redirect |

**Curinga:** use `"*"` como valor para aceitar **qualquer** valor daquela chave:

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

Isso só verifica se `utm_source` existe na URL — o valor real não importa.

`params: {}` vazio significa **sem filtro por parâmetros de URL**.

## Pré-requisitos (toda requisição `/v1`)

Cadeia de middleware aplicada a todas as rotas `/v1`:

1. API key válida (`Authorization: Bearer ck_...`)
2. Rate limit: **120 requisições/minuto** por key
3. Plano deve ter **API keys habilitadas** (`plan.api_keys_enabled`)
4. Assinatura **cloakup** ativa
5. **Scope** específico da rota (veja cada endpoint)

## Scopes

| Scope | Descrição |
|-------|-----------|
| `campaigns:list` | Listar campanhas |
| `campaigns:create` | Criar campanha |
| `campaigns:show` | Obter campanha por ID |
| `campaigns:update` | Atualizar campanha |
| `campaigns:delete` | Excluir campanha |
| `campaigns:clone` | Clonar campanha |
| `campaigns:toggle` | Alternar status ativo da campanha |
| `domains:list` | Listar domínios |
| `domains:create` | Registrar domínio |
| `domains:verify` | Verificar DNS/certificado do domínio |
| `domains:delete` | Excluir domínio |

## Resposta de erro

Todos os erros da aplicação retornam JSON:

```json
{
  "message": "string",
  "action": "string",
  "code": "string",
  "data": {}
}
```

| HTTP | Significado |
|------|-------------|
| `401` | API key ausente/inválida/expirada/revogada |
| `403` | Scope ausente, API keys desabilitadas no plano ou assinatura inativa |
| `404` | Recurso não encontrado (ou não pertence ao usuário) |
| `422` | Erro de validação (`data` contém erros por campo) |
| `429` | Rate limit excedido |

---

## Schemas compartilhados

### Error

```json
{
  "$id": "Error",
  "type": "object",
  "properties": {
    "message": { "type": "string" },
    "action": { "type": "string" },
    "code": { "type": "string" },
    "data": { "type": "object", "additionalProperties": true }
  },
  "required": ["message", "action"]
}
```

### Valores permitidos nos filtros de campanha

> **Para agentes de IA:** create/update retorna `422` se um valor não bate com o formato (ou não está na allowlist da API). Não adivinhe *nomes* de país ou strings de locale livres.

| Campo | Tipo | Máx. itens | Formato |
|-------|------|------------|---------|
| `filters.geolocation.countries` | `string[]` | 256 | ISO 3166-1 alpha-2, **maiúsculas** |
| `filters.browser_language.languages` | `string[]` | 255 | Tag de idioma IETF, **tudo minúsculo** (`xx` ou `xx-yy`) |
| `filters.state.states` | `string[]` | 100 | Nome do lugar (veja abaixo) |
| `filters.city.cities` | `string[]` | 100 | Nome do lugar (veja abaixo) |
| `pages.offers[].segmentation.country` | `string[]` | 256 | Igual a `filters.geolocation.countries` |

#### Códigos de país

Usados em `filters.geolocation.countries` e `pages.offers[].segmentation.country`.

| Regra | Valor |
|-------|-------|
| Formato | Exatamente 2 letras maiúsculas ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)) |
| Lista completa | [Wikipedia — ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) |
| Exemplos | `"BR"`, `"US"`, `"PT"` |
| Inválido | `"br"`, `"Brazil"`, `"BRA"`, `"brasil"` |

#### Códigos de idioma do navegador

Usados em `filters.browser_language.languages`.

| Regra | Valor |
|-------|-------|
| Formato | [Tag de idioma IETF](https://en.wikipedia.org/wiki/IETF_language_tag) em minúsculas: `xx` ou `xx-yy` (hífen, não underscore; região também minúscula) |
| Lista / referência | [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) · [Códigos ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) |
| Exemplos | `"pt-br"`, `"en"`, `"es"` |
| Inválido | `"pt-BR"` (case), `"portuguese"`, `"pt_BR"` (underscore) |

#### Nomes de estado e cidade

Usados em `filters.state.states` e `filters.city.cities`.

| Regra | Valor |
|-------|-------|
| Tamanho | 1–100 caracteres (após trim) |
| Caracteres permitidos | Letras Unicode, espaços, `'` `-` `.` |
| Deve começar com | Uma letra |
| Exemplos | `"São Paulo"`, `"California"`, `"St. Louis"` |
| Inválido | `"123"`, `""`, `"City@Name"` |

Nomes de lugar GeoIP em texto livre (não códigos ISO).

### CampaignFilters

Usado no body de create/update de campanha.

> **Agentes:** chaves aninhadas com default podem ser omitidas ou enviadas como `{}` / objetos parciais. O JSON Schema abaixo lista **chaves de topo que devem estar presentes** no create/update. `allow` / `remove_params` / arrays vazios aninhados costumam ter default no servidor — veja [Defaults na criação de campanha](#defaults-na-criação-de-campanha) e o exemplo mínimo de POST. **Não** trate toda propriedade aninhada como obrigatória.

**Enviar obrigatoriamente (topo de `filters`):** `geolocation`, `referer`, `query`, `domain`, `browser`, `browser_language`, `isp`, `os`, `user_agent`, `blacklist`.

**Opcional (defaults aplicam):** flags de rede de anúncios, `deny_always`, `bots`, `proxy`, `adspy`, `cloakup_ai`, `state`, `city`, `network_type`, `device`, `whitelist`.

```json
{
  "$id": "CampaignFilters",
  "type": "object",
  "required": [
    "geolocation",
    "referer",
    "query",
    "domain",
    "browser",
    "browser_language",
    "isp",
    "os",
    "user_agent",
    "blacklist"
  ],
  "properties": {
    "facebook": { "type": "boolean", "default": false },
    "google": { "type": "boolean", "default": false },
    "tiktok": { "type": "boolean", "default": false },
    "kwai": { "type": "boolean", "default": false },
    "taboola": { "type": "boolean", "default": false },
    "pinterest": { "type": "boolean", "default": false },
    "yandex": { "type": "boolean", "default": false },
    "mgid": { "type": "boolean", "default": false },
    "outbrain": { "type": "boolean", "default": false },
    "sms": { "type": "boolean", "default": false },
    "revcontent": { "type": "boolean", "default": false },
    "adskeeper": { "type": "boolean", "default": false },
    "newsbreak": { "type": "boolean", "default": false },
    "adspy": { "type": "boolean", "default": true },
    "deny_always": { "type": "boolean", "default": false, "description": "PERIGO: true = modo in_review, sempre mostra página segura. Use false em produção." },
    "bots": { "type": "boolean", "default": true },
    "proxy": { "type": "boolean", "default": true },
    "cloakup_ai": { "type": "boolean", "default": true },
    "geolocation": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "countries": {
          "type": "array",
          "maxItems": 256,
          "items": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "ISO 3166-1 alpha-2 maiúsculo. Veja https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2",
            "examples": ["BR", "US", "PT"]
          }
        }
      }
    },
    "state": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean" },
        "states": {
          "type": "array",
          "maxItems": 100,
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "pattern": "^[\\p{L}\\p{M}][\\p{L}\\p{M}\\s'.-]{0,99}$",
            "description": "Nome do lugar (não código ISO). Letras, espaços, apóstrofo, hífen, ponto.",
            "examples": ["São Paulo", "California", "St. Louis"]
          }
        }
      }
    },
    "city": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean" },
        "cities": {
          "type": "array",
          "maxItems": 100,
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "pattern": "^[\\p{L}\\p{M}][\\p{L}\\p{M}\\s'.-]{0,99}$",
            "description": "Nome do lugar (não código ISO). Letras, espaços, apóstrofo, hífen, ponto.",
            "examples": ["Campinas", "New York", "O'Fallon"]
          }
        }
      }
    },
    "network_type": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean" },
        "types": {
          "type": "array",
          "items": { "enum": ["residential", "business", "wireless", "hosting", "unknown"] }
        }
      }
    },
    "device": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean" },
        "devices": {
          "type": "array",
          "items": { "enum": ["desktop", "smartphone", "tablet", "unknown"] }
        }
      }
    },
    "referer": {
      "type": "object",
      "required": ["block_null", "domains"],
      "properties": {
        "block_null": { "type": "boolean" },
        "allow": { "type": "boolean", "default": true },
        "domains": { "type": "array", "items": { "type": "string", "maxLength": 255 } }
      }
    },
    "query": {
      "type": "object",
      "required": ["params", "condition", "rules"],
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "remove_params": { "type": "boolean", "default": false },
        "params": {
          "type": "object",
          "description": "Record<string, string[]>. Nome do parâmetro → array de valores permitidos na URL.",
          "additionalProperties": {
            "type": "array",
            "items": { "type": "string" }
          }
        },
        "condition": { "enum": ["some", "every"] },
        "rules": {
          "type": "object",
          "additionalProperties": {
            "enum": ["equals", "contains", "starts_with", "ends_with"]
          }
        }
      }
    },
    "domain": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "domains": { "type": "array", "items": { "type": "string", "maxLength": 255 } }
      }
    },
    "browser": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "browsers": { "type": "array", "items": { "type": "string", "maxLength": 255 } }
      }
    },
    "browser_language": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "languages": {
          "type": "array",
          "maxItems": 255,
          "items": {
            "type": "string",
            "pattern": "^[a-z]{2}(-[a-z]{2})?$",
            "description": "Tag IETF, tudo minúsculo (xx ou xx-yy). Veja https://en.wikipedia.org/wiki/IETF_language_tag",
            "examples": ["pt-br", "en", "es"]
          }
        }
      }
    },
    "isp": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "isps": { "type": "array", "items": { "type": "string", "maxLength": 255 } }
      }
    },
    "os": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "os": { "type": "array", "items": { "type": "string", "maxLength": 255 } }
      }
    },
    "user_agent": {
      "type": "object",
      "properties": {
        "allow": { "type": "boolean", "default": true },
        "user_agents": { "type": "array", "items": { "type": "string", "maxLength": 255 } }
      }
    },
    "blacklist": { "type": "array", "items": { "type": "string", "description": "Endereço IP" } },
    "whitelist": { "type": "array", "items": { "type": "string", "description": "Endereço IP" } }
  }
}
```

### Defaults na criação de campanha

Campos omitidos são preenchidos no servidor (validação `campaign-schema`). **Não copie o objeto `filters` completo dos exemplos** — envie só o que precisar.

| Campo | Default quando omitido |
|-------|------------------------|
| `active` | `true` |
| `filters.facebook`, `google`, `tiktok`, `kwai`, `taboola`, `pinterest`, `yandex`, `mgid`, `outbrain`, `sms`, `revcontent`, `adskeeper`, `newsbreak` | `false` |
| `filters.adspy`, `bots`, `proxy`, `cloakup_ai` | `true` |
| `filters.deny_always` | `false` |
| `filters.state` | `{ "allow": true, "states": [] }` |
| `filters.city` | `{ "allow": true, "cities": [] }` |
| `filters.network_type` | `{ "allow": true, "types": [] }` |
| `filters.device` | `{ "allow": true, "devices": [] }` |
| `filters.geolocation.allow` | `true` |
| `filters.referer.allow` | `true` |
| `filters.query.allow` | `true` |
| `filters.query.remove_params` | `false` |
| `filters.domain.allow`, `browser.allow`, `browser_language.allow`, `isp.allow`, `os.allow`, `user_agent.allow` | `true` |

**Ainda obrigatórios** (sem default para a chave em si): `name`, `mode`, `pages`, `domain`, `alias`, e em `filters`: `geolocation`, `referer`, `query`, `domain`, `browser`, `browser_language`, `isp`, `os`, `user_agent`, `blacklist`.

Dentro dos objetos obrigatórios você pode omitir defaults aninhados (ex.: enviar `"domain": {}`, `"referer": { "block_null": true, "domains": [] }`).

Para mirar uma única rede de anúncios, envie só ela como `true` (ex.: `"facebook": true`). As demais redes ficam `false` por default.

### Regras de negócio de pages / offers (422 se violadas)

| Regra | Detalhe |
|-------|---------|
| `share` da oferta | Dentro de cada grupo de segmentação, os valores de `share` devem somar **exatamente 100**. Chave do grupo = join ordenado de `segmentation.country`, ou `"fallback"` quando a lista de países está vazia/ausente. |
| Oferta fallback | Pelo menos **uma** oferta deve não ter segmentação por país (lista `segmentation.country` vazia/ausente) — essa é a fallback. |
| `mode: "basic"` | Exatamente **uma** flag de rede de anúncios `true`, e `filters.device.devices` deve ter **≥ 1** entrada. |
| `mode: "advanced"` | Zero ou uma rede de anúncios; `device.devices` vazio permitido. |

### CampaignBody

Body de create e update de campanha. **PUT usa o mesmo body completo** — não há update parcial.

```json
{
  "$id": "CampaignBody",
  "type": "object",
  "required": ["name", "mode", "pages", "filters", "domain", "alias"],
  "properties": {
    "name": { "type": "string", "minLength": 3, "maxLength": 255 },
    "active": { "type": "boolean", "default": true, "description": "Opcional. Default true." },
    "mode": { "enum": ["advanced", "basic"] },
    "domain": { "type": ["string", "null"], "maxLength": 128 },
    "alias": { "type": ["string", "null"], "maxLength": 32 },
    "pages": {
      "type": "object",
      "required": ["white", "offers"],
      "properties": {
        "white": {
          "type": "object",
          "required": ["type", "content"],
          "properties": {
            "type": { "enum": ["content", "redirect", "iframe"] },
            "content": { "type": "string", "maxLength": 255 }
          }
        },
        "offers": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "required": ["type", "content", "share"],
            "properties": {
              "type": { "enum": ["content", "redirect", "iframe"] },
              "content": { "type": "string", "maxLength": 255 },
              "share": { "type": "number", "description": "Percentual; somas por grupo devem ser 100" },
              "segmentation": {
                "type": "object",
                "properties": {
                  "country": {
                    "type": "array",
                    "maxItems": 256,
                    "items": {
                      "type": "string",
                      "pattern": "^[A-Z]{2}$",
                      "description": "ISO 3166-1 alpha-2 maiúsculo. Igual a filters.geolocation.countries."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "filters": { "$ref": "CampaignFilters" }
  }
}
```

### CampaignFull

Resposta de campanha única (show, create, update, clone). Modo revisão = `filters.deny_always` (listagem expõe a mesma flag como `in_review` no topo).

```json
{
  "$id": "CampaignFull",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "created_at": { "type": "string", "format": "date-time" },
    "updated_at": { "type": "string", "format": "date-time" },
    "name": { "type": "string" },
    "active": { "type": "boolean" },
    "slug": { "type": "string" },
    "mode": { "enum": ["advanced", "basic"] },
    "domain": { "type": ["string", "null"] },
    "alias": { "type": ["string", "null"] },
    "network": { "type": "string" },
    "show_content_enabled": { "type": "boolean" },
    "pages": { "type": "object" },
    "filters": { "$ref": "CampaignFilters" },
    "is_test_mode": { "type": "boolean", "description": "Só no GET show: true se o IP do cliente está em filters.whitelist" }
  }
}
```

### CampaignShort

Item na resposta de listagem de campanhas.

```json
{
  "$id": "CampaignShort",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string" },
    "active": { "type": "boolean" },
    "created_at": { "type": "string", "format": "date-time" },
    "updated_at": { "type": "string", "format": "date-time" },
    "mode": { "enum": ["advanced", "basic"] },
    "network": { "type": "string" },
    "in_review": { "type": "boolean", "description": "Espelho somente leitura de filters.deny_always. true = campanha bloqueia todo tráfego de oferta." }
  }
}
```

### Domain

```json
{
  "$id": "Domain",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string" },
    "verified": { "type": "boolean" },
    "migrated": { "type": "boolean" },
    "created_at": { "type": "string", "format": "date-time" }
  }
}
```

---

# Campanhas (`/v1/campaigns`)

Todos os endpoints exigem `Authorization: Bearer ck_...`.

## `GET /v1/campaigns`

Listar campanhas.

**Scope:** `campaigns:list`

**Query:**

```json
{
  "type": "object",
  "properties": {
    "page": { "type": "integer", "minimum": 1, "default": 1 },
    "limit": { "enum": [1, 5, 10, 25, 50, 100], "default": 10 },
    "active": { "type": "boolean" },
    "name": { "type": "string", "maxLength": 255 },
    "network": {
      "enum": [
        "facebook",
        "google",
        "tiktok",
        "kwai",
        "taboola",
        "pinterest",
        "yandex",
        "mgid",
        "outbrain",
        "sms",
        "revcontent",
        "adskeeper",
        "newsbreak"
      ]
    }
  }
}
```

**Resposta `200`:**

```json
{
  "type": "object",
  "properties": {
    "count": { "type": "integer" },
    "data": {
      "type": "array",
      "items": { "$ref": "CampaignShort" }
    }
  }
}
```

**Exemplo:**

```bash
curl -H "Authorization: Bearer ck_YOUR_KEY" \
  "https://api.cloakup.me/v1/campaigns?page=1&limit=10"
```

---

## `POST /v1/campaigns`

Criar campanha.

**Scope:** `campaigns:create`

**Request body:** `{ "$ref": "CampaignBody" }`

**Resposta `201`:** `{ "$ref": "CampaignFull" }`

> **Defaults:** veja [Defaults na criação de campanha](#defaults-na-criação-de-campanha). Chaves omitidas são preenchidas no servidor — não envie o blob `filters` completo a menos que precise sobrescrever tudo.
>
> **Regras de negócio:** veja [Regras de negócio de pages / offers](#regras-de-negócio-de-pages--offers-422-se-violadas) (somas de `share`, oferta fallback, modo `basic`).

**Exemplo mínimo** (seguro para produção; só valores não-default):

```json
{
  "name": "Minha Campanha",
  "mode": "advanced",
  "domain": null,
  "alias": null,
  "pages": {
    "white": { "type": "content", "content": "safe.html" },
    "offers": [{ "type": "content", "content": "offer.html", "share": 100 }]
  },
  "filters": {
    "facebook": true,
    "geolocation": { "countries": ["BR"] },
    "referer": { "block_null": true, "domains": [] },
    "query": { "condition": "some", "params": {}, "rules": {} },
    "domain": {},
    "browser": {},
    "browser_language": { "languages": ["pt-br"] },
    "isp": {},
    "os": {},
    "user_agent": {},
    "blacklist": []
  }
}
```

Omitido aqui (defaults aplicam): `active`, demais redes de anúncios, `deny_always` (= `false`), `bots`, `proxy`, `adspy`, `cloakup_ai`, `state`, `city`, `network_type`, `device`, e toda flag `allow` / `remove_params`.

**Modo teste/revisão apenas** — defina `"deny_always": true` para forçar página segura para todo visitante (`in_review: true` na listagem).

---

## `GET /v1/campaigns/:id`

Obter campanha por ID.

**Scope:** `campaigns:show`

**Path params:**

```json
{
  "type": "object",
  "properties": { "id": { "type": "integer" } },
  "required": ["id"]
}
```

**Resposta `200`:** `{ "$ref": "CampaignFull" }` (inclui `is_test_mode` conforme o IP do caller está em `filters.whitelist`).

---

## `PUT /v1/campaigns/:id`

Atualizar campanha. **Substituição completa** — mesmo schema do create (`CampaignBody`). Envie o body inteiro; chaves de filtro omitidas ainda seguem os defaults de create, mas isso **não** é JSON Merge Patch.

**Scope:** `campaigns:update`

**Path params:** igual ao show.

**Request body:** `{ "$ref": "CampaignBody" }`

**Resposta `200`:** `{ "$ref": "CampaignFull" }`

---

## `DELETE /v1/campaigns/:id`

Exclusão lógica (soft-delete) da campanha.

**Scope:** `campaigns:delete`

**Path params:** igual ao show.

**Resposta `204`:** O status HTTP é `204`. O handler pode ainda anexar body JSON `{ "message": "..." }` (quirk do Express); trate sucesso como status `204` e ignore o body se vazio/não parseado.

---

## `POST /v1/campaigns/:id/clone`

Clonar campanha.

**Scope:** `campaigns:clone`

**Path params:** igual ao show.

**Request body:** nenhum

**Resposta `200`:** `{ "$ref": "CampaignFull" }`

---

## `PUT /v1/campaigns/:id/toggle-status`

Alternar flag `active` da campanha.

**Scope:** `campaigns:toggle`

**Path params:** igual ao show.

**Request body:** nenhum

**Resposta `200`:** body vazio (use case retorna void; re-busque com show se precisar)

---

# Domínios (`/v1/domains`)

## `GET /v1/domains`

Listar domínios.

**Scope:** `domains:list`

**Query:**

```json
{
  "type": "object",
  "properties": {
    "page": { "type": "integer", "minimum": 1, "default": 1 },
    "limit": { "enum": [1, 5, 10, 25, 50, 100], "default": 100 },
    "verified": { "type": "boolean" },
    "name": { "type": "string", "maxLength": 128 }
  }
}
```

**Resposta `200`:**

```json
{
  "type": "object",
  "properties": {
    "count": { "type": "integer" },
    "data": {
      "type": "array",
      "items": { "$ref": "Domain" }
    }
  }
}
```

---

## `POST /v1/domains`

Registrar domínio (**subdomínio obrigatório**, ex.: `track.example.com`). Rejeitado se `name` contiver `http`, `www` ou `/`, ou não for hostname de subdomínio válido.

**Scope:** `domains:create`

**Request body:**

```json
{
  "type": "object",
  "required": ["name"],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 128,
      "description": "Hostname de subdomínio, ex. track.example.com — sem scheme, www ou path"
    }
  }
}
```

**Resposta `201`:**

```json
{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string" },
    "verified": { "type": "boolean" },
    "migrated": { "type": "boolean" }
  }
}
```

---

## `PATCH /v1/domains/:id/verify`

Verificar propagação DNS/CNAME do domínio e ativar certificado.

**Scope:** `domains:verify`

**Path params:**

```json
{
  "type": "object",
  "properties": { "id": { "type": "integer" } },
  "required": ["id"]
}
```

**Request body:** nenhum

**Resposta `200`:**

```json
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "verified": { "type": "boolean" }
  }
}
```

---

## `DELETE /v1/domains/:id`

Excluir domínio.

**Scope:** `domains:delete`

**Path params:** igual ao verify.

**Request body:** nenhum

**Resposta `204`:** Mesmo quirk do delete de campanha — status `204`, body opcional `{ "message": "..." }`; trate o status como sucesso.

---

## Referência rápida (mapeamento para ferramentas de IA)

| Sugestão de nome da ferramenta | Método | Path | Scope |
|-------------------------------|--------|------|-------|
| `cloakup_list_campaigns` | GET | `/v1/campaigns` | `campaigns:list` |
| `cloakup_create_campaign` | POST | `/v1/campaigns` | `campaigns:create` |
| `cloakup_get_campaign` | GET | `/v1/campaigns/:id` | `campaigns:show` |
| `cloakup_update_campaign` | PUT | `/v1/campaigns/:id` | `campaigns:update` |
| `cloakup_delete_campaign` | DELETE | `/v1/campaigns/:id` | `campaigns:delete` |
| `cloakup_clone_campaign` | POST | `/v1/campaigns/:id/clone` | `campaigns:clone` |
| `cloakup_toggle_campaign` | PUT | `/v1/campaigns/:id/toggle-status` | `campaigns:toggle` |
| `cloakup_list_domains` | GET | `/v1/domains` | `domains:list` |
| `cloakup_create_domain` | POST | `/v1/domains` | `domains:create` |
| `cloakup_verify_domain` | PATCH | `/v1/domains/:id/verify` | `domains:verify` |
| `cloakup_delete_domain` | DELETE | `/v1/domains/:id` | `domains:delete` |

## Variáveis de ambiente (para clientes)

| Variável | Descrição |
|----------|-----------|
| `CLOAKUP_API_URL` | URL base sem barra no final (ex.: `https://api.cloakup.me`) |
| `CLOAKUP_API_KEY` | Key completa `ck_...` |

**Exemplo de requisição autenticada:**

```http
GET /v1/campaigns?page=1&limit=10 HTTP/1.1
Host: api.cloakup.me
Authorization: Bearer ck_0123456789abcdef...
Accept: application/json
```
