Documentação

API Indik — Manual de Integração

Baixar OpenAPI

ou importe pela URL https://indik.app.br/docs/api/openapi.yaml

Índice

Versão v1 · Base: https://indik.app.br/api/v1 · Formato: JSON · Idioma das mensagens: pt-BR

API REST para o ERP/PDV do comerciante operar o programa de indicações de dentro do próprio sistema: validar, reservar e resgatar cupons, cadastrar clientes e ler os indicadores que aparecem no painel.

Referência formal (importável no Postman/Insomnia): openapi.yaml.


Contexto

Sem a API, validar um cupom exige que o operador abra /painel/validar numa segunda tela, paralela ao caixa. Com ela, o fluxo inteiro cabe dentro do ERP.

Um ponto importante de arquitetura: a API e o painel compartilham o mesmo código de regra de negócio (CouponRedemptionService, MetricsService, ReferralLinker). Um cupom resgatado pela API produz exatamente os mesmos efeitos que um resgatado no painel — inclusive a confirmação da indicação e a emissão do cupom de recompensa do indicador.


1. Obtendo a chave

No painel, o dono da loja acessa Conta → Integrações (/painel/integracoes), clica em Nova chave, dá um nome (ex.: PDV Loja Centro) e escolhe as permissões.

⚠️ A chave é exibida uma única vez

O servidor guarda apenas o hash SHA-256 do segredo. Se você perder a chave, não há como recuperá-la — só gerar uma nova e revogar a antiga.

Crie uma chave por integração, com o menor conjunto de permissões que ela precisa. Assim, se um sistema for descomissionado ou tiver a credencial exposta, você revoga só aquela chave sem derrubar as demais.

Na criação você ainda define duas restrições opcionais:

Restrição O que faz
Validade A chave para de funcionar na data escolhida (30, 90, 180 dias ou 1 ano). Limita o estrago de um vazamento que passe despercebido. Sem prazo, ela vale até alguém revogar.
Endereços autorizados A chave só é aceita a partir dos IPs ou faixas CIDR informados. Peça o endereço de saída a quem cuida do seu sistema. Chamada de outra origem recebe 403 ip_not_allowed.

Você também acompanha, na mesma tela, as chamadas recentes daquela loja — rota, resultado, chave usada e origem — para conferir se a integração está se comportando como esperado.


2. Autenticação

Toda requisição leva o token no header Authorization:

Authorization: Bearer indk_live_8c17d1238ad36c3f.5ZqL1HQvNkonJ3UM9pIWxj4tXrZNyEn7zcjcsXMI

O token tem duas partes separadas por ponto: o prefixo (público, aparece na listagem do painel para você identificar qual chave é qual) e o segredo.

Teste a credencial antes de qualquer coisa:

curl https://indik.app.br/api/v1/ping \
  -H "Authorization: Bearer $INDIK_TOKEN"
{
  "data": {
    "tenant": { "slug": "minha-loja", "name": "Minha Loja", "status": "active" },
    "api_key": { "name": "PDV Loja Centro", "prefix": "indk_live_8c17d1238ad36c3f", "scopes": ["coupons.read", "coupons.write"] },
    "server_time": "2026-08-13T19:36:44+00:00"
  }
}

Escopos

Cada chave carrega uma lista de permissões. Conceda só o que o ERP realmente usa.

Escopo Libera
coupons.read Consultar cupons e cupons de um cliente
coupons.write Reservar, resgatar e liberar cupons
customers.read Listar e consultar clientes
customers.write Cadastrar clientes
referrals.read Consultar indicações
campaigns.read Consultar campanhas e lojas
metrics.read Consultar indicadores
* Todos os escopos acima

3. Envelope de resposta

Sucesso — sempre dentro de data; listas paginadas trazem também meta:

{ "data": { "...": "..." } }
{ "data": [ ... ], "meta": { "page": 1, "per_page": 25, "total": 87, "last_page": 4 } }

Erro — sempre dentro de error:

{
  "error": {
    "code": "coupon_already_redeemed",
    "message": "Este cupom já foi utilizado.",
    "details": {}
  }
}

code é o contrato estável — programe suas condições contra ele. message é texto em pt-BR, pronto para exibir ao operador do caixa, e pode ser reescrito a qualquer momento sem aviso.

Códigos de erro

code HTTP Quando acontece
unauthenticated 401 Header Authorization ausente ou malformado
invalid_api_key 401 Chave inexistente, revogada, expirada ou com segredo errado
subscription_inactive 402 Assinatura da loja cancelada ou vencida fora da carência
insufficient_scope 403 A chave não tem o escopo exigido pelo endpoint
ip_not_allowed 403 A chave tem endereços autorizados e a chamada veio de outro
not_found 404 Recurso inexistente (ou de outra loja)
coupon_not_found 404 Nenhum cupom com esse código nesta loja
coupon_already_redeemed 409 Cupom já utilizado
coupon_expired 409 Cupom vencido ou marcado como expirado
coupon_unavailable 409 Cupom em estado incompatível com a operação
coupon_not_reserved 409 Tentou liberar um cupom que não estava reservado
below_min_purchase 422 Valor da venda abaixo da compra mínima da campanha
validation_failed 422 Corpo/parâmetros inválidos (veja details)
idempotency_conflict 409 Mesma Idempotency-Key reenviada com corpo diferente
idempotency_in_progress 409 Requisição com essa Idempotency-Key ainda em execução
rate_limited 429 Limite de requisições estourado
method_not_allowed 405 Verbo HTTP errado para a rota
server_error 500 Falha interna — pode repetir

Em validation_failed, details traz os campos e as mensagens:

{
  "error": {
    "code": "validation_failed",
    "message": "Os dados enviados são inválidos.",
    "details": { "accepted_terms": ["É obrigatório registrar o aceite dos termos do programa pelo cliente."] }
  }
}

4. Limites e idempotência

Rate limiting

Escopo do limite Padrão Contado por
Geral 120 requisições/minuto chave de API
Mutações de cupom e cadastro (POST) 30 requisições/minuto chave de API
Tentativas de autenticação recusadas 10 a cada 5 minutos endereço IP

Os dois primeiros limites são por chave, não por IP — vários caixas da mesma loja saem pelo mesmo endereço e não podem consumir a cota uns dos outros. Ao estourar, a resposta é 429 com os headers Retry-After e X-RateLimit-*.

O terceiro protege contra tentativa de adivinhação de credencial e é contado por IP, porque uma requisição recusada não tem chave para contabilizar. Uma autenticação bem-sucedida zera o contador, então errar o token uma vez e corrigir não deixa o caixa bloqueado.

⚠️ Trate o 429 como espera, não como erro

Respeite o Retry-After antes de repetir. Um retry imediato em laço só empurra a janela para frente e mantém a integração parada por mais tempo.

Idempotência

Todo POST aceita o header opcional (mas fortemente recomendado) Idempotency-Key:

Idempotency-Key: venda-2026-08-13-000123
  • Reenviar a mesma chave com o mesmo corpo devolve a resposta original, com o header Idempotency-Replayed: true. O cupom não é resgatado duas vezes.
  • Reenviar a mesma chave com corpo diferente retorna 409 idempotency_conflict.
  • A janela de replay é de 24 horas.

Use um identificador da própria venda no ERP (número do cupom fiscal, ID do pedido). Isso transforma o retry automático da sua camada de rede numa operação segura.

⚠️ Concorrência

Cada mutação roda em transação com lock de linha. Duas chamadas simultâneas para resgatar o mesmo cupom nunca resultam em dois resgates — a segunda recebe coupon_already_redeemed.


5. Fluxo recomendado no caixa

                    ┌──────────────┐
   cliente          │  available   │◄──────────────┐
   apresenta        └──────┬───────┘               │
   o cupom                 │                       │ POST /release
        │          POST /reserve                   │ (venda cancelada)
        │                  ▼                       │
   GET /coupons/{code}  ┌──────────────┐───────────┘
   (validar, sem        │  reserved    │
    efeito colateral)   └──────┬───────┘
                               │ POST /redeem  { "sale_value": 150.00 }
                               ▼
                        ┌──────────────┐
                        │  redeemed    │  ← estado final
                        └──────────────┘

Duas etapas (recomendado para PDV): reserve ao iniciar a venda, resgate no fechamento, libere se o cliente desistir. Evita que o mesmo cupom seja usado em dois caixas ao mesmo tempo.

Uma etapa: chame POST /redeem direto num cupom available — a reserva acontece implícita. Bom para integrações simples ou pós-venda.

⚠️ Sempre envie sale_value

Em cupons percentuais, é o valor da venda que determina o desconto real (prize_value = sale_value × value / 100). Sem ele, o cupom é resgatado mas os indicadores de ticket médio, custo de campanha e CAC ficam sem base de cálculo.


6. Referência dos endpoints

GET /ping

Verifica conectividade e credencial. Não exige escopo. Ver exemplo na seção 2.


GET /coupons/{code} — consultar cupom

Escopo: coupons.read. Sem efeito colateral: é o "validar" do caixa. O código não diferencia maiúsculas de minúsculas.

curl https://indik.app.br/api/v1/coupons/ABC12345 \
  -H "Authorization: Bearer $INDIK_TOKEN"
{
  "data": {
    "code": "ABC12345",
    "status": "available",
    "type": "percent",
    "origin": "welcome",
    "value": 10,
    "value_label": "10% OFF",
    "product": null,
    "sale_value": null,
    "prize_value": null,
    "campaign_id": 1,
    "customer_id": 26,
    "referral_id": 23,
    "store_id": null,
    "seller_id": 6,
    "min_purchase": 50,
    "customer": { "id": 26, "name": "Ana", "phone": "11900000002", "email": null },
    "expires_at": "2026-09-02T14:36:50+00:00",
    "reserved_at": null,
    "redeemed_at": null,
    "created_at": "2026-08-03T14:36:50+00:00"
  }
}
Campo Observação
status available · reserved · redeemed · expired
type percent · fixed · cashback · product
origin welcome (boas-vindas) · referral (recompensa do indicador) · milestone (meta) · manual
value Percentual quando type = percent; valor em R$ nos demais
value_label Rótulo pronto para exibir ao operador ("10% OFF", "R$ 30,00 OFF")
min_purchase Compra mínima da campanha, se houver — valide antes de fechar a venda

Erros: coupon_not_found (404), coupon_already_redeemed (409), coupon_expired (409).


POST /coupons/{code}/reserve — reservar

Escopo: coupons.write. Sem corpo. Só funciona a partir de available.

curl -X POST https://indik.app.br/api/v1/coupons/ABC12345/reserve \
  -H "Authorization: Bearer $INDIK_TOKEN" \
  -H "Idempotency-Key: venda-000123-reserva"

Devolve o cupom com status: "reserved" e reserved_at preenchido.


POST /coupons/{code}/redeem — resgatar

Escopo: coupons.write. Aceita cupom reserved ou available.

Campo Tipo Obrigatório Observação
sale_value número ≥ 0 não (mas recomendado) Valor total da venda em R$
store_id inteiro não Loja do resgate; só é gravado se o cupom ainda não tiver loja
seller_id inteiro não Vendedor do resgate; mesma regra
curl -X POST https://indik.app.br/api/v1/coupons/ABC12345/redeem \
  -H "Authorization: Bearer $INDIK_TOKEN" \
  -H "Idempotency-Key: venda-000123" \
  -H "Content-Type: application/json" \
  -d '{"sale_value": 150.00, "store_id": 3}'
{
  "data": {
    "code": "ABC12345",
    "status": "redeemed",
    "type": "percent",
    "value": 10,
    "sale_value": 150,
    "prize_value": 15,
    "redeemed_at": "2026-08-13T19:36:59+00:00"
  }
}

prize_value é o desconto efetivo em R$ — para percent, calculado a partir de sale_value; para os demais tipos, igual a value.

⚠️ Efeito colateral importante

Resgatar um cupom de boas-vindas (origin: "welcome") confirma a indicação que o gerou: a indicação passa a used e o indicador recebe automaticamente o cupom de recompensa (e o de meta, se bateu um milestone). É o mesmo comportamento de /painel/validar.

Erros: coupon_not_found (404), coupon_already_redeemed (409), coupon_expired (409), below_min_purchase (422, com details.min_purchase), validation_failed (422).


POST /coupons/{code}/release — liberar reserva

Escopo: coupons.write. Sem corpo. Devolve um cupom reserved para available — use quando a venda é cancelada, senão o cupom fica preso.

Erros: coupon_not_reserved (409), coupon_already_redeemed (409).


GET /customers — listar clientes

Escopo: customers.read.

Parâmetro Observação
phone Aceita formatado ((11) 98888-7777) — comparação por dígitos
cpf Aceita formatado (390.533.447-05)
email Comparação exata
q Busca livre em nome, telefone e e-mail
include_removed true inclui clientes removidos do programa
page, per_page per_page máximo 100 (padrão 25)
curl "https://indik.app.br/api/v1/customers?phone=11988887777" \
  -H "Authorization: Bearer $INDIK_TOKEN"

GET /customers/{id} — detalhe do cliente

Escopo: customers.read. Inclui os cupons do cliente.

{
  "data": {
    "id": 26,
    "name": "Ana",
    "phone": "11900000002",
    "email": null,
    "cpf": null,
    "invite_token": "S17hEMaLzABq5vvRHePtE908d2nzXjTX",
    "invite_url": "https://indik.app.br/minha-loja?invite=S17hEMaLzABq5vvRHePtE908d2nzXjTX",
    "referrer_id": 25,
    "store_id": null,
    "seller_id": null,
    "accepted_terms_at": "2026-07-27T20:26:36+00:00",
    "removed_at": null,
    "created_at": "2026-07-31T20:26:36+00:00",
    "coupons": [ ... ]
  }
}

invite_url é o link pronto para o cliente compartilhar e indicar amigos — imprima no cupom fiscal ou mande por WhatsApp direto do ERP.


POST /customers — cadastrar cliente

Escopo: customers.write.

Campo Tipo Obrigatório Observação
name texto (≤100) não
phone 10–11 dígitos sim, se não houver email Só dígitos, com DDD
email e-mail sim, se não houver phone
cpf 11 dígitos não
accepted_terms booleano sim Deve ser true — LGPD
store_id inteiro não Loja de origem do cadastro
seller_id inteiro não Vendedor que cadastrou
referrer_invite_token texto não Quem indicou, pelo token do link de convite
referrer_phone texto não Quem indicou, pelo telefone
referrer_cpf texto não Quem indicou, pelo CPF
curl -X POST https://indik.app.br/api/v1/customers \
  -H "Authorization: Bearer $INDIK_TOKEN" \
  -H "Idempotency-Key: cadastro-cpf-39053344705" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Joana Silva",
        "phone": "11988887777",
        "cpf": "39053344705",
        "accepted_terms": true,
        "referrer_phone": "11900000002"
      }'

Retorna 201 quando o cliente é criado e 200 quando ele já participava do programa — meta.created diferencia os dois casos, e meta.referral_id traz a indicação aberta (ou null).

Se houver indicador válido e campanha ativa, a resposta já vem com o cupom de boas-vindas dentro de data.coupons:

{
  "data": {
    "id": 31,
    "name": "Joana Silva",
    "coupons": [
      { "code": "RZSY2428", "status": "available", "type": "fixed", "origin": "welcome", "value": 20, "value_label": "R$ 20,00 OFF" }
    ]
  },
  "meta": { "created": true, "referral_id": 23 }
}

⚠️ Sem OTP, com responsabilidade

Diferente do PWA, o cadastro via API não passa por verificação por SMS — o ERP é um canal confiável. Em compensação, accepted_terms é obrigatório: você está afirmando que colheu o aceite dos termos do programa junto ao cliente. O identificador global da pessoa continua sendo o telefone (ou o e-mail), então o mesmo cliente cadastrado em duas lojas Indik é reconhecido como a mesma pessoa.


GET /customers/{id}/coupons — cupons de um cliente

Escopo: coupons.read. Aceita status (available · reserved · redeemed · expired) e paginação.


GET /referrals — listar indicações

Escopo: referrals.read.

Parâmetro Observação
status pending · used · expired
campaign_id, referrer_id Filtros por campanha e por indicador
from, to Datas (YYYY-MM-DD) sobre a criação da indicação
page, per_page
{
  "data": [
    {
      "id": 23,
      "status": "used",
      "campaign_id": 1,
      "referrer": { "id": 26, "name": "Ana", "phone": "11900000002" },
      "referred": { "id": 31, "name": "Joana Silva", "phone": "11988887777" },
      "referred_contact": { "name": null, "phone": null, "email": null },
      "coupon": { "code": "UQFHBGPO", "value": 50, "origin": "referral", "status": "available" },
      "created_at": "2026-08-13T19:37:26+00:00"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 1, "last_page": 1 }
}

referred é o cliente já cadastrado; referred_contact traz os dados soltos de um convite direto (SMS/e-mail) que ainda não virou cadastro.


GET /campaigns — listar campanhas

Escopo: campaigns.read. Parâmetro active_only=1 restringe à campanha vigente.

Use este endpoint para ler min_purchase e bloquear a venda no ERP antes mesmo de chamar o resgate.


GET /stores — listar lojas

Escopo: campaigns.read. Devolve id, name, address, phone, is_active — a fonte dos store_id que você envia no resgate e no cadastro.


GET /metrics — indicadores

Escopo: metrics.read. Os mesmos números de /painel/dashboard, em formato cru.

Parâmetro Observação
period 7d · 30d (padrão) · 90d
from, to Intervalo explícito (YYYY-MM-DD), máximo 366 dias; ignora period
campaign_id Restringe a uma campanha

Cada métrica traz o valor do período, o do período imediatamente anterior e a variação:

{
  "data": {
    "period": {
      "start": "2026-07-15T00:00:00+00:00",
      "end": "2026-08-13T23:59:59+00:00",
      "previous_start": "2026-06-15T00:00:00+00:00",
      "previous_end": "2026-07-14T23:59:59+00:00",
      "days": 30
    },
    "metrics": {
      "referrals": { "current": 2, "previous": 0, "delta": 100, "up": true },
      "conversion_rate": { "current": 50, "previous": 0, "delta": 100, "up": true }
    },
    "series": { "referrals_daily": [0, 1, 0, ...] },
    "active_campaign": { "id": 1, "name": "Campanha Demo", "progress": 12, "goal": 100 },
    "usage": [ { "key": "coupons_included", "label": "Cupons este mês", "used": 12, "limit": 500, "state": "ok" } ]
  }
}
Métrica Significado
referrals Indicações criadas no período
converted_referrals Indicações confirmadas (status used)
conversion_rate converted_referrals / referrals (%)
redeemed_coupons Cupons resgatados
redeemed_value / campaign_cost Custo em R$ dos cupons resgatados
participants Total de clientes cadastrados no programa
unique_referrers Clientes que fizeram ao menos 1 indicação no período
penetration_rate unique_referrers / participants (%)
viral_effect Indicações por indicador (multiplicador)
influence_rate converted_referrals / unique_referrers (%)
redemption_rate redeemed_coupons / converted_referrals (%)
sales_value Soma dos sale_value informados nos resgates
avg_ticket sales_value / redeemed_coupons
cac_per_referral campaign_cost / converted_referrals

As séries em series têm um ponto por dia do período, na ordem cronológica: referrals_daily, converted_daily, pending_daily, redeemed_daily.


7. Erros comuns na integração

Sintoma Causa provável
401 invalid_api_key logo após criar a chave Token copiado pela metade — ele tem prefixo e segredo separados por ponto
402 subscription_inactive Assinatura da loja vencida; regularize em /painel/planos
403 insufficient_scope no resgate A chave tem coupons.read mas não coupons.write
403 ip_not_allowed depois de migrar de servidor O IP de saída mudou; atualize os endereços autorizados no painel
401 invalid_api_key numa chave que sempre funcionou A validade expirou — gere uma nova em Integrações
404 coupon_not_found para um cupom que existe O cupom é de outra loja — cada chave enxerga apenas o próprio tenant
409 coupon_already_redeemed no retry Faltou Idempotency-Key: a primeira chamada funcionou e você não viu a resposta
422 below_min_purchase A campanha exige compra mínima; leia min_purchase em GET /coupons/{code}
prize_value nulo depois do resgate sale_value não foi enviado
Indicador não recebeu a recompensa O cupom resgatado não era de boas-vindas, ou a indicação já estava confirmada

8. Checklist de homologação

  1. GET /ping responde 200 com o slug correto da loja.
  2. GET /coupons/{code} de um cupom real devolve status: "available".
  3. POST /reservePOST /release volta o cupom para available.
  4. POST /redeem com sale_value grava prize_value corretamente.
  5. Repetir o redeem com a mesma Idempotency-Key devolve Idempotency-Replayed: true e não resgata de novo.
  6. POST /customers com referrer_phone cria o cliente e o cupom de boas-vindas.
  7. Resgatar esse cupom de boas-vindas confirma a indicação no painel e premia o indicador.
  8. GET /metrics?period=30d bate com /painel/dashboard no mesmo período.
  9. Uma chave revogada no painel passa a responder 401 imediatamente.