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
429como espera, não como erroRespeite o
Retry-Afterantes 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_valueEm 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 ausede 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 |
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
GET /pingresponde 200 com o slug correto da loja.GET /coupons/{code}de um cupom real devolvestatus: "available".POST /reserve→POST /releasevolta o cupom paraavailable.POST /redeemcomsale_valuegravaprize_valuecorretamente.- Repetir o
redeemcom a mesmaIdempotency-KeydevolveIdempotency-Replayed: truee não resgata de novo. POST /customerscomreferrer_phonecria o cliente e o cupom de boas-vindas.- Resgatar esse cupom de boas-vindas confirma a indicação no painel e premia o indicador.
GET /metrics?period=30dbate com/painel/dashboardno mesmo período.- Uma chave revogada no painel passa a responder
401imediatamente.