# Guia de integração: Indica — API para lojas parceiras

> Um arquivo só, para pessoas e agentes de IA. Gerado do contrato OpenAPI (versão 1.0.0-beta.2), o mesmo de `https://api.indicacompras.com.br/v1/openapi.json`. Página interativa: https://api.indicacompras.com.br/docs

**Endereço base:** `https://api.indicacompras.com.br/v1`

**Autenticação:** cabeçalho `Authorization: Bearer <chave>` em todas as chamadas.

## Instruções para agentes de IA

1. Comece na homologação, com a chave `ik_test_…`. Leia a chave de uma variável de ambiente (por exemplo `INDICA_API_KEY`); nunca a grave no código, em log ou no repositório.
2. Antes de tudo, confira a chave com `GET /me`. `partner_type` diz se a conta é `hub` (várias lojas) ou `store` (loja direta, uma loja por ambiente).
3. Mande só os campos do contrato: campo desconhecido é recusado (`unknown_field`). Não invente dado — preço, estoque, frete e ficha técnica vêm do sistema da loja. Frete que a loja não sabe: não mande `shipping` (o Indica mostra "a calcular", nunca grátis).
4. Dinheiro em centavos inteiros (`29990` = R$ 299,90). Datas em ISO 8601 com fuso. `updated_at` é quando o dado mudou na loja: envio com `updated_at` mais antigo que o último é ignorado (`ignored_stale`).
5. No cadastro da empresa (`PUT /stores/{store_id}`), envie `default_commission`: `{"type":"percentage","rate":6.5}` ou `{"type":"fixed","amount_cents":900}`. O campo `commission` no produto prevalece sobre a geral; `null` ou ausência herda a geral. `PATCH` com `commission:null` volta a herdar; no `PUT` omitir remove a regra específica. Zero é explícito; ausência de ambas é desconhecida. `GET /products/{sku}` informa regra efetiva, origem e `expected_commission_cents`. Estimativa por unidade ao preço atual, sem frete; receita efetiva depende de pedido pago. Comissão nunca altera o ranking.
6. O `sku` é único por parceiro e ambiente, entre todas as lojas do hub. Use prefixos por loja (ex.: `centro-TEN-1`). Um SKU não pode mudar de `store_id`, mesmo depois de removido (`sku_store_conflict`).
7. Em toda escrita, gere uma `Idempotency-Key` e use a MESMA ao repetir a chamada com a mesma rota e corpo: repetir em até 24 h devolve a resposta original sem gravar de novo. Outra rota ou corpo com a mesma chave é recusado (`422`).
8. `429`: espere os segundos de `Retry-After`. `5xx` ou falha de rede: repita com espera crescente e a mesma `Idempotency-Key`. `422`: corrija todos os campos de `errors` antes de repetir — repetir igual dá o mesmo erro.
9. Carga inicial com `POST /products/batch` (até 1.000 itens e 5 MB por chamada): `202` confirma a fila persistida (`queued`), não a publicação. Acompanhe `GET /jobs/{job_id}` até `done`. A fila retoma após reinício; em `done`, `received = accepted + rejected + pending + removed`. `removed` conta operações de exclusão concluídas, inclusive SKU ausente. Depois, `PATCH /products/{sku}` só no que mudou.
10. A validade conta do último envio recebido: produto sem `PUT` ou `PATCH` há mais de 48 h sai da busca (`stale`) até o próximo. Agende um envio periódico — por exemplo um `PATCH` com `available` e `updated_at` atuais.
11. Depois de enviar, confira `GET /products/{sku}`: `state` diz se entrou e `reasons` diz por que não.
12. No site da loja, leia o parâmetro `indica_click` da URL de chegada e guarde por 30 dias (o último clique vence). É ele que liga a venda ao Indica.
13. Consulte GET /clicks por período. Quando o cliente pagar, envie POST /conversions com event_id único, status paid, paid_at=occurred_at, moeda BRL e todos os itens líquidos de desconto. Guarde o click_id da loja por 30 dias. Não envie dados pessoais. A regra vigente no primeiro recebimento de paid vira snapshot; não é uma reconstrução de termos históricos nem comissão liquidada.
14. Estornos usam status refunded e refunded_cents acumulado por linha. Cancelamento usa cancelled e devolve todo o valor. Repita os itens originais e paid_at. Se chegar antes de paid, 409 payment_required: envie paid primeiro e reenvie. Eventos antigos são auditados sem retroceder saldo. GET /reports/commission usa UTC e paid_at com estado atual; mantenha since/until nas próximas páginas. POST /sandbox/clicks permite testar tudo com ik_test.
15. Termine pela "Lista de pronto" abaixo e só então peça a chave de produção (`ik_live_…`).

## Visão geral

O Indica é um assistente de compras: as pessoas perguntam o que querem
comprar e recebem as melhores ofertas de várias lojas. Esta API permite
que uma loja — ou um hub com várias lojas — envie seus produtos para
aparecerem nas buscas do Indica. A v1 recebe catálogo e comissões,
devolve cliques e relatórios e recebe pedidos pagos, cancelamentos e estornos.
Campanhas/cupons e distribuição na vitrine/WhatsApp seguem em preparação.

**Fases 1 e 2 disponíveis:** conta, lojas, produtos, lotes e ciclo comercial.
A fase 3 ainda responde `404` e não aparece na referência pública.
Comece pela homologação, com uma chave `ik_test_…`.

### Primeiros passos

1. **Peça sua chave de homologação** ao Indica (`ik_test_…`). Ela grava
   numa área de testes que não aparece no site.
2. **Confira a chave:** `GET /me`. Na página interativa
   (`/desenvolvedores/api`), clique em *Authorize* e cole `ik_test_…`;
   depois use *Try it out* em qualquer operação.
3. **Cadastre a loja** (`PUT /stores/{store_id}`) com nome, CNPJ, site e
   domínios.
4. **Envie os produtos** (`PUT /products/{sku}` ou `POST /products/batch`)
   e confira o estado de cada um em `GET /products/{sku}`.
5. Tudo certo? Peça a chave de produção (`ik_live_…`) e repita os envios
   com ela. O Indica aprova cada loja e os produtos entram nas buscas.

```bash
curl https://api.indicacompras.com.br/v1/me \
  -H "Authorization: Bearer ik_test_SUA_CHAVE"
```

O contrato também está em
[openapi.json](https://api.indicacompras.com.br/v1/openapi.json) e
[openapi.yaml](https://api.indicacompras.com.br/v1/openapi.yaml), para
importar no Postman, no Insomnia ou num gerador de cliente.
Integrando com um agente de IA? Entregue a ele o
[guia.md](https://api.indicacompras.com.br/v1/guia.md): tudo num arquivo
só, com instruções e a lista de pronto.

### Hub de lojas ou loja direta

O contrato é o mesmo para os dois; muda quantas lojas a conta tem.

- **Hub (várias lojas, um acesso só):** uma chave para o hub inteiro. Cada
  loja é cadastrada com um `store_id` escolhido por você (o código que o
  hub já usa) e o **CNPJ** dela; cada produto diz de qual loja é, em
  `store_id`. O Indica aprova ou suspende loja por loja.
- **Loja direta:** a mesma coisa com uma loja só. A conta de loja direta
  aceita uma loja por ambiente (`single_store` na segunda); para várias,
  a conta vira hub — fale com o Indica.

`GET /me` diz qual é o seu caso em `partner_type` (`hub` ou `store`).

O `sku` é único por parceiro e ambiente, inclusive entre lojas do hub.
Use um prefixo por loja (por exemplo `centro-TEN-1`). Trocar o `store_id`
de um SKU já cadastrado, mesmo removido, responde `sku_store_conflict`.
Envios simultâneos não fazem o preço retroceder: prevalece o maior `updated_at`.
A mesma `Idempotency-Key` com rota ou corpo diferente responde `422`.

### Comissão esperada

No cadastro da empresa (PUT /stores/{store_id}), envie default_commission:
`{"type":"percentage","rate":6.5}` ou `{"type":"fixed","amount_cents":900}`.
No produto, commission define uma exceção; null ou ausência herda a geral.
PATCH /products/{sku} aceita commission e updated_at; commission=null volta
a herdar. PUT substitui o cadastro inteiro: omitir a regra remove a exceção.
GET /products/{sku} informa regra efetiva, origem e expected_commission_cents.
A estimativa é privada e por unidade ao preço atual, sem frete. Descontos,
quantidade e valores líquidos são calculados em POST /conversions. O primeiro
evento paid recebido guarda a regra vigente naquele recebimento, não uma
reconstrução do contrato na data histórica da venda. O snapshot não muda
com alterações do catálogo. Comissão esperada não confirma pagamento ao Indica.

### Como funciona

1. Você cadastra suas **lojas** (`PUT /stores/{store_id}`), com nome,
   CNPJ, site e domínios. O Indica aprova cada loja e o domínio dela antes
   de publicar qualquer produto.
2. Você envia os **produtos** (um a um ou em lote) e mantém preço e
   estoque atualizados com `PATCH`. Nós indexamos do nosso lado: a busca
   não chama a sua API.
3. Quem clica num produto seu passa pelo nosso redirecionador e chega à
   sua página com o parâmetro **`indica_click=<código>`** na URL. Guarde
   esse código (cookie de 30 dias, último clique vence).
4. Consulte `GET /clicks`. Quando o cliente pagar, envie `POST /conversions`
   com status `paid`, sem dados pessoais. Envie novos eventos para estornos
   ou cancelamentos. Consulte `GET /reports/commission` para conciliar.
5. Em homologação, `POST /sandbox/clicks` cria um clique de teste para um
   SKU seu. Nunca cria visitas ou pedidos na produção.

### Regras que valem para todos os parceiros

- **Comissão não altera o ranking.** Seus produtos competem com os de
  todas as outras lojas por relevância, preço e qualidade. A taxa de
  comissão nunca entra na ordem da busca.
- **Preço, estoque, frete e cupom vêm de você.** O Indica não estima nem
  completa esses dados. Frete não informado aparece como desconhecido,
  nunca como grátis.
- **Produto sem atualização há mais de 48 horas sai da busca** até o
  próximo `PATCH` ou `PUT`. Preço velho é preço errado.
- **Foto de capa conferida** antes de ir ao ar: abre por HTTPS num
  domínio da loja, JPEG/PNG/WebP, pelo menos 500 px no menor lado.
- **Preço fora da realidade não aparece.** Em cada busca, uma oferta
  muito abaixo das outras do mesmo produto (golpe, réplica ou erro de
  cadastro) fica de fora daquela busca — a mesma régua para todas as
  lojas, parceiras ou não. O estado do produto não muda.
- **Nenhum dado pessoal de comprador.** As vendas não aceitam nome, CPF,
  e-mail, telefone ou endereço, nem qualquer campo fora do contrato.
- Toda URL enviada precisa ser HTTPS, e as de produto e campanha precisam
  ser de um domínio aprovado da loja.

### Convenções

- JSON em UTF-8. Dinheiro em **centavos inteiros de real** (`29990` =
  R$ 299,90). Datas em ISO 8601 com fuso.
- Autenticação por chave no cabeçalho `Authorization: Bearer <chave>`.
  Chaves `ik_live_…` publicam no site; chaves `ik_test_…` gravam numa
  área de homologação que não aparece para ninguém.
- Erros seguem o RFC 9457 (`application/problem+json`), com a lista de
  campos inválidos em `errors`.
- Escritas aceitam o cabeçalho `Idempotency-Key`: repetir a mesma chave
  em até 24 h devolve a resposta original, sem gravar de novo.
- Freio por chave: 600 escritas e 1.200 leituras por minuto. Acima disso,
  `429` com `Retry-After`.
- Listagens paginam por cursor (`cursor` + `limit`, máximo 200).
- Chamadas identificadas recebem **X-Request-Id**, gerado pelo Indica.
  Informe esse ID ao suporte para localizar a operação. O painel interno
  registra resultado, referências e campos enviados, sem corpo, query
  string ou credenciais. Lote recebido na fila e lote processado são
  entradas diferentes. O histórico começa após sua ativação; falha do
  gravador não altera o resultado de uma operação já concluída.

## Lista de pronto

A integração está pronta quando tudo abaixo funciona com a chave de homologação:

- [ ] `GET /me` responde `200` com a chave `ik_test_…` e o `partner_type` esperado (`hub` ou `store`).
- [ ] Cada loja cadastrada com `PUT /stores/{store_id}`: CNPJ válido, site e todos os domínios das páginas de produto (e de fotos, em `image_hosts`).
- [ ] Um produto enviado com `PUT /products/{sku}` aparece em `GET /products/{sku}`, segundos depois (conferência da capa), com `state` `accepted` — ou `pending` com `store_not_active` ou `domain_not_approved` enquanto o Indica não aprova a loja e o domínio. Se vier `rejected` com motivo de foto (`image_*`), a capa foi corrigida e reenviada.
- [ ] Carga inicial com `POST /products/batch`, acompanhada em `GET /jobs/{job_id}` até `done`, e cada item de `errors` corrigido e reenviado.
- [ ] Mudança de preço ou estoque enviada com `PATCH /products/{sku}` e conferida em `GET /products/{sku}`.
- [ ] Produto que saiu de linha tirado do ar com `DELETE /products/{sku}`.
- [ ] Envio periódico agendado: todo produto ativo recebe `PUT` ou `PATCH` em menos de 48 horas.
- [ ] Toda escrita manda `Idempotency-Key` e repete a mesma ao tentar de novo; `429` espera `Retry-After`; `422` corrige os campos de `errors` antes de repetir.
- [ ] O site da loja lê `indica_click` da URL de chegada e guarda por 30 dias.
- [ ] Com ik_test, crie um clique em `POST /sandbox/clicks`, envie paid e estorno em `POST /conversions` e confira `GET /reports/commission`.
- [ ] Tudo acima conferido: peça a chave de produção (`ik_live_…`) e repita os envios com ela.

## Operações disponíveis (fases 1 e 2)

### GET /me — Quem é esta chave

Útil como primeiro teste da integração.

**Respostas**

- `200` — Parceiro, ambiente e escopos da chave. Corpo: `Account`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### GET /stores — Lista as lojas deste parceiro

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `cursor` | query | não | string |  |
| `limit` | query | não | integer | Regras: mínimo 1; máximo 200. |

**Respostas**

- `200` — Página de lojas.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

Corpo da resposta `200`:

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `data` | lista de `Store` | sim | Veja o esquema `Store`. |
| `next_cursor` | string ou null | não |  |

### GET /stores/{store_id} — Consulta uma loja

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `store_id` | path | sim | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

**Respostas**

- `200` — A loja. Corpo: `Store`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `404` — veja `NotFound` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### PUT /stores/{store_id} — Cria ou substitui uma loja

Domínios novos ficam `pending_review` até aprovação do Indica. Os
produtos da loja são aceitos nesse meio-tempo, mas só aparecem no
site depois da aprovação.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `store_id` | path | sim | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `Idempotency-Key` | header | não | string | Repetir a mesma chave em até 24 h devolve a resposta original. Regras: máx. 100 caracteres. |

**Corpo** (`application/json`, esquema `StoreInput`)

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `default_commission` | `Commission` | não | Veja o esquema `Commission`. |
| `name` | string | sim | Regras: mín. 2 caracteres; máx. 80 caracteres. |
| `cnpj` | string | sim | CNPJ da empresa dona da loja, com ou sem pontuação. Aceita o alfanumérico (Receita, desde julho de 2026). Conferimos os dígitos verificadores; a resposta devolve só os caracteres (`11222333000181`). Identifica a loja para a comissão e evita a mesma empresa cadastrada duas vezes. Regras: formato `^[A-Za-z0-9./-]{14,18}$`. |
| `logo_url` | string | não | Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `website_url` | string | sim | Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `domains` | lista de string | sim | Domínios das páginas de produto e campanha desta loja (ex. `loja.com.br`). Entram na lista de domínios oficiais do nosso redirecionador só depois de aprovados. Regras: mín. 1 itens; máx. 5 itens; cada item: formato `^[a-z0-9.-]+\.[a-z]{2,}$`. |
| `image_hosts` | lista de string | não | Domínios de CDN das fotos, se diferentes dos da loja. Regras: máx. 5 itens; cada item: formato `^[a-z0-9.-]+\.[a-z]{2,}$`. |
| `returns_days` | integer | não | Prazo de troca/devolução. Regras: mínimo 0; máximo 365. |
| `handling_days` | integer | não | Prazo para despachar. Regras: mínimo 0; máximo 60. |

Campos fora desta lista são recusados.

Exemplo — Uma das lojas de um hub:

```json
{
  "name": "Loja Centro Calçados",
  "cnpj": "11.222.333/0001-81",
  "website_url": "https://centro.calcadosexemplo.com.br",
  "domains": [
    "centro.calcadosexemplo.com.br"
  ],
  "image_hosts": [
    "cdn.calcadosexemplo.com.br"
  ],
  "logo_url": "https://cdn.calcadosexemplo.com.br/logos/centro.png",
  "returns_days": 7,
  "handling_days": 1
}
```

O hub escolhe o `store_id` (o código que ele já usa) e repete esta chamada para cada loja.

Exemplo — Loja que integra direto (uma loja só):

```json
{
  "name": "Casa Exemplo",
  "cnpj": "12.ABC.345/01DE-35",
  "website_url": "https://www.casaexemplo.com.br",
  "domains": [
    "casaexemplo.com.br"
  ]
}
```

**Respostas**

- `200` — Loja salva. Corpo: `Store`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

Resposta `200` — Loja recém-cadastrada, aguardando aprovação do Indica:

```json
{
  "store_id": "centro",
  "name": "Loja Centro Calçados",
  "cnpj": "11222333000181",
  "website_url": "https://centro.calcadosexemplo.com.br",
  "domains": [
    "centro.calcadosexemplo.com.br"
  ],
  "image_hosts": [
    "cdn.calcadosexemplo.com.br"
  ],
  "status": "pending_review",
  "domains_approved": []
}
```

### GET /products — Lista produtos e o estado de cada um do lado do Indica

Para conciliação. Filtre por loja e estado.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `store_id` | query | não | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `state` | query | não | string | `pending` em validação; `accepted` no ar; `rejected` recusado (veja `reasons`); `stale` sem atualização há mais de 48 h, fora da busca até o próximo envio; `removed` apagado. Regras: um de: `pending`, `accepted`, `rejected`, `stale`, `removed`. |
| `cursor` | query | não | string |  |
| `limit` | query | não | integer | Regras: mínimo 1; máximo 200. |

**Respostas**

- `200` — Página de produtos.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

Corpo da resposta `200`:

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `data` | lista de `ProductStatus` | sim | Veja o esquema `ProductStatus`. |
| `next_cursor` | string ou null | não |  |

### GET /products/{sku} — Como o produto ficou do lado do Indica

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `sku` | path | sim | string | SKU estável do seu lado. Cada variação (cor, tamanho) tem o seu. Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

**Respostas**

- `200` — Estado e, se recusado, os motivos. Corpo: `ProductStatus`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `404` — veja `NotFound` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### PUT /products/{sku} — Cria ou substitui um produto

Idempotente. Se `updated_at` for mais antigo que o já recebido para o
mesmo SKU, a chamada é aceita e ignorada (`200` com `ignored_stale`),
para que envios fora de ordem não voltem um preço antigo.

A conferência da foto de capa (`image_urls[0]`) é assíncrona e leva
segundos: a resposta diz `pending`, e o resultado aparece em
`GET /products/{sku}` — `accepted`, ou `rejected` com o motivo
(`image_unreachable`, `image_type`, `image_too_small`). Foto
recusada é conferida de novo a cada hora (CDN que voltou passa
sozinho); foto boa, a cada 7 dias. Corrigiu a foto no mesmo
endereço? Envie o produto de novo (`PUT`): capa recusada é
conferida outra vez em segundos. Capa aprovada não é refeita a cada
envio.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `sku` | path | sim | string | SKU estável do seu lado. Cada variação (cor, tamanho) tem o seu. Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `Idempotency-Key` | header | não | string | Repetir a mesma chave em até 24 h devolve a resposta original. Regras: máx. 100 caracteres. |

**Corpo** (`application/json`, esquema `ProductInput`)

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `commission` | `Commission` | não | Veja o esquema `Commission`. |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `title` | string | sim | Regras: mín. 5 caracteres; máx. 200 caracteres. |
| `description` | string | não | Regras: máx. 5000 caracteres. |
| `brand` | string | não | Regras: máx. 80 caracteres. |
| `model` | string | não | Regras: máx. 80 caracteres. |
| `gtin` | string | não | EAN/GTIN. A melhor prova para comparar o mesmo produto entre lojas. Regras: formato `^(\d{8}\|\d{12,14})$`. |
| `category` | string | não | Caminho da categoria na sua loja, ex. `Calçados > Tênis > Corrida`. Regras: máx. 200 caracteres. |
| `attributes` | object | não | Ficha técnica (voltagem, capacidade, tamanho…). Só o que é verdade; nada estimado. Regras: máx. 50 chaves. |
| `condition` | string | não | Usado e recondicionado aparecem como alternativa rotulada, nunca como o produto novo pedido. Regras: um de: `new`, `used`, `refurbished`. |
| `group_id` | string | não | Mesmo valor em todas as variações do mesmo produto (cores, tamanhos). Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `variant_attributes` | object | não | O que distingue esta variação, ex. `{"cor":"preto","tamanho":"42"}`. Regras: máx. 5 chaves. |
| `product_url` | string | sim | Página do produto, num domínio aprovado da loja. Sem parâmetro de rastreio — o `indica_click` é acrescentado por nós. Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `image_urls` | lista de string | sim | 1 a 10 fotos públicas, JPEG/PNG/WebP, mínimo 500 px no menor lado. A primeira é a capa, a foto que o site mostra; é ela que o Indica confere (abre por HTTPS no domínio declarado, tipo e tamanho). Regras: mín. 1 itens; máx. 10 itens; cada item: máx. 2048 caracteres; cada item: formato `^https://`; cada item: uri. |
| `price_cents` | integer | sim | Preço à vista que a pessoa paga hoje, sem cupom. Regras: mínimo 1. |
| `original_price_cents` | integer | não | Preço "de". Só se foi praticado antes; maior que `price_cents`. Regras: mínimo 1. |
| `available` | boolean | sim |  |
| `stock_quantity` | integer | não | Regras: mínimo 0. |
| `shipping` | `Shipping` | não | Veja o esquema `Shipping`. |
| `installments` | object | não |  |
| `rating` | object | não | Avaliação sem quantidade não vale. |
| `updated_at` | string | sim | Quando o dado mudou do seu lado. Envio mais antigo que o último recebido é ignorado. Regras: date-time. |

Campos fora desta lista são recusados.

Exemplo — Produto com todos os campos que ajudam a vender:

```json
{
  "store_id": "centro",
  "title": "Tênis de Corrida Leve Masculino Preto 42",
  "description": "Cabedal em tela respirável, entressola em EVA, solado de borracha.",
  "brand": "RunFast",
  "model": "RF-200",
  "gtin": "7891234567895",
  "category": "Calçados > Tênis > Corrida",
  "attributes": {
    "cor": "preto",
    "tamanho": "42",
    "genero": "masculino"
  },
  "condition": "new",
  "group_id": "RF-200",
  "variant_attributes": {
    "cor": "preto",
    "tamanho": "42"
  },
  "product_url": "https://centro.calcadosexemplo.com.br/p/rf-200-preto-42",
  "image_urls": [
    "https://cdn.calcadosexemplo.com.br/rf-200/preto-1.jpg",
    "https://cdn.calcadosexemplo.com.br/rf-200/preto-2.jpg"
  ],
  "price_cents": 29990,
  "original_price_cents": 39990,
  "available": true,
  "stock_quantity": 12,
  "shipping": {
    "type": "fixed",
    "cost_cents": 1990,
    "delivery_days_min": 2,
    "delivery_days_max": 5
  },
  "installments": {
    "count": 10,
    "value_cents": 2999,
    "interest_free": true
  },
  "rating": {
    "average": 4.7,
    "count": 318
  },
  "updated_at": "2026-10-03T09:00:00-03:00"
}
```

Quanto mais dado verdadeiro, melhor o produto compete: GTIN ajuda a
comparar com outras lojas, frete e parcelamento entram no preço final
mostrado, avaliação com quantidade pesa na qualidade.

Exemplo — Só o obrigatório:

```json
{
  "store_id": "centro",
  "title": "Meia Esportiva Cano Curto Kit 3 Pares",
  "price_cents": 3990,
  "available": true,
  "product_url": "https://centro.calcadosexemplo.com.br/p/meia-kit-3",
  "image_urls": [
    "https://cdn.calcadosexemplo.com.br/meia-kit-3.jpg"
  ],
  "updated_at": "2026-10-03T09:00:00-03:00"
}
```

Sem `shipping`, o frete aparece como "a calcular" — nunca como grátis.

**Respostas**

- `200` — Recebido. Corpo: `ProductStatus`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

Resposta `200` — Recebido, esperando a aprovação da loja:

```json
{
  "sku": "RF-200-PRETO-42",
  "store_id": "centro",
  "state": "pending",
  "reasons": [
    {
      "code": "store_not_active",
      "message": "A loja ainda não foi aprovada pelo Indica (ou está suspensa)."
    }
  ],
  "received_at": "2026-10-03T12:00:01.000Z",
  "published_at": null
}
```

Resposta `200` — No ar, aparecendo nas buscas:

```json
{
  "sku": "RF-200-PRETO-42",
  "store_id": "centro",
  "state": "accepted",
  "reasons": [],
  "received_at": "2026-10-03T12:00:01.000Z",
  "published_at": "2026-10-03T12:00:01.000Z"
}
```

Resposta `200` — Envio mais antigo que o último recebido (ignorado):

```json
{
  "sku": "RF-200-PRETO-42",
  "store_id": "centro",
  "state": "accepted",
  "reasons": [],
  "received_at": "2026-10-03T12:00:01.000Z",
  "published_at": "2026-10-03T12:00:01.000Z",
  "ignored_stale": true
}
```

### PATCH /products/{sku} — Atualiza preço, estoque, disponibilidade ou frete

A chamada do dia a dia. Só os campos enviados mudam. Mesma regra de
`updated_at` do `PUT`.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `sku` | path | sim | string | SKU estável do seu lado. Cada variação (cor, tamanho) tem o seu. Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `Idempotency-Key` | header | não | string | Repetir a mesma chave em até 24 h devolve a resposta original. Regras: máx. 100 caracteres. |

**Corpo** (`application/json`, esquema `ProductPatch`)

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `commission` | `Commission` | não | Veja o esquema `Commission`. |
| `price_cents` | integer | não | Regras: mínimo 1. |
| `original_price_cents` | integer ou null | não | `null` remove o preço "de". Regras: mínimo 1. |
| `available` | boolean | não |  |
| `stock_quantity` | integer | não | Regras: mínimo 0. |
| `shipping` | `Shipping` | não | Veja o esquema `Shipping`. |
| `updated_at` | string | sim | Regras: date-time. |

Campos fora desta lista são recusados.

Exemplo — Mudou o preço:

```json
{
  "price_cents": 27990,
  "updated_at": "2026-10-03T14:30:00-03:00"
}
```

Exemplo — Esgotou:

```json
{
  "available": false,
  "stock_quantity": 0,
  "updated_at": "2026-10-03T15:00:00-03:00"
}
```

Exemplo — Acabou a promoção (tira o preço "de"):

```json
{
  "price_cents": 39990,
  "original_price_cents": null,
  "updated_at": "2026-10-04T00:00:00-03:00"
}
```

**Respostas**

- `200` — Atualizado. Corpo: `ProductStatus`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `404` — veja `NotFound` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### DELETE /products/{sku} — Tira o produto do ar

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `sku` | path | sim | string | SKU estável do seu lado. Cada variação (cor, tamanho) tem o seu. Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

**Respostas**

- `204` — Removido (ou já não existia).
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### POST /products/batch — Envia até 1.000 produtos de uma vez (carga inicial)

Retorna 202 com state=queued somente após persistir o lote. Processamento em trechos de até 50 itens, retomado após falha ou reinício; acompanhe por GET /jobs/{job_id}. Cada trecho confirma produtos e progresso juntos. Em done, received = accepted + rejected + pending + removed. removed conta operações delete concluídas, inclusive SKU já ausente. pending é o estado na hora do processamento; consulte o produto para o estado atual.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | não | string | Repetir a mesma chave em até 24 h devolve a resposta original. Regras: máx. 100 caracteres. |

**Corpo** (`application/json`, esquema `BatchInput`)

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `items` | lista de uma das formas abaixo | sim | Regras: mín. 1 itens; máx. 1000 itens. |

Campos fora desta lista são recusados.

Cada item de `items` tem uma destas formas.

Forma 1 (`upsert`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `op` | string | sim | Regras: sempre `upsert`. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `product` | `ProductInput` | sim | Veja o esquema `ProductInput`. |

Campos fora desta lista são recusados.

Forma 2 (`delete`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `op` | string | sim | Regras: sempre `delete`. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

Campos fora desta lista são recusados.

Exemplo — Carga inicial com inclusão e remoção:

```json
{
  "items": [
    {
      "op": "upsert",
      "sku": "RF-200-PRETO-42",
      "product": {
        "store_id": "centro",
        "title": "Tênis de Corrida Leve Masculino Preto 42",
        "price_cents": 29990,
        "available": true,
        "product_url": "https://centro.calcadosexemplo.com.br/p/rf-200-preto-42",
        "image_urls": [
          "https://cdn.calcadosexemplo.com.br/rf-200/preto-1.jpg"
        ],
        "updated_at": "2026-10-03T09:00:00-03:00"
      }
    },
    {
      "op": "delete",
      "sku": "RF-100-AZUL-40"
    }
  ]
}
```

**Respostas**

- `202` — Lote aceito para processamento. Corpo: `Job`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `413` — Corpo acima de 5 MB ou mais de 1.000 itens. Corpo: `Problem`.
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### GET /jobs/{job_id} — Andamento e erros de um lote

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `job_id` | path | sim | string |  |

**Respostas**

- `200` — O lote. Corpo: `Job`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `404` — veja `NotFound` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### GET /clicks — Consulta os cliques enviados às suas lojas

Sem IP, navegador ou dados pessoais. Ordenação por momento, loja e ID.
Retorna since/until normalizados; mantenha esses valores nas próximas páginas.
Janela UTC [since, until), até 31 dias. Padrão de 30 dias até agora.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `since` | query | não | string | Regras: date-time. |
| `until` | query | não | string | Regras: date-time. |
| `store_id` | query | não | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `cursor` | query | não | string |  |
| `limit` | query | não | integer | Regras: mínimo 1; máximo 200. |

**Respostas**

- `200` — Página de cliques limitada ao parceiro e ambiente da chave. Corpo: `ClickPage`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### POST /sandbox/clicks — Gera clique de homologação para testar o ciclo comercial

Apenas ik_test. SKU deve existir nesta loja. Não exige aprovação pública.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | não | string | Repetir a mesma chave em até 24 h devolve a resposta original. Regras: máx. 100 caracteres. |

**Corpo** (`application/json`)

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

Campos fora desta lista são recusados.

**Respostas**

- `200` — Clique de teste. Use click_id e occurred_at no evento paid. Corpo: `PartnerClick`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### GET /conversions — Lista pedidos pelo momento do pagamento e estado atual

Intervalo UTC [since, until), até 31 dias, padrão de 30 dias até agora.
Filtra paid_at, não a data do estorno. O saldo de um período anterior
pode mudar após um estorno. Mantenha since/until nas próximas páginas.
Exportação de fechamento e extrato de movimentos ainda não disponíveis.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `since` | query | não | string | Regras: date-time. |
| `until` | query | não | string | Regras: date-time. |
| `store_id` | query | não | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `cursor` | query | não | string |  |
| `limit` | query | não | integer | Regras: mínimo 1; máximo 200. |

**Respostas**

- `200` — Página de pedidos. Corpo: `ConversionPage`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### POST /conversions — Informa pedido pago, cancelamento ou estorno acumulado

Envie por HTTPS com Bearer; não há segredo ou assinatura HMAC adicional nesta versão.
Esta é uma chamada servidor a servidor feita pela loja, não um webhook enviado pelo Indica.
Não envie dados pessoais do comprador. Somente pedidos pagos originados no Indica.

Primeiro envie status paid, occurred_at igual a paid_at e refunded_cents=0
em todos os itens. click_id deve ser do parceiro/ambiente/loja e ocorrer
até 30 dias antes de paid_at, inclusive. Clique inválido responde 422
invalid_attribution e não grava pedido. A data de recebimento não limita
atribuição: uma venda paga dentro da janela pode ser notificada depois.

Use event_id único por parceiro/ambiente para CADA evento, inclusive entre
lojas do hub. Deduplicação permanente: mesma entrada normalizada retorna
duplicate=true, mesmo depois de 24 h; outro corpo com event_id repetido é
409 event_conflict. Idempotency-Key também é aceita (rota/corpo por 24 h).
Pedido é único por store_id/order_id. Repetir não soma faturamento.

O primeiro paid recebido guarda commission, commission_source e
commission_snapshot_at vigentes naquele recebimento. Não reconstrói
termos históricos e não confirma comissão devida/paga ao Indica.
Comissão percentual usa o valor líquido real dos produtos, sem frete;
comissão fixa é por unidade e reduz proporcionalmente ao valor estornado.
Arredondamento por item ao centavo mais próximo, meio para cima.
Base líquida zero gera comissão zero; regra ausente gera null se há saldo.

Para refunded ou cancelled repita TODOS os itens originais e paid_at.
refunded_cents é o acumulado da LINHA (quantidade inteira), não por unidade
nem o incremento deste evento. Cancelamento estorna todas as linhas.
Estornos não diminuem e cancelled não volta a paid. Valores/quantidades,
SKUs, click_id e paid_at originais são imutáveis. Até 200 SKUs únicos.
Se o estorno chegar antes de paid: 409 payment_required; envie paid e
reenvie o estorno. Evento anterior ao último é auditado com ignored_stale=true
sem alterar saldo; eventos divergentes no mesmo instante respondem 409.
A resposta de repetição é a original daquele evento; GET de pedido traz
o estado atual. Limite de cinco minutos no futuro nas datas de eventos.

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | não | string | Repetir a mesma chave em até 24 h devolve a resposta original. Regras: máx. 100 caracteres. |

**Corpo** (`application/json`, esquema `ConversionEvent`)

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `event_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `order_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `click_id` | string | sim | Regras: formato `^ic_[A-Za-z0-9_-]{16,64}$`. |
| `status` | string | sim | Regras: um de: `paid`, `refunded`, `cancelled`. |
| `currency` | string | sim | Regras: sempre `BRL`. |
| `paid_at` | string | sim | Pagamento original; imutável. Regras: date-time. |
| `occurred_at` | string | sim | Momento do evento. Em paid, igual a paid_at. Regras: date-time. |
| `items` | lista de `ConversionItem` | sim | SKUs únicos; total até 1 trilhão de centavos. Repita todos os itens originais nos estornos. Veja o esquema `ConversionItem`. Regras: mín. 1 itens; máx. 200 itens. |

Campos fora desta lista são recusados.

**Respostas**

- `200` — Pedido e evento persistidos atomicamente. Corpo: `ConversionReceipt`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `409` — event_conflict, order_conflict, payment_required ou invalid_transition. Corrija a causa antes de repetir. Corpo: `Problem`.
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### GET /conversions/{order_id} — Consulta o estado atual de um pedido da sua loja

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `order_id` | path | sim | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `store_id` | query | sim | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

**Respostas**

- `200` — Pedido com snapshot de comissão. Corpo: `OrderView`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `404` — Pedido não encontrado neste parceiro/ambiente/loja.
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

### GET /reports/commission — Resume cliques, pedidos e comissão esperada por loja

Intervalo UTC [since, until), até 31 dias. Cliques por occurred_at;
pedidos por paid_at, com seu estado atual, inclusive estornos posteriores.
Não é taxa de conversão por coorte nem fechamento financeiro imutável.
expected_commission_cents é null se qualquer pedido com saldo tiver regra
desconhecida. known_expected_commission_cents é só o subtotal conhecido.
Saldo de comissão esperado, não pagamento recebido pelo Indica. Toda loja
com clique ou pedido aparece, inclusive lojas sem pedidos. Sem atividade
no período retorna totals zerados e stores vazio (contagens reais).

**Parâmetros**

| Nome | Onde | Obrigatório | Tipo | Observações |
| --- | --- | --- | --- | --- |
| `since` | query | não | string | Regras: date-time. |
| `until` | query | não | string | Regras: date-time. |
| `store_id` | query | não | string | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

**Respostas**

- `200` — Totais e agrupamento por loja do mesmo snapshot de leitura. Corpo: `CommissionReport`.
- `401` — veja `Unauthorized` em "Respostas de erro comuns".
- `422` — veja `ValidationError` em "Respostas de erro comuns".
- `429` — veja `TooManyRequests` em "Respostas de erro comuns".

## Respostas de erro comuns

Todo erro segue o RFC 9457 (`application/problem+json`): `type`, `title`, `status`, às vezes `detail`, e `errors` com cada campo inválido (`field`, `code`, `message`).

### Unauthorized

Chave ausente, inválida ou revogada.

Exemplo — Chave ausente ou inválida:

```json
{
  "type": "https://api.indicacompras.com.br/v1/problemas/401",
  "title": "Chave ausente, inválida ou revogada.",
  "status": 401,
  "detail": "Envie Authorization: Bearer ik_live_… (ou ik_test_…)."
}
```

### NotFound

Não existe para este parceiro.

### ValidationError

Corpo inválido. `errors` lista cada campo e o motivo. Além dos códigos
de formato (`required`, `pattern`, `integer`, `https`, `unknown_field`…),
a loja usa `cnpj` + `invalid` (dígitos não conferem), `store_id` +
`single_store` (conta de loja direta já tem uma loja) e `store_id` +
`conflict` (outro id seu vira o mesmo endereço no Indica);
a validação de produto usa `store_not_found` (cadastre a loja antes),
`domain_not_declared` (página fora dos `domains` da loja),
`image_host_not_declared` (foto fora dos `domains`/`image_hosts`),
`not_higher` (preço "de" não é maior que o preço) e `future`
(`updated_at` mais de 24 h no futuro).

Exemplo — Produto com campos inválidos (todos os erros de uma vez):

```json
{
  "type": "https://api.indicacompras.com.br/v1/problemas/422",
  "title": "Corpo inválido.",
  "status": 422,
  "errors": [
    {
      "field": "price_cents",
      "code": "integer",
      "message": "Precisa ser número inteiro."
    },
    {
      "field": "product_url",
      "code": "domain_not_declared",
      "message": "A página precisa ser de um domínio declarado da loja."
    },
    {
      "field": "comprador_cpf",
      "code": "unknown_field",
      "message": "Campo fora do contrato."
    }
  ]
}
```

### TooManyRequests

Freio da chave: 600 escritas e 1.200 leituras por minuto. Tente de novo
depois de `Retry-After` segundos; o lote conta como uma escrita só.

Cabeçalhos: `Retry-After`.

Exemplo — Muitas chamadas no último minuto:

```json
{
  "type": "https://api.indicacompras.com.br/v1/problemas/429",
  "title": "Muitas chamadas para esta chave.",
  "status": 429,
  "detail": "Tente de novo depois de Retry-After segundos."
}
```

## Esquemas

### ExternalId

string. Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`.

### HttpsUrl

string. Regras: máx. 2048 caracteres; formato `^https://`; uri.

### Account

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `partner_id` | string | sim |  |
| `name` | string | sim |  |
| `partner_type` | string | sim | `hub`: várias lojas com um acesso; `store`: loja direta, uma loja por ambiente. Regras: um de: `hub`, `store`. |
| `environment` | string | sim | Regras: um de: `live`, `test`. |
| `scopes` | lista de string | sim | Regras: cada item: um de: `catalog`, `clicks`, `promotions`, `conversions`, `reports`. |

### Commission

Comissão declarada pela loja, estimada por unidade vendida em BRL. percentage aplica rate ao price_cents atual, sem frete, arredondando meio centavo para cima. fixed paga amount_cents por unidade. Zero é uma regra explícita; null/ausência no produto herda a geral da loja. Sem nenhuma regra a expectativa é desconhecida, não zero. Não confirma receita nem interfere no ranking.

Forma 1 (`percentage`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `type` | string | sim | Regras: sempre `percentage`. |
| `rate` | number | sim | Regras: mínimo 0; máximo 100; múltiplo de 0.01. |

Campos fora desta lista são recusados.

Forma 2 (`fixed`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `type` | string | sim | Regras: sempre `fixed`. |
| `amount_cents` | integer | sim | Regras: mínimo 0; máximo 9007199254740991. |

Campos fora desta lista são recusados.

Forma 3:

`null`.

### StoreInput

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `default_commission` | `Commission` | não | Veja o esquema `Commission`. |
| `name` | string | sim | Regras: mín. 2 caracteres; máx. 80 caracteres. |
| `cnpj` | string | sim | CNPJ da empresa dona da loja, com ou sem pontuação. Aceita o alfanumérico (Receita, desde julho de 2026). Conferimos os dígitos verificadores; a resposta devolve só os caracteres (`11222333000181`). Identifica a loja para a comissão e evita a mesma empresa cadastrada duas vezes. Regras: formato `^[A-Za-z0-9./-]{14,18}$`. |
| `logo_url` | string | não | Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `website_url` | string | sim | Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `domains` | lista de string | sim | Domínios das páginas de produto e campanha desta loja (ex. `loja.com.br`). Entram na lista de domínios oficiais do nosso redirecionador só depois de aprovados. Regras: mín. 1 itens; máx. 5 itens; cada item: formato `^[a-z0-9.-]+\.[a-z]{2,}$`. |
| `image_hosts` | lista de string | não | Domínios de CDN das fotos, se diferentes dos da loja. Regras: máx. 5 itens; cada item: formato `^[a-z0-9.-]+\.[a-z]{2,}$`. |
| `returns_days` | integer | não | Prazo de troca/devolução. Regras: mínimo 0; máximo 365. |
| `handling_days` | integer | não | Prazo para despachar. Regras: mínimo 0; máximo 60. |

Campos fora desta lista são recusados.

### Store

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `default_commission` | `Commission` | não | Veja o esquema `Commission`. |
| `name` | string | sim | Regras: mín. 2 caracteres; máx. 80 caracteres. |
| `cnpj` | string | sim | CNPJ da empresa dona da loja, com ou sem pontuação. Aceita o alfanumérico (Receita, desde julho de 2026). Conferimos os dígitos verificadores; a resposta devolve só os caracteres (`11222333000181`). Identifica a loja para a comissão e evita a mesma empresa cadastrada duas vezes. Regras: formato `^[A-Za-z0-9./-]{14,18}$`. |
| `logo_url` | string | não | Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `website_url` | string | sim | Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `domains` | lista de string | sim | Domínios das páginas de produto e campanha desta loja (ex. `loja.com.br`). Entram na lista de domínios oficiais do nosso redirecionador só depois de aprovados. Regras: mín. 1 itens; máx. 5 itens; cada item: formato `^[a-z0-9.-]+\.[a-z]{2,}$`. |
| `image_hosts` | lista de string | não | Domínios de CDN das fotos, se diferentes dos da loja. Regras: máx. 5 itens; cada item: formato `^[a-z0-9.-]+\.[a-z]{2,}$`. |
| `returns_days` | integer | não | Prazo de troca/devolução. Regras: mínimo 0; máximo 365. |
| `handling_days` | integer | não | Prazo para despachar. Regras: mínimo 0; máximo 60. |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `status` | string | sim | Regras: um de: `pending_review`, `active`, `suspended`. |
| `domains_approved` | lista de string | não |  |

### Shipping

Frete do produto. **Ausente = desconhecido**: o Indica mostra "frete a
calcular", nunca "grátis". Só use `free` quando for grátis de fato
(com o mínimo de pedido, se houver).

Forma 1 (`free`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `type` | string | sim | Regras: sempre `free`. |
| `min_order_cents` | integer | não | Regras: mínimo 0. |
| `delivery_days_min` | integer | não | Regras: mínimo 0. |
| `delivery_days_max` | integer | não | Regras: mínimo 0. |

Campos fora desta lista são recusados.

Forma 2 (`fixed`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `type` | string | sim | Regras: sempre `fixed`. |
| `cost_cents` | integer | sim | Regras: mínimo 0. |
| `delivery_days_min` | integer | não | Regras: mínimo 0. |
| `delivery_days_max` | integer | não | Regras: mínimo 0. |

Campos fora desta lista são recusados.

### ProductInput

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `commission` | `Commission` | não | Veja o esquema `Commission`. |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `title` | string | sim | Regras: mín. 5 caracteres; máx. 200 caracteres. |
| `description` | string | não | Regras: máx. 5000 caracteres. |
| `brand` | string | não | Regras: máx. 80 caracteres. |
| `model` | string | não | Regras: máx. 80 caracteres. |
| `gtin` | string | não | EAN/GTIN. A melhor prova para comparar o mesmo produto entre lojas. Regras: formato `^(\d{8}\|\d{12,14})$`. |
| `category` | string | não | Caminho da categoria na sua loja, ex. `Calçados > Tênis > Corrida`. Regras: máx. 200 caracteres. |
| `attributes` | object | não | Ficha técnica (voltagem, capacidade, tamanho…). Só o que é verdade; nada estimado. Regras: máx. 50 chaves. |
| `condition` | string | não | Usado e recondicionado aparecem como alternativa rotulada, nunca como o produto novo pedido. Regras: um de: `new`, `used`, `refurbished`. |
| `group_id` | string | não | Mesmo valor em todas as variações do mesmo produto (cores, tamanhos). Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `variant_attributes` | object | não | O que distingue esta variação, ex. `{"cor":"preto","tamanho":"42"}`. Regras: máx. 5 chaves. |
| `product_url` | string | sim | Página do produto, num domínio aprovado da loja. Sem parâmetro de rastreio — o `indica_click` é acrescentado por nós. Regras: máx. 2048 caracteres; formato `^https://`; uri. |
| `image_urls` | lista de string | sim | 1 a 10 fotos públicas, JPEG/PNG/WebP, mínimo 500 px no menor lado. A primeira é a capa, a foto que o site mostra; é ela que o Indica confere (abre por HTTPS no domínio declarado, tipo e tamanho). Regras: mín. 1 itens; máx. 10 itens; cada item: máx. 2048 caracteres; cada item: formato `^https://`; cada item: uri. |
| `price_cents` | integer | sim | Preço à vista que a pessoa paga hoje, sem cupom. Regras: mínimo 1. |
| `original_price_cents` | integer | não | Preço "de". Só se foi praticado antes; maior que `price_cents`. Regras: mínimo 1. |
| `available` | boolean | sim |  |
| `stock_quantity` | integer | não | Regras: mínimo 0. |
| `shipping` | `Shipping` | não | Veja o esquema `Shipping`. |
| `installments` | object | não |  |
| `rating` | object | não | Avaliação sem quantidade não vale. |
| `updated_at` | string | sim | Quando o dado mudou do seu lado. Envio mais antigo que o último recebido é ignorado. Regras: date-time. |

Campos fora desta lista são recusados.

### ProductPatch

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `commission` | `Commission` | não | Veja o esquema `Commission`. |
| `price_cents` | integer | não | Regras: mínimo 1. |
| `original_price_cents` | integer ou null | não | `null` remove o preço "de". Regras: mínimo 1. |
| `available` | boolean | não |  |
| `stock_quantity` | integer | não | Regras: mínimo 0. |
| `shipping` | `Shipping` | não | Veja o esquema `Shipping`. |
| `updated_at` | string | sim | Regras: date-time. |

Campos fora desta lista são recusados.

### ProductState

string. `pending` em validação; `accepted` no ar; `rejected` recusado (veja `reasons`); `stale` sem atualização há mais de 48 h, fora da busca até o próximo envio; `removed` apagado. Regras: um de: `pending`, `accepted`, `rejected`, `stale`, `removed`.

### RejectionReason

`store_not_active` e `domain_not_approved` seguram o produto em
`pending` até a aprovação do Indica. `image_unreachable` (não abriu,
HTTP diferente de 200, redirecionou para fora dos domínios da loja),
`image_type` (não é JPEG, PNG nem WebP) e `image_too_small` (menor
lado abaixo de 500 px) recusam (`rejected`) até você corrigir a capa.
Preço muito abaixo das outras ofertas do mesmo produto não muda o
estado: a régua vale na busca, para todas as lojas (veja "Regras").

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `code` | string | sim | Regras: um de: `store_not_active`, `domain_not_approved`, `image_unreachable`, `image_too_small`, `image_type`. |
| `field` | string | não |  |
| `message` | string | sim |  |

### ProductStatus

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `commission` | `Commission` | não | Veja o esquema `Commission`. |
| `commission_source` | string ou null | não | Origem da regra efetiva; produto prevalece sobre loja. Regras: um de: `product`, `store`, `null`. |
| `expected_commission_cents` | integer ou null | não | Estimativa por unidade ao preço atual, sem frete. Não é receita de pedido pago. Regras: mínimo 0; máximo 9007199254740991. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `state` | string | sim | `pending` em validação; `accepted` no ar; `rejected` recusado (veja `reasons`); `stale` sem atualização há mais de 48 h, fora da busca até o próximo envio; `removed` apagado. Regras: um de: `pending`, `accepted`, `rejected`, `stale`, `removed`. |
| `ignored_stale` | boolean | não | `true` quando o envio tinha `updated_at` mais antigo que o último e foi ignorado. |
| `reasons` | lista de `RejectionReason` | não | Veja o esquema `RejectionReason`. |
| `received_at` | string | sim | Regras: date-time. |
| `published_at` | string ou null | não | Regras: date-time. |
| `indica_url` | string | não | Onde o produto aparece no Indica, quando aceito. Regras: máx. 2048 caracteres; formato `^https://`; uri. |

### BatchInput

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `items` | lista de uma das formas abaixo | sim | Regras: mín. 1 itens; máx. 1000 itens. |

Campos fora desta lista são recusados.

Cada item de `items` tem uma destas formas.

Forma 1 (`upsert`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `op` | string | sim | Regras: sempre `upsert`. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `product` | `ProductInput` | sim | Veja o esquema `ProductInput`. |

Campos fora desta lista são recusados.

Forma 2 (`delete`):

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `op` | string | sim | Regras: sempre `delete`. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |

Campos fora desta lista são recusados.

### Job

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `job_id` | string | sim |  |
| `state` | string | sim | Regras: um de: `queued`, `running`, `done`, `failed`. |
| `totals` | object | sim |  |
| `errors` | lista de object | não |  |
| `created_at` | string | sim | Regras: date-time. |
| `finished_at` | string ou null | não | Regras: date-time. |

### PartnerClick

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `click_id` | string | sim |  |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `price_cents` | integer ou null | sim | Regras: mínimo 0. |
| `occurred_at` | string | sim | Regras: date-time. |

### ClickPage

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `data` | lista de `PartnerClick` | sim | Veja o esquema `PartnerClick`. |
| `next_cursor` | string ou null | sim |  |
| `since` | string | sim | Regras: date-time. |
| `until` | string | sim | Regras: date-time. |

### ConversionStatus

string. Regras: um de: `paid`, `refunded`, `cancelled`.

### ConversionItem

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `sku` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `quantity` | integer | sim | Regras: mínimo 1; máximo 10000. |
| `unit_price_cents` | integer | sim | Valor líquido pago por unidade, já com descontos, sem frete. Regras: mínimo 0; máximo 1000000000. |
| `refunded_cents` | integer | sim | Estorno acumulado da linha inteira; até quantity vezes unit_price_cents. Regras: mínimo 0; máximo 1000000000000. |

Campos fora desta lista são recusados.

### ConversionEvent

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `event_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `order_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `store_id` | string | sim | Identificador do seu lado. Letras, números, `.`, `_` e `-`. Regras: formato `^[A-Za-z0-9._-]{1,80}$`. |
| `click_id` | string | sim | Regras: formato `^ic_[A-Za-z0-9_-]{16,64}$`. |
| `status` | string | sim | Regras: um de: `paid`, `refunded`, `cancelled`. |
| `currency` | string | sim | Regras: sempre `BRL`. |
| `paid_at` | string | sim | Pagamento original; imutável. Regras: date-time. |
| `occurred_at` | string | sim | Momento do evento. Em paid, igual a paid_at. Regras: date-time. |
| `items` | lista de `ConversionItem` | sim | SKUs únicos; total até 1 trilhão de centavos. Repita todos os itens originais nos estornos. Veja o esquema `ConversionItem`. Regras: mín. 1 itens; máx. 200 itens. |

Campos fora desta lista são recusados.

### CommissionSnapshotItem

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `sku` | string | sim |  |
| `quantity` | integer | sim |  |
| `unit_price_cents` | integer | sim | Regras: mínimo 0. |
| `refunded_cents` | integer | sim | Regras: mínimo 0. |
| `commission` | `Commission` | sim | Veja o esquema `Commission`. |
| `commission_source` | string ou null | sim | Regras: um de: `product`, `store`, `null`. |
| `expected_commission_cents` | integer ou null | sim | Regras: mínimo 0. |

### OrderView

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `event_id` | string | sim | Último evento aplicado ao saldo. |
| `order_id` | string | sim |  |
| `store_id` | string | sim |  |
| `click_id` | string | sim |  |
| `status` | string | sim | Regras: um de: `paid`, `refunded`, `cancelled`. |
| `currency` | string | sim | Regras: sempre `BRL`. |
| `paid_at` | string | sim | Regras: date-time. |
| `occurred_at` | string | sim | Regras: date-time. |
| `items` | lista de `CommissionSnapshotItem` | sim | Veja o esquema `CommissionSnapshotItem`. |
| `gross_cents` | integer | sim | Soma original dos produtos líquidos de desconto e sem frete. Regras: mínimo 0. |
| `refunded_cents` | integer | sim | Regras: mínimo 0. |
| `net_cents` | integer | sim | Regras: mínimo 0. |
| `expected_commission_cents` | integer ou null | sim | Regras: mínimo 0. |
| `known_expected_commission_cents` | integer | sim | Subtotal das linhas com regra conhecida. Regras: mínimo 0. |
| `commission_snapshot_at` | string | sim | Regra do catálogo no primeiro recebimento de paid; não reconstrói termos históricos. Regras: date-time. |
| `recorded_at` | string | sim | Regras: date-time. |
| `updated_at` | string | sim | Regras: date-time. |

### ConversionReceipt

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `event_id` | string | sim | Último evento aplicado ao saldo. |
| `order_id` | string | sim |  |
| `store_id` | string | sim |  |
| `click_id` | string | sim |  |
| `status` | string | sim | Regras: um de: `paid`, `refunded`, `cancelled`. |
| `currency` | string | sim | Regras: sempre `BRL`. |
| `paid_at` | string | sim | Regras: date-time. |
| `occurred_at` | string | sim | Regras: date-time. |
| `items` | lista de `CommissionSnapshotItem` | sim | Veja o esquema `CommissionSnapshotItem`. |
| `gross_cents` | integer | sim | Soma original dos produtos líquidos de desconto e sem frete. Regras: mínimo 0. |
| `refunded_cents` | integer | sim | Regras: mínimo 0. |
| `net_cents` | integer | sim | Regras: mínimo 0. |
| `expected_commission_cents` | integer ou null | sim | Regras: mínimo 0. |
| `known_expected_commission_cents` | integer | sim | Subtotal das linhas com regra conhecida. Regras: mínimo 0. |
| `commission_snapshot_at` | string | sim | Regra do catálogo no primeiro recebimento de paid; não reconstrói termos históricos. Regras: date-time. |
| `recorded_at` | string | sim | Regras: date-time. |
| `updated_at` | string | sim | Regras: date-time. |
| `duplicate` | boolean | sim |  |
| `ignored_stale` | boolean | sim |  |

### ConversionPage

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `data` | lista de `OrderView` | sim | Veja o esquema `OrderView`. |
| `next_cursor` | string ou null | sim |  |
| `since` | string | sim | Regras: date-time. |
| `until` | string | sim | Regras: date-time. |

### CommercialTotals

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `clicks` | integer | sim | Regras: mínimo 0. |
| `orders` | integer | sim | Regras: mínimo 0. |
| `paid_orders` | integer | sim | Regras: mínimo 0. |
| `refunded_orders` | integer | sim | Regras: mínimo 0. |
| `cancelled_orders` | integer | sim | Regras: mínimo 0. |
| `gross_cents` | integer | sim | Regras: mínimo 0. |
| `refunded_cents` | integer | sim | Regras: mínimo 0. |
| `net_cents` | integer | sim | Regras: mínimo 0. |
| `unknown_commission_orders` | integer | sim | Regras: mínimo 0. |
| `known_expected_commission_cents` | integer | sim | Regras: mínimo 0. |
| `expected_commission_cents` | integer ou null | sim | Regras: mínimo 0. |

### CommissionReport

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `since` | string | sim | Regras: date-time. |
| `until` | string | sim | Regras: date-time. |
| `currency` | string | sim | Regras: sempre `BRL`. |
| `environment` | string | sim | Regras: um de: `test`, `live`. |
| `basis` | string | sim | Regras: sempre `paid_at_current_state`. |
| `commission_is` | string | sim | Regras: sempre `expected_not_settled`. |
| `totals` | `CommercialTotals` | sim | Veja o esquema `CommercialTotals`. |
| `stores` | lista de qualquer | sim |  |

### Problem

RFC 9457.

| Campo | Tipo | Obrigatório | Observações |
| --- | --- | --- | --- |
| `type` | string | sim |  |
| `title` | string | sim |  |
| `status` | integer | sim |  |
| `detail` | string | não |  |
| `request_id` | string | não |  |
| `errors` | lista de object | não |  |

## Em breve

Descritas no contrato para você planejar, mas ainda NÃO existem: não chame. Ficam disponíveis por fase, com aviso.

- `GET /promotions/{promotion_id}` (fase 3) — Consulta uma promoção
- `PUT /promotions/{promotion_id}` (fase 3) — Cria ou substitui uma promoção de preço com período
- `DELETE /promotions/{promotion_id}` (fase 3) — Encerra a promoção agora
- `GET /coupons/{code}` (fase 3) — Consulta um cupom
- `PUT /coupons/{code}` (fase 3) — Cria ou substitui um cupom
- `DELETE /coupons/{code}` (fase 3) — Desativa o cupom agora
- `GET /campaigns/{campaign_id}` (fase 3) — Consulta uma campanha
- `PUT /campaigns/{campaign_id}` (fase 3) — Cria ou substitui uma campanha da vitrine
- `DELETE /campaigns/{campaign_id}` (fase 3) — Encerra a campanha agora
