# curtei: referência completa da API pública

> curtei é um encurtador de links brasileiro e gratuito. Este arquivo reúne toda a documentação da API REST (https://api.curtei.com/v1) para uma IA ou um desenvolvedor integrar sem abrir outras páginas.

- Preço: grátis. Não há plano pago nem cota mensal de links; só um teto antiabuso de 1.000 links por hora e 5.000 por dia por conta.
- Sem cadastro: dá para encurtar em https://curtei.com (com verificação anti-robô). O link anônimo vale 30 dias e não tem métricas.
- Com conta grátis: links sem validade, métricas de clique (país, estado, cidade, dispositivo, origem), QR Code com cores e logo, senha, validade, limite de cliques, roteamento por visitante, teste A/B, campanhas, conversões, domínio próprio, webhooks e até 5 páginas de links (/@usuario).
- Links curtos ficam em https://l.curtei.com/<código>. Código personalizado só em domínio próprio.
- API REST pública: https://api.curtei.com/v1 (JSON, HTTPS, chave de API no cabeçalho Authorization).
- Privacidade: IP truncado antes de salvar, redirecionamento sem cookies, LGPD e GDPR. Destinos passam pelo Google Web Risk.

### API do curtei: encurte links pelo seu sistema

> Crie links curtos, QR Codes e leia métricas de clique pela API REST do curtei. Chave de API, respostas em JSON e exemplos em cURL, JavaScript, PHP e Python.

Fonte: https://curtei.com/docs/api


A API do curtei é REST, fala JSON em UTF-8 e só aceita HTTPS. A base é `https://api.curtei.com/v1`. Ela é gratuita como o resto do curtei: a conta não tem plano pago nem cota mensal de links.

### Primeiro link em 3 passos

1. [Crie uma conta grátis](https://curtei.com/cadastro) e confirme o e-mail. Sem a confirmação a conta só lê.
2. Em [Painel › Integrações](https://curtei.com/painel/integracoes), crie uma chave e escolha os escopos. O token (`<id>|wsk_live_...`) aparece uma vez só: guarde num cofre de segredos.
3. Chame a API com o cabeçalho `Authorization: Bearer <token>`.

cURL:

```bash
curl -X POST https://api.curtei.com/v1/links \
  -H "Authorization: Bearer $CURTEI_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://minhaloja.com.br/promo","tags":["newsletter"]}'
```

JavaScript:

```js
const res = await fetch("https://api.curtei.com/v1/links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CURTEI_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ url: "https://minhaloja.com.br/promo", tags: ["newsletter"] }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const link = await res.json();
console.log(link.short_url);
```

PHP:

```php
$ch = curl_init('https://api.curtei.com/v1/links');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('CURTEI_TOKEN'),
        'Content-Type: application/json',
        'Idempotency-Key: '.bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode(['url' => 'https://minhaloja.com.br/promo', 'tags' => ['newsletter']]),
]);
$link = json_decode(curl_exec($ch), true);
echo $link['short_url'];
```

Python:

```python
import os, uuid, requests

res = requests.post(
    "https://api.curtei.com/v1/links",
    headers={
        "Authorization": f"Bearer {os.environ['CURTEI_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"url": "https://minhaloja.com.br/promo", "tags": ["newsletter"]},
    timeout=10,
)
res.raise_for_status()
print(res.json()["short_url"])
```

Resposta `201 Created`:

```json
{
  "id": "0193a0b2-7c1e-7d3a-9f10-5b2c8e4a1d22",
  "short_url": "https://l.curtei.com/aB3xK9",
  "slug": "aB3xK9",
  "domain": "l.curtei.com",
  "url": "https://minhaloja.com.br/promo",
  "status": "active",
  "has_password": false,
  "qr_code_url": "https://api.curtei.com/v1/links/0193a0b2-7c1e-7d3a-9f10-5b2c8e4a1d22/qr",
  "total_clicks": 0,
  "unique_clicks": 0,
  "external_id": null,
  "metadata": null,
  "created_at": "2026-11-13T21:30:00Z"
}
```

### O que a API faz

| Recurso | O que dá para fazer | Página |
| --- | --- | --- |
| Links | Criar, listar, buscar por `external_id`, editar, desativar e apagar. | [Links](https://curtei.com/docs/api/links) |
| Lote | Até 1.000 links por chamada; até 50.000 em segundo plano. | [Links em lote](https://curtei.com/docs/api/links-em-lote) |
| Roteamento e A/B | Destino por país, estado, cidade, sistema, dispositivo ou idioma; variantes com peso. | [Roteamento e A/B](https://curtei.com/docs/api/roteamento) |
| QR Code | PNG ou SVG com cores, legenda e logo. | [QR Code](https://curtei.com/docs/api/qr-code) |
| Métricas | Resumo, série temporal, distribuição por dimensão, cliques e exportação CSV. | [Métricas](https://curtei.com/docs/api/metricas) |
| Conversões | Postback servidor a servidor ou pixel, com receita por moeda. | [Conversões](https://curtei.com/docs/api/conversoes) |
| Webhooks | Eventos de clique, expiração e bloqueio, assinados com HMAC. | [Webhooks](https://curtei.com/docs/api/webhooks) |
| Domínios | Links no seu próprio domínio, com código personalizado. | [Domínios](https://curtei.com/docs/api/dominios) |
| Páginas de links | A página /@usuario com os seus links agrupados. | [Páginas de links](https://curtei.com/docs/api/paginas-de-links) |
| Campanhas | Agrupar links e somar as métricas. | [Campanhas](https://curtei.com/docs/api/campanhas) |

### Limites e regras

| Item | Valor |
| --- | --- |
| Base da API | `https://api.curtei.com/v1` |
| Requisições | 120 por minuto por conta (cabeçalhos `X-RateLimit-Limit` e `X-RateLimit-Remaining`) |
| Criação de links | 1.000 por hora e 5.000 por dia por conta (teto antiabuso; cada item de lote conta) |
| Código personalizado (`slug`) | Só em domínio próprio |
| Idempotência | `Idempotency-Key` em todo `POST`, vale 24 h |
| Paginação | Por cursor: `{ "data": [...], "next_cursor": "..." }` |
| Versão | No caminho (`/v1`). Mudança incompatível vira `/v2`, com 6 meses de convivência |

### Usando uma IA para integrar

Cole `https://curtei.com/llms-full.txt` no ChatGPT, Claude, Cursor ou Copilot e peça a integração: o arquivo traz toda a API em texto puro. Cada página desta documentação também existe em Markdown, com `.md` no fim do endereço (por exemplo, [/docs/api/links.md](https://curtei.com/docs/api/links.md)).

### Autenticação com chave de API

> Como criar uma chave de API no curtei, escolher escopos, restringir por IP e enviar o token no cabeçalho Authorization.

Fonte: https://curtei.com/docs/api/autenticacao


Toda chamada autenticada leva o token no cabeçalho `Authorization`:

```bash
curl https://api.curtei.com/v1/me \
  -H "Authorization: Bearer 12|wsk_live_8f3K..."
```

### Criar uma chave

- Crie a chave em [Painel › Integrações](https://curtei.com/painel/integracoes). A conta precisa ter o e-mail confirmado.
- O token tem o formato `<id>|wsk_live_<segredo>` e aparece uma vez só. Guardamos apenas o hash; se perder, revogue e crie outra.
- O prefixo `wsk_` existe para ferramentas de varredura de segredos reconhecerem um token vazado.
- Cada conta pode ter até 20 chaves. Revogar uma chave tem efeito na hora.
- Opcional: restrinja a chave a uma lista de IPs. Chamada de outro IP recebe `403 ip_not_allowed`.

### Escopos

| Escopo | Permite |
| --- | --- |
| `links:read` | Listar e ler links, campanhas e o status de lotes |
| `links:write` | Criar, editar e apagar links e campanhas |
| `analytics:read` | Métricas, cliques, conversões, insights e exportações |
| `conversions:write` | Registrar conversões (postback) |
| `webhooks:manage` | Criar e gerenciar webhooks |
| `domains:manage` | Cadastrar e verificar domínios próprios |
| `pages:manage` | Criar e editar páginas de links |

Chamada sem o escopo necessário recebe `403 forbidden_scope`. Dê a cada sistema só os escopos que ele usa.

### Conferir a chave

`GET /me` devolve a conta, os escopos da chave e o uso. É a chamada mais simples para testar se o token está certo.

> Nunca coloque o token em código que roda no navegador ou no app do celular: qualquer pessoa conseguiria lê-lo. Chame a API a partir do seu servidor.

### Erros, limites e idempotência

> Formato de erro da API do curtei, códigos HTTP, rate limit, teto de criação, Idempotency-Key e paginação por cursor.

Fonte: https://curtei.com/docs/api/erros


### Formato de erro

Todo erro tem o mesmo formato. Use `code` no seu código (é estável) e `message` só para mostrar ou registrar.

```json
{
  "error": {
    "code": "url_blocked",
    "message": "A URL de destino foi classificada como insegura.",
    "details": {
      "reason": "phishing"
    },
    "request_id": "req_01JC..."
  }
}
```

Toda resposta traz `X-Request-Id`. Mande esse valor ao suporte quando algo der errado.

### Códigos

| HTTP | code | Quando |
| --- | --- | --- |
| 400 | `invalid_request` | Campo inválido; `details.fields` diz qual (ex.: `items.2.url`) |
| 400 | `invalid_url` | Destino não é uma URL http/https válida |
| 401 | `unauthorized` | Token ausente, errado ou revogado |
| 403 | `forbidden_scope` | A chave não tem o escopo da rota |
| 403 | `email_not_verified` | A conta ainda não confirmou o e-mail |
| 403 | `ip_not_allowed` | A chave está restrita a outros IPs |
| 404 | `not_found` | Recurso não existe ou é de outra conta |
| 409 | `slug_taken` | O código já está em uso no domínio |
| 409 | `idempotency_conflict` | Mesma `Idempotency-Key` com corpo diferente |
| 409 | `link_blocked` | Tentativa de reativar link bloqueado pela moderação |
| 422 | `url_blocked` | Destino inseguro (Google Web Risk ou lista de bloqueio) |
| 422 | `custom_slug_requires_own_domain` | `slug` enviado no domínio compartilhado |
| 422 | `domain_not_verified` | Domínio ainda não verificado |
| 429 | `rate_limited` | Passou do limite de requisições por minuto |
| 429 | `creation_rate_exceeded` | Passou do teto de criação (hora ou dia) |
| 5xx | `internal_error` | Erro do nosso lado; tente de novo com a mesma `Idempotency-Key` |

### Limites

- Requisições: 120 por minuto por conta. Toda resposta traz `X-RateLimit-Limit` e `X-RateLimit-Remaining`; ao passar, `429` com `Retry-After` (segundos) e `X-RateLimit-Reset`.
- Criação de links: 1.000 por hora e 5.000 por dia. Ao passar, `429 creation_rate_exceeded` com `details.window` (`hour` ou `day`), `details.limit` e `details.retry_after`.
- Não existe cota mensal de links nem plano pago.

### Idempotência

Mande `Idempotency-Key: <uuid>` em todo `POST`. Se a mesma chave chegar de novo em 24 h, a API devolve a resposta original, com `Idempotent-Replayed: true`, sem criar outro link. Assim um retry depois de timeout não duplica nada.

### Paginação

Listas devolvem `{ "data": [...], "next_cursor": "..." }`. Para a próxima página, repita a chamada com `cursor=<next_cursor>`; quando `next_cursor` vier `null`, acabou. `limit` vai até 100.

```js
let cursor = null;
do {
  const url = new URL("https://api.curtei.com/v1/links");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.CURTEI_TOKEN}` } });
  const page = await res.json();
  for (const link of page.data) console.log(link.short_url, link.total_clicks);
  cursor = page.next_cursor;
} while (cursor);
```

### Criar link curto

> POST /v1/links no curtei: parâmetros, exemplos em cURL, JavaScript, PHP e Python, resposta e erros. Também listar, editar e apagar links.

Fonte: https://curtei.com/docs/api/links

Endpoint: `POST https://api.curtei.com/v1/links`
Escopo: `links:write`

Cria um link curto para uma URL. Só `url` é obrigatório. A conta precisa ter o e-mail confirmado.

### Parâmetros do corpo

| Campo | Tipo | Descrição |
| --- | --- | --- |
| `url` (obrigatório) | string | Destino http ou https. Passa pela checagem de segurança (Google Web Risk e lista de bloqueio). |
| `domain` | string | Domínio do link. Padrão: o domínio padrão da conta (`l.curtei.com`, se você não tiver um próprio). |
| `slug` | string | Código personalizado (`promo-natal`). Só em domínio próprio; no compartilhado o código é sempre aleatório. |
| `title` | string | Nome para achar o link no painel. |
| `redirect_type` | 301 ou 302 | Tipo do redirecionamento. Padrão: 302. |
| `expires_at` | ISO-8601 | Depois disso o link para de redirecionar. |
| `max_clicks` | inteiro | Limite de cliques; ao atingir, o link expira. |
| `password` | string | 8 a 128 caracteres. O visitante digita a senha antes de seguir; a resposta traz `has_password`. |
| `utm` | objeto | `source`, `medium`, `campaign`, `term`, `content`, anexados ao destino. |
| `tags` | string[] | Etiquetas para filtrar e somar métricas. |
| `campaign_id` | uuid | Vincula o link a uma [campanha](https://curtei.com/docs/api/campanhas). |
| `external_id` | string | Seu identificador (pedido, cliente). Buscável por `GET /links/by-external-id/{external_id}`. |
| `metadata` | objeto | Dados livres seus, até 4 KB; voltam nas respostas e nos webhooks. |
| `track_clicks` | boolean | Registrar cliques. Padrão: `true`. |
| `track_conversions` | boolean | Requer `track_clicks`. Anexa `wsc_id` ao destino para registrar [conversões](https://curtei.com/docs/api/conversoes). |
| `open_in_app` | boolean | No celular, tenta abrir o app (YouTube, Instagram, WhatsApp, TikTok…) em vez do navegador. |
| `hide_referrer` | boolean | O destino não fica sabendo de qual página veio o clique. |
| `rules` / `variants` | objeto[] | [Roteamento e teste A/B](https://curtei.com/docs/api/roteamento). |

### Exemplo

cURL:

```bash
curl -X POST https://api.curtei.com/v1/links \
  -H "Authorization: Bearer $CURTEI_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://minhaloja.com.br/promo","tags":["newsletter"]}'
```

JavaScript:

```js
const res = await fetch("https://api.curtei.com/v1/links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CURTEI_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ url: "https://minhaloja.com.br/promo", tags: ["newsletter"] }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const link = await res.json();
console.log(link.short_url);
```

PHP:

```php
$ch = curl_init('https://api.curtei.com/v1/links');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('CURTEI_TOKEN'),
        'Content-Type: application/json',
        'Idempotency-Key: '.bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode(['url' => 'https://minhaloja.com.br/promo', 'tags' => ['newsletter']]),
]);
$link = json_decode(curl_exec($ch), true);
echo $link['short_url'];
```

Python:

```python
import os, uuid, requests

res = requests.post(
    "https://api.curtei.com/v1/links",
    headers={
        "Authorization": f"Bearer {os.environ['CURTEI_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"url": "https://minhaloja.com.br/promo", "tags": ["newsletter"]},
    timeout=10,
)
res.raise_for_status()
print(res.json()["short_url"])
```

### Resposta 201

```json
{
  "id": "0193a0b2-7c1e-7d3a-9f10-5b2c8e4a1d22",
  "short_url": "https://l.curtei.com/aB3xK9",
  "slug": "aB3xK9",
  "domain": "l.curtei.com",
  "url": "https://minhaloja.com.br/promo",
  "status": "active",
  "has_password": false,
  "qr_code_url": "https://api.curtei.com/v1/links/0193a0b2-7c1e-7d3a-9f10-5b2c8e4a1d22/qr",
  "total_clicks": 0,
  "unique_clicks": 0,
  "external_id": null,
  "metadata": null,
  "created_at": "2026-11-13T21:30:00Z"
}
```

### Erros comuns

| HTTP | code | O que fazer |
| --- | --- | --- |
| 400 | `invalid_url` | Envie uma URL completa, com `https://`. |
| 403 | `email_not_verified` | Confirme o e-mail da conta. |
| 409 | `slug_taken` | Escolha outro `slug` ou omita para gerar um aleatório. |
| 422 | `url_blocked` | O destino foi classificado como inseguro; `details.reason` diz o motivo. |
| 422 | `custom_slug_requires_own_domain` | Cadastre um [domínio próprio](https://curtei.com/docs/api/dominios) ou não envie `slug`. |
| 429 | `creation_rate_exceeded` | Espere `details.retry_after` segundos. |

### Listar links

`GET /links` (escopo `links:read`). Filtros: `tag`, `campaign_id`, `external_id`, `status`, `domain`, `search` (título, destino ou código), `created_from`, `created_to`, `sort` (`created_at` ou `total_clicks`), `limit` (até 100) e `cursor`.

```bash
curl "https://api.curtei.com/v1/links?tag=newsletter&limit=50" \
  -H "Authorization: Bearer $CURTEI_TOKEN"
```

### Obter, editar e apagar

| Rota | Uso |
| --- | --- |
| `GET /links/{id}` | Um link pelo id |
| `GET /links/by-external-id/{external_id}` | Um link pelo seu identificador |
| `PATCH /links/{id}` | Muda `url`, `title`, `expires_at`, `max_clicks`, `status` (`active`/`disabled`), `tags`, `utm`, `metadata`, `password`, `rules`, `variants`. O `slug` não muda. |
| `DELETE /links/{id}` | Apaga. O código fica reservado por 90 dias. |

Trocar a `url` de um link já divulgado é o jeito de mudar o destino sem trocar o link curto nem reimprimir o QR Code. A nova URL passa pela mesma checagem de segurança.

### Criar links em lote

> POST /v1/links/bulk: até 1.000 links por chamada com resultado por item, ou até 50.000 em segundo plano.

Fonte: https://curtei.com/docs/api/links-em-lote

Endpoint: `POST https://api.curtei.com/v1/links/bulk`
Escopo: `links:write`

Mande `{ "links": [ {...}, {...} ] }`; cada item aceita os mesmos campos do [`POST /links`](https://curtei.com/docs/api/links). Um item com erro não impede os outros.

### Até 1.000 itens: resposta na hora

Resposta `207 Multi-Status` com um resultado por item, na mesma ordem (`index`):

```json
{
  "summary": {
    "total": 2,
    "created": 1,
    "failed": 1
  },
  "results": [
    {
      "index": 0,
      "status": 201,
      "link": {
        "id": "...",
        "short_url": "https://l.curtei.com/aB3xK9"
      }
    },
    {
      "index": 1,
      "status": 400,
      "error": {
        "code": "invalid_url",
        "message": "...",
        "details": {
          "reason": "scheme_not_allowed"
        }
      }
    }
  ]
}
```

### De 1.001 a 50.000 itens: em segundo plano

- A resposta é `202` com `{ "id", "status": "pending", "total", "status_url" }`.
- Consulte `GET /links/bulk/{id}` (`processed`, `created`, `failed`) até `status` ser `done`.
- Aí `results_url` é um endereço assinado, válido por 24 h e sem token, com um resultado por linha (NDJSON).

> Cada destino passa pela checagem de segurança, então o lote tem um teto de URLs de destino (contando regras e variantes): 3.000 na resposta imediata e 75.000 em segundo plano. Acima disso, `400` com `details.destinations` e `details.max`.

### Roteamento por visitante e teste A/B

> Mande cada visitante para um destino conforme país, estado, cidade, sistema, dispositivo ou idioma, e divida o tráfego entre variantes A/B.

Fonte: https://curtei.com/docs/api/roteamento


### Regras (`rules`)

Até 20 regras por link. A primeira que casar decide o destino; sem nenhuma, vale `url`. Condições na mesma regra são combinadas com E; valores numa lista, com OU.

```json
{
  "url": "https://minhaloja.com.br",
  "rules": [
    {
      "label": "iPhone",
      "conditions": {
        "os": [
          "ios"
        ],
        "device_type": [
          "mobile"
        ]
      },
      "url": "https://apps.apple.com/br/app/id123"
    },
    {
      "label": "Android",
      "conditions": {
        "os": [
          "android"
        ]
      },
      "url": "https://play.google.com/store/apps/details?id=br.loja"
    },
    {
      "label": "Portugal",
      "conditions": {
        "country": [
          "PT"
        ]
      },
      "url": "https://loja.pt/"
    }
  ]
}
```

| Condição | Valores |
| --- | --- |
| `country` | ISO-3166 alfa-2: `BR`, `PT`… |
| `region` | Estado em ISO 3166-2: `BR-SP`, `BR-MG`… |
| `city` | Nome da cidade (`São Paulo`), sem diferenciar acento nem maiúsculas; combine com `region` |
| `os` | `ios`, `android`, `windows`, `macos`, `linux`, `other` |
| `device_type` | `mobile`, `tablet`, `desktop` |
| `language` | Do navegador: `pt`, `pt-br`, `es`… |

Para abrir um app, use links universais (iOS), App Links (Android) ou a página da loja. Esquemas próprios como `meuapp://` não são aceitos como destino.

### Teste A/B (`variants`)

De 2 a 10 variantes com peso de 1 a 100. O mesmo visitante cai sempre na mesma variante. As regras são avaliadas antes; as variantes valem para quem não casou com nenhuma regra.

```json
{
  "variants": [
    {
      "label": "A",
      "url": "https://loja.com.br/oferta-a",
      "weight": 50
    },
    {
      "label": "B",
      "url": "https://loja.com.br/oferta-b",
      "weight": 50
    }
  ]
}
```

No `PATCH`, mandar `rules` ou `variants` substitui a lista inteira daquele tipo (`[]` remove); omitir mantém. Compare as variantes com `GET /links/{id}/stats/breakdown?dimension=rule` ou, em vendas, `GET /links/{id}/conversions/stats?dimension=rule`.

### QR Code do link

> Baixe o QR Code de um link curto em PNG ou SVG, com cores, legenda e logo no centro.

Fonte: https://curtei.com/docs/api/qr-code

Endpoint: `GET https://api.curtei.com/v1/links/{id}/qr`
Escopo: `links:read`

A resposta de todo link traz `qr_code_url`. O QR aponta para o link curto com `?qr=1`, então as métricas separam quem escaneou de quem clicou.

| Parâmetro | Valores |
| --- | --- |
| `format` | `png` (padrão) ou `svg` |
| `size` | 128 a 2048 pixels |
| `margin` | 0 a 64 |
| `color`, `background` | `#rrggbb`. O código precisa ser mais escuro que o fundo, com contraste de pelo menos 3:1 |
| `error_correction` | `low`, `medium`, `quartile` ou `high` |
| `label` | Texto embaixo do código, até 40 caracteres |

```bash
curl "https://api.curtei.com/v1/links/{id}/qr?format=svg&size=1024&color=%2315171A" \
  -H "Authorization: Bearer $CURTEI_TOKEN" -o qr.svg
```

### Com logo

Use `POST /links/{id}/qr` em multipart com o campo `logo` (PNG ou JPG até 512 KB) e os mesmos parâmetros acima. O logo vai no centro e a correção de erro passa a `high`.

Só precisa do QR, sem conta nem API? Use o [gerador de QR Code grátis](https://curtei.com/gerador-qr-code).

### Métricas de clique

> Leia cliques por link ou da conta inteira: resumo, série temporal, distribuição por país, cidade, dispositivo e origem, cliques individuais e exportação CSV.

Fonte: https://curtei.com/docs/api/metricas

Escopo: `analytics:read`

Todas as rotas aceitam `from` e `to` (ISO-8601 ou só a data; padrão: últimos 30 dias; até 400 dias), `timezone` (padrão `America/Sao_Paulo`) e `include_bots` (padrão `false`).

| Rota | Retorno |
| --- | --- |
| `GET /links/{id}/stats` | Total, únicos, bots, QR, último clique e top 5 de país, estado, dispositivo, sistema, navegador e canal |
| `GET /links/{id}/stats/timeseries?interval=hour\|day\|week\|month` | `[{ "t", "clicks", "unique", "bots" }]`, com zeros nos intervalos sem clique |
| `GET /links/{id}/stats/breakdown?dimension=...` | `[{ "key", "clicks", "unique", "pct" }]` |
| `GET /links/{id}/clicks` | Cliques individuais, mais recentes primeiro, sem IP |
| `GET /stats`, `/stats/timeseries`, `/stats/breakdown` | O mesmo somando todos os links; filtros `tag`, `campaign_id`, `link_ids[]` |
| `GET /stats/top-links?metric=clicks\|unique` | Ranking de links no período |
| `GET /stats/insights` | Destaques do período comparados ao anterior (canal, cidade, link que cresceu) |
| `POST /exports` → `GET /exports/{id}` | CSV ou NDJSON gerado em segundo plano; `download_url` vale 24 h |

Dimensões de `breakdown`: `country`, `region`, `city`, `device_type`, `os`, `browser`, `language`, `referer_host`, `referer_channel`, `utm_source`, `utm_medium`, `utm_campaign`, `as_org` (operadora) e `rule`.

```bash
curl "https://api.curtei.com/v1/links/{id}/stats/breakdown?dimension=city&from=2026-11-01&to=2026-11-13" \
  -H "Authorization: Bearer $CURTEI_TOKEN"
```

```json
{
  "period": {
    "from": "2026-11-01T00:00:00-03:00",
    "to": "2026-11-13T23:59:59-03:00",
    "timezone": "America/Sao_Paulo",
    "include_bots": false
  },
  "dimension": "city",
  "data": [
    {
      "key": "BR/SP/São Paulo",
      "clicks": 602,
      "unique": 511,
      "pct": 0.393
    },
    {
      "key": "BR/MG/Belo Horizonte",
      "clicks": 211,
      "unique": 190,
      "pct": 0.1377
    }
  ]
}
```

> Únicos = visitantes distintos por link por dia, somados no período. Cliques de robôs e de pré-visualização (WhatsApp, Telegram, Slack) ficam fora por padrão.

### Conversões e receita

> Ligue track_conversions no link e registre vendas e cadastros por postback servidor a servidor ou pixel, com receita por moeda.

Fonte: https://curtei.com/docs/api/conversoes

Endpoint: `POST https://api.curtei.com/v1/conversions`
Escopo: `conversions:write`

1. Crie o link com `"track_conversions": true`. O redirecionamento passa a anexar `wsc_id=<id do clique>` ao destino.
2. No seu site, guarde o `wsc_id` (cookie, sessão ou junto do pedido).
3. Quando a venda ou o cadastro acontecer, devolva esse id por postback ou pixel.

### Postback (recomendado)

```bash
curl -X POST https://api.curtei.com/v1/conversions \
  -H "Authorization: Bearer $CURTEI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"click_id": "WSC_ID", "event": "purchase", "order_id": "PED-1", "value": "199.90", "currency": "BRL"}'
```

Só `click_id` é obrigatório (`event` padrão: `conversion`; `currency` obrigatória com `value`). `201` gravou; `200` com `"duplicate": true` quando o mesmo `click_id` + `event` + `order_id` já existe; `202` quando o clique tem menos de 10 minutos e ainda está sendo gravado.

### Pixel

```html
<img src="https://api.curtei.com/v1/conversions/pixel.gif?click_id=WSC_ID&event=signup" width="1" height="1" alt="">
```

O pixel não usa token, então os valores podem ser forjados por quem tem o `click_id`. Para receita, prefira o postback.

### Relatórios

`GET /links/{id}/conversions/stats` e `GET /stats/conversions` (escopo `analytics:read`) devolvem total, taxa de conversão (conversões ÷ cliques únicos) e receita por moeda, com `dimension` = `event`, `rule`, `country`, `device_type`, `referer_channel` ou `source`.

### Webhooks

> Receba eventos de clique, expiração, bloqueio e conversão no seu servidor, assinados com HMAC-SHA256, com reenvio automático.

Fonte: https://curtei.com/docs/api/webhooks

Endpoint: `POST https://api.curtei.com/v1/webhooks`
Escopo: `webhooks:manage`

```bash
curl -X POST https://api.curtei.com/v1/webhooks \
  -H "Authorization: Bearer $CURTEI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://meusistema.com.br/hooks/curtei", "events": ["link.clicked", "link.expired"]}'
```

A resposta traz o `secret` uma vez só. A URL precisa ser HTTPS (porta 443 ou 8443) e apontar para um IP público.

### Eventos

| Evento | Quando |
| --- | --- |
| `link.created`, `link.updated`, `link.deleted` | Mudanças no link |
| `link.blocked` | Moderação, lista de bloqueio ou denúncia |
| `link.expired` | Passou de `expires_at` ou atingiu `max_clicks` (`reason`) |
| `link.clicked` | Um por clique humano |
| `clicks.batch` | Cliques acumulados, até 500 por entrega, a cada minuto. Prefira para volume alto |
| `conversion.created` | Conversão registrada |

### Verificar a assinatura

Cada entrega traz `X-Webhook-Timestamp` e `X-Webhook-Signature: v1=<HMAC-SHA256(secret, timestamp + "." + corpo)>`. Rejeite assinaturas diferentes e timestamps com mais de 5 minutos.

Node:

```js
import crypto from "node:crypto";

export function webhookValido(rawBody, headers, secret) {
  const ts = headers["x-webhook-timestamp"];
  const esperado = "v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const recebido = headers["x-webhook-signature"] ?? "";
  const recente = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  return recente && recebido.length === esperado.length && crypto.timingSafeEqual(Buffer.from(recebido), Buffer.from(esperado));
}
```

PHP:

```php
$ts = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$esperado = 'v1='.hash_hmac('sha256', $ts.'.'.file_get_contents('php://input'), $secret);
$valido = hash_equals($esperado, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'])
    && abs(time() - (int) $ts) <= 300;
```

### Entrega e reenvio

- Responda `2xx` em até 10 s. Redirecionamentos não são seguidos.
- Falhou? Nova tentativa em 1 min, 5 min, 30 min, 2 h, 6 h e 24 h. Depois da 7ª, a entrega fica `failed`.
- Depois de 50 falhas seguidas o webhook é desativado; `PATCH` com `"status": "active"` reativa.
- `GET /webhooks/{id}/deliveries` mostra 30 dias de histórico; `POST .../deliveries/{id}/retry` reenvia; `POST /webhooks/{id}/test` manda um `webhook.test`.

### Domínio próprio

> Cadastre um domínio próprio (go.suamarca.com.br) para links curtos com a sua marca e código personalizado.

Fonte: https://curtei.com/docs/api/dominios

Endpoint: `POST https://api.curtei.com/v1/domains`
Escopo: `domains:manage`

```bash
curl -X POST https://api.curtei.com/v1/domains \
  -H "Authorization: Bearer $CURTEI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "go.minhaloja.com.br", "not_found_url": "https://minhaloja.com.br"}'
```

1. A resposta traz as instruções de DNS: um registro TXT `_encurtador-verify.<seu domínio>` (prova de posse) e um CNAME para o tráfego.
2. Crie os registros no seu provedor de DNS e chame `POST /domains/{id}/verify`.
3. Com o domínio ativo, o certificado HTTPS é emitido sozinho no primeiro acesso.

- `PATCH /domains/{id}` muda `not_found_url` (para onde vão códigos inexistentes) e `is_default` (links sem `domain` passam a usar este).
- No domínio próprio você escolhe o `slug` dos links.
- `DELETE /domains/{id}` só funciona sem links no domínio (`409 domain_in_use`).
- Um cadastro pendente não reserva o domínio: verifique em até 7 dias.

### Páginas de links

> Crie e edite pela API a página /@usuario do curtei, com os seus links organizados em grupos, para a bio do Instagram e do TikTok.

Fonte: https://curtei.com/docs/api/paginas-de-links

Endpoint: `POST https://api.curtei.com/v1/link-pages`
Escopo: `pages:manage`

Cada conta pode ter até 5 páginas, com até 100 itens cada, publicadas em `https://curtei.com/@<handle>`.

```json
{
  "handle": "casaipe",
  "name": "Casa Ipê",
  "bio": "Decoração artesanal feita à mão",
  "socials": [
    {
      "network": "instagram",
      "url": "https://instagram.com/casaipe"
    }
  ],
  "published": true,
  "items": [
    {
      "title": "Black Friday · até 50% off",
      "url": "https://casaipe.com.br/bf",
      "featured": true,
      "badge": "Só até domingo"
    },
    {
      "title": "Loja online",
      "link_id": "<uuid de um link da conta>",
      "group": "Comprar",
      "icon": "shopping-bag"
    },
    {
      "title": "Fale no WhatsApp",
      "url": "https://wa.me/5511999999999",
      "group": "Atendimento",
      "icon": "message-circle"
    }
  ]
}
```

- `handle`: 3 a 30 caracteres (`a-z`, `0-9`, `.`, `-`, `_`). Em uso → `409 handle_taken`.
- `items` é salvo inteiro: a ordem do array é a ordem na página. Itens seguidos com o mesmo `group` aparecem juntos.
- Cada item aponta para `link_id` (um link da conta; o clique entra nas métricas) ou para `url` direta.
- Rotas: `GET /link-pages`, `GET`/`PATCH`/`DELETE /link-pages/{id}`. Leitura pública, sem token: `GET /public/pages/{handle}`.

### Campanhas

> Agrupe links em campanhas e veja as métricas somadas: cliques, cidades, canais e conversões da campanha inteira.

Fonte: https://curtei.com/docs/api/campanhas

Endpoint: `POST https://api.curtei.com/v1/campaigns`
Escopo: `links:write`

```bash
curl -X POST https://api.curtei.com/v1/campaigns \
  -H "Authorization: Bearer $CURTEI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Black Friday 2026", "color": "amber"}'
```

- `name` até 80 caracteres, único na conta. `color`: `blue`, `green`, `amber`, `red`, `purple`, `pink`, `teal` ou `gray`.
- Vincule links com `campaign_id` no `POST`/`PATCH /links`.
- Métricas: `/stats`, `/stats/timeseries`, `/stats/breakdown`, `/stats/top-links` e `/stats/insights` aceitam `campaign_id`.
- `PATCH /campaigns/{id}` com `"archived": true` arquiva. `DELETE` apaga a campanha; os links continuam.
- Até 200 campanhas por conta. Leitura com `links:read`.
