Endpoints PDV/ERP
Download da spec OpenAPI · Versão JSON · Swagger UI Interativo
Endpoints destinados à integração de sistemas de caixa (PDV) e ERP. Todos utilizam autenticação via X-Api-Key.
URL base de produção: https://api.elofiel.com.br
URL base de sandbox: https://api-sandbox.elofiel.com.br
Header obrigatório em todas as chamadas:
X-Api-Key: SUA_API_KEY_AQUI
Content-Type: application/json
POST /api/v1/venda
Registra uma venda e credita o benefício de fidelidade ao cliente.
Request
Body: VendaPdvRequest
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cnpjLoja | string | Sim | CNPJ do estabelecimento (com ou sem formatação) |
cpfCliente | string | Sim | CPF do cliente (com ou sem formatação) |
valorVenda | number | Sim | Valor total da venda em R$ (decimal > 0) |
documentoExterno | string | Não | Identificador do PDV/ERP (ex.: número do cupom fiscal, máx. 100 chars) |
observacao | string | Não | Observação livre (máx. 500 chars); null é aceito |
cliente | ClienteAutoRequest | Não | Dados para auto-cadastro do cliente se o CPF ainda não existir |
Sub-objeto cliente (opcional — para auto-cadastro):
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cliente.nome | string | Sim (se cliente presente) | Nome completo (mín. 2, máx. 150 chars) |
cliente.email | string | Não | E-mail do cliente |
cliente.telefone | string | Não | Telefone do cliente |
Se o CPF já existir no sistema, o objeto cliente é ignorado — o cadastro existente é mantido. Se o CPF não existir e cliente não for enviado, o cliente é criado sem nome.
Exemplo (com auto-cadastro):
{
"cnpjLoja": "12.345.678/0001-90",
"cpfCliente": "123.456.789-09",
"valorVenda": 150.00,
"documentoExterno": "PDV-001234",
"observacao": null,
"cliente": {
"nome": "Maria Silva",
"email": "maria@email.com",
"telefone": "(44) 99999-0000"
}
}
Response
HTTP 200 — ApiResult<VendaResponse>
{
"sucesso": true,
"mensagem": "Venda registrada com sucesso.",
"dados": {
"vendaId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"clienteId": "8e9f1234-abcd-4321-efgh-000000000001",
"clienteNome": "Maria Silva",
"valorVenda": 150.00,
"beneficiosAplicados": [
{
"movimentacaoId": "aaaaaaaa-1111-2222-3333-444444444444",
"campanhaId": "cccccccc-aaaa-bbbb-dddd-eeeeeeeeeeee",
"campanhaDescricao": "Cashback 5% Verão",
"tipoBeneficio": "Cashback",
"pontosCreditados": 0,
"cashbackCreditado": 7.50,
"statusBeneficio": "Disponivel",
"dataLiberacao": null,
"dataExpiracao": "2026-09-29"
},
{
"movimentacaoId": "bbbbbbbb-5555-6666-7777-888888888888",
"campanhaId": "ffffffff-1111-2222-3333-444444444444",
"campanhaDescricao": "1 ponto por real",
"tipoBeneficio": "Pontos",
"pontosCreditados": 150,
"cashbackCreditado": 0,
"statusBeneficio": "Disponivel",
"dataLiberacao": null,
"dataExpiracao": "2027-05-20"
}
],
"pontosCreditados": 150,
"cashbackCreditado": 7.50,
"saldoAtual": {
"saldoPontos": 150,
"saldoCashback": 22.50,
"saldoPendente": 0.00
}
},
"erros": []
}
Campos da resposta (dados):
| Campo | Tipo | Descrição |
|---|---|---|
vendaId | uuid | ID agrupador da venda. Compartilhado por todos os elementos de beneficiosAplicados[]; usado pelo cancelamento bulk |
clienteId | uuid | ID do cliente |
clienteNome | string | null | Nome do cliente (null se criado sem nome) |
valorVenda | number | Valor bruto da venda registrado |
beneficiosAplicados | BeneficioAplicado[] | Lista de 1 ou 2 benefícios aplicados (max 1 Cashback + 1 Pontos por loja — issue #3) |
pontosCreditados | number | Agregado — soma dos pontos creditados em todas as campanhas |
cashbackCreditado | number | Agregado — soma do cashback creditado em todas as campanhas |
cashbackUsado | number | Modelo 2 — cashback do saldo anterior usado como desconto. 0 quando usarCashbackDisponivel: false |
descontoGerado | number | Modelo 3 — cashback gerado por esta venda e aplicado como desconto imediato. 0 quando usarCashbackGerado: false |
valorFinal | number | Valor líquido pago: valorVenda − cashbackUsado − descontoGerado |
saldoAtual | SaldoResponse | Saldo atualizado do cliente após a venda |
Cada elemento de beneficiosAplicados:
| Campo | Tipo | Descrição |
|---|---|---|
movimentacaoId | uuid | ID único da movimentação gerada por esta campanha |
campanhaId | uuid | ID da campanha que gerou este benefício |
campanhaDescricao | string | Nome da campanha |
tipoBeneficio | string | "Cashback" ou "Pontos" |
pontosCreditados | number | Pontos por esta campanha (zero se Cashback) |
cashbackCreditado | number | Cashback em R$ por esta campanha (zero se Pontos) |
statusBeneficio | string | "Disponivel" ou "Pendente" |
dataLiberacao | date | null | Data de liberação do cashback diferido |
dataExpiracao | date | null | Data de expiração do lote |
A partir de 2026-05-20, cada loja pode operar com até 1 campanha de Cashback + 1 de Pontos simultaneamente. Cada venda elegível aplica TODAS as campanhas elegíveis, gerando 1 elemento em beneficiosAplicados[] por campanha. Cancelar uma venda cancela TODOS os benefícios dela (bulk forçado).
cashbackUsado e descontoGerado são mutuamente exclusivos — no máximo um será > 0 em cada resposta. valorFinal reflete sempre o valor efetivamente cobrado do cliente.
Campos do request — desconto (novos):
| Campo | Tipo | Padrão | Descrição |
|---|---|---|---|
usarCashbackDisponivel | boolean | false | Modelo 2 — aplica o saldo de cashback acumulado como desconto. Requer permiteCheckoutCashbackNaVenda: true na configuração |
usarCashbackGerado | boolean | false | Modelo 3 — aplica o cashback que esta venda geraria como desconto imediato. Requer permiteDescontoSobreGerado: true na configuração. Chamar POST /api/v1/venda/preview antes para exibir o valor ao cliente |
usarCashbackDisponivel e usarCashbackGerado não podem ser true ao mesmo tempo. A API retorna 422 se ambos forem enviados como true.
Possíveis erros:
| HTTP | Causa |
|---|---|
| 400 | Campo obrigatório ausente, formato inválido ou cliente.nome em branco quando cliente é enviado |
| 401 | X-Api-Key inválida ou ausente |
| 422 | usarCashbackDisponivel e usarCashbackGerado ambos true |
| 500 | Erro interno |
POST /api/v1/venda/preview
Calcula o desconto previsto do Modelo 3 (usarCashbackGerado) sem gravar nada. Use este endpoint para exibir o valor do desconto ao cliente antes de confirmar a venda.
/venda/preview não cria movimentações, não altera saldo e não tem efeitos colaterais. Pode ser chamado quantas vezes necessário — inclusive quando o cliente ainda está decidindo.
Request
Body: VendaPreviewRequest
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cnpjLoja | string | Sim | CNPJ do estabelecimento (com ou sem formatação) |
valorVenda | number | Sim | Valor total da venda em R$ (decimal > 0) |
Exemplo:
{
"cnpjLoja": "12.345.678/0001-90",
"valorVenda": 200.00
}
Response
HTTP 200 — ApiResult<VendaPreviewResponse>
{
"sucesso": true,
"mensagem": "Preview calculado com sucesso.",
"dados": {
"valorVenda": 200.00,
"permiteAplicarDesconto": true,
"motivoBloqueio": null,
"cashbackGeradoPrevisto": 20.00,
"valorComDesconto": 180.00,
"cashbackSobreDesconto": 18.00
}
}
Quando o desconto não está disponível (campanha sem Modelo 3, valor abaixo do mínimo, etc.):
{
"sucesso": true,
"dados": {
"valorVenda": 200.00,
"permiteAplicarDesconto": false,
"motivoBloqueio": "Campanha não permite uso do cashback gerado como desconto imediato.",
"cashbackGeradoPrevisto": 0,
"valorComDesconto": 0,
"cashbackSobreDesconto": 0
}
}
Campos da resposta (dados):
| Campo | Tipo | Descrição |
|---|---|---|
permiteAplicarDesconto | boolean | true = desconto disponível; false = não aplicar (ver motivoBloqueio) |
motivoBloqueio | string | null | Explicação quando permiteAplicarDesconto: false. null quando desconto está disponível |
valorVenda | number | Valor bruto informado no request |
cashbackGeradoPrevisto | number | Valor que será descontado (cashback que esta venda geraria). 0 se bloqueado |
valorComDesconto | number | Valor que o cliente pagará após o desconto. 0 se bloqueado |
cashbackSobreDesconto | number | Novo cashback gerado sobre valorComDesconto quando permiteCashbackEmResgate: true na campanha. 0 quando permiteCashbackEmResgate: false |
Quando a campanha tem permiteCashbackEmResgate: false, o novo cashback é calculado sobre o valorVenda original — não sobre valorComDesconto. O campo cashbackSobreDesconto retorna 0 neste caso; o cliente ainda acumula cashback, mas o valor real só é conhecido após confirmar a venda.
Possíveis erros:
| HTTP | Causa |
|---|---|
| 400 | Campo obrigatório ausente ou valorVenda <= 0 |
| 401 | X-Api-Key inválida |
| 422 | CNPJ inválido ou loja não encontrada |
GET /api/v1/resgate/saldo
Consulta o saldo de cashback e pontos de um cliente.
Request
Query parameters:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cnpjLoja | string | Sim | CNPJ do estabelecimento |
cpfCliente | string | Sim | CPF do cliente |
Exemplo:
GET /api/v1/resgate/saldo?cnpjLoja=12.345.678%2F0001-90&cpfCliente=123.456.789-09
Response
HTTP 200 — ApiResult<ClienteSaldoResponse>
{
"sucesso": true,
"dados": {
"clienteId": "8e9f1234-abcd-4321-efgh-000000000001",
"clienteNome": "Maria Silva",
"saldoPontos": 150,
"saldoCashback": 22.50,
"saldoPendente": 5.00,
"lotesAtivos": [
{
"id": "lote-uuid-001",
"tipoBeneficio": "Cashback",
"valorOriginal": 10.00,
"valorDisponivel": 7.50,
"status": "Disponivel",
"dataLiberacao": null,
"dataExpiracao": "2026-09-29"
},
{
"id": "lote-uuid-002",
"tipoBeneficio": "Cashback",
"valorOriginal": 5.00,
"valorDisponivel": 5.00,
"status": "Pendente",
"dataLiberacao": "2026-04-10",
"dataExpiracao": "2026-10-07"
}
]
},
"mensagem": null,
"erros": []
}
Campos principais:
| Campo | Tipo | Descrição |
|---|---|---|
clienteId | uuid | ID do cliente |
clienteNome | string | Nome do cliente |
saldoPontos | number | Pontos disponíveis para resgate |
saldoCashback | number | Cashback em R$ disponível (liberado) |
saldoPendente | number | Cashback diferido bloqueado (não disponível) |
lotesAtivos | LoteResponse[] | Detalhamento por lote de crédito |
Campos de cada lote (lotesAtivos[]):
| Campo | Tipo | Descrição |
|---|---|---|
id | uuid | ID do lote |
tipoBeneficio | string | "Cashback" ou "Pontos" |
valorOriginal | number | Valor original creditado no lote |
valorDisponivel | number | Saldo atual disponível neste lote |
status | string | "Disponivel", "Pendente", "Resgatado" ou "Expirado" |
dataLiberacao | date | null | Data de liberação (apenas lotes pendentes) |
dataExpiracao | date | null | Data de expiração do lote |
Para a maioria dos PDVs, os campos saldoCashback e saldoPontos são suficientes. Os lotesAtivos são úteis para exibir detalhes ao cliente (ex.: "R$ 5,00 disponível até set/2026").
Possíveis erros:
| HTTP | Causa |
|---|---|
| 400 | Parâmetro ausente |
| 401 | X-Api-Key inválida |
| 404 | Cliente não encontrado para o tenant |
POST /api/v1/resgate
Registra o resgate de cashback ou pontos como desconto.
Request
Body: ResgatePdvRequest
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cnpjLoja | string | Sim | CNPJ do estabelecimento |
cpfCliente | string | Sim | CPF do cliente |
tipoBeneficio | string | Sim | "Cashback" ou "Pontos" |
valorResgate | number | null | Não | Valor em R$ a resgatar (Cashback). null = resgatar tudo disponível |
pontosResgate | number | null | Não | Quantidade de pontos a resgatar. null = resgatar tudo disponível |
documentoExterno | string | Não | Identificador do PDV (máx. 100 chars) |
observacao | string | Não | Observação livre (máx. 500 chars) |
descricaoTroca | string | Não | Descrição do brinde ou produto entregue ao cliente (máx. 200 chars). Somente para tipoBeneficio: "Pontos" — exibido no histórico do app clube. Ignorado para Cashback. |
Exemplo — resgate parcial de cashback:
{
"cnpjLoja": "12.345.678/0001-90",
"cpfCliente": "123.456.789-09",
"tipoBeneficio": "Cashback",
"valorResgate": 10.00,
"pontosResgate": null,
"documentoExterno": "PDV-001235",
"observacao": null
}
Response
HTTP 200 — ApiResult<ResgateResponse>
{
"sucesso": true,
"mensagem": "Resgate realizado com sucesso.",
"dados": {
"movimentacaoId": "7ab12345-1234-5678-9012-abcdef000001",
"clienteId": "8e9f1234-abcd-4321-efgh-000000000001",
"clienteNome": "Maria Silva",
"tipoBeneficio": "Cashback",
"pontosResgatados": 0,
"cashbackResgatado": 10.00,
"saldoAtual": {
"saldoPontos": 0,
"saldoCashback": 12.50,
"saldoPendente": 5.00
}
},
"erros": []
}
Possíveis erros:
| HTTP | Causa | erros[0] |
|---|---|---|
| 400 | Campo obrigatório ausente | "O campo TipoBeneficio é obrigatório." |
| 401 | X-Api-Key inválida | "Não autorizado." |
| 422 | Saldo insuficiente | "Saldo insuficiente para resgate." |
| 422 | Mínimo não atingido | "Cashback mínimo não atingido." |
Erros 422 indicam violação de regra de negócio (saldo insuficiente, mínimo não atingido). A requisição está correta sintaticamente — reenviar sem alterar os dados resultará no mesmo erro.
POST /api/v1/cliente
Cadastra um novo cliente no programa de fidelidade. Útil para pré-registrar o cliente no balcão ou totem antes de qualquer venda.
Request
Body: CadastrarClientePdvRequest
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cpf | string | Sim | CPF do cliente (com ou sem formatação, 11-14 chars) |
nome | string | Sim | Nome completo (mín. 2, máx. 150 chars) |
email | string | Sim | E-mail válido (máx. 150 chars). Enviar "" quando não disponível |
telefone | string | Sim | Telefone (máx. 20 chars). Enviar "" quando não disponível |
dataNascimento | string | Não | Data de nascimento no formato YYYY-MM-DD |
aceiteLgpd | boolean | Sim | Deve ser true. Recusa resulta em erro 422 |
Se o CPF já estiver cadastrado, o endpoint retorna os dados existentes sem modificá-los e sinaliza com jaExistia: true. É seguro chamar múltiplas vezes.
Exemplo:
{
"cpf": "123.456.789-09",
"nome": "Maria Silva",
"email": "maria@email.com",
"telefone": "(44) 99999-0000",
"dataNascimento": "1990-05-20",
"aceiteLgpd": true
}
Response
HTTP 201 — Cliente novo cadastrado HTTP 200 — CPF já cadastrado (retorna dados existentes)
{
"sucesso": true,
"mensagem": "Cliente cadastrado com sucesso.",
"dados": {
"clienteId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"cpf": "12345678909",
"nome": "Maria Silva",
"email": "maria@email.com",
"telefone": "44999990000",
"jaExistia": false
},
"erros": []
}
| Campo | Tipo | Descrição |
|---|---|---|
clienteId | uuid | ID do cliente no EloFiel |
cpf | string | CPF normalizado (apenas dígitos) |
jaExistia | boolean | true se o CPF já estava cadastrado (dados não foram alterados) |
Possíveis erros:
| HTTP | Causa |
|---|---|
422 | aceiteLgpd: false |
422 | CPF inválido |
400 | Nome ausente ou fora do limite de tamanho |
GET /api/v1/cliente/buscar
Consulta um cliente já cadastrado na plataforma EloFiel por CPF ou QR Code. Útil quando o operador do PDV está atendendo o cliente e quer recuperar nome, e-mail e telefone para preencher o atendimento sem digitação.
A consulta busca na base global do clube EloFiel — retorna dados cadastrais de qualquer cliente da plataforma, não apenas dos já vinculados à sua loja. O vínculo per-tenant é materializado automaticamente na primeira venda (POST /api/v1/venda).
Este endpoint não cria nenhum registro nem altera estado. É 100% leitura.
Request
Query string — exatamente um dos parâmetros:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cpf | string | Excludente com qr | CPF do cliente (com ou sem formatação) |
qr | uuid | Excludente com cpf | QrToken do cliente (Guid) |
Exemplos:
GET /api/v1/cliente/buscar?cpf=12345678909
GET /api/v1/cliente/buscar?cpf=123.456.789-09
GET /api/v1/cliente/buscar?qr=e2e00000-0000-0000-0001-000000000002
Response
HTTP 200 — ApiResult<ClienteBuscaResponse>
{
"sucesso": true,
"mensagem": null,
"dados": {
"cpf": "123.456.789-09",
"nome": "Maria Silva",
"email": "maria@email.com",
"telefone": "44999990000",
"dataNascimento": "1990-05-20",
"whatsappJid": "5544999990000@s.whatsapp.net",
"cadastradoEm": "2025-08-12T10:00:00Z"
},
"erros": []
}
| Campo | Tipo | Descrição |
|---|---|---|
cpf | string | CPF formatado (xxx.xxx.xxx-xx) |
nome | string | Nome completo do cliente |
email | string? | E-mail cadastral (pode ser null) |
telefone | string? | Telefone cadastral (pode ser null) |
dataNascimento | string? | Data de nascimento (YYYY-MM-DD, pode ser null) |
whatsappJid | string? | Número verificado no WhatsApp (pode ser null) |
cadastradoEm | string | Timestamp do cadastro global do cliente (ISO 8601 UTC) |
O endpoint retorna apenas dados cadastrais. Saldo, cashback, pontos e histórico de compras não atravessam tenants — são segregados por estabelecimento e acessíveis apenas via GET /api/v1/resgate/saldo e endpoints autenticados por JWT do operador.
Possíveis erros:
| HTTP | Causa |
|---|---|
401 | X-Api-Key ausente ou inválido |
404 | CPF ou QrToken não existe na plataforma |
422 | Nem cpf nem qr informados, ou ambos informados simultaneamente |
422 | CPF inválido (DV incorreto) |
422 | qr não é um UUID — tipicamente o CPF mandado no parâmetro errado |
429 | Rate limit estourado |
qr não é o CPFO parâmetro qr espera o QR Token (UUID gerado pelo app do clube), não o CPF do cliente. Mandar ?qr=12345678909 retorna 422 com a mensagem indicando o formato esperado. Para consultar por CPF use ?cpf=.
Rate limiting
Defesa em duas camadas para inviabilizar varredura de CPFs:
| Camada | Quota | Particionado por |
|---|---|---|
| Limite por IP do integrador | 60 req/min | IP de origem |
| Limite por CPF (filtro do issue #389) | 3 req/h | CPF normalizado |
O limite por CPF aplica-se apenas quando a consulta usa cpf — consultas por qr ficam protegidas só pelo limite por IP, já que o QrToken é um Guid opaco e enumeração é inviável.
Exemplo: curl
curl -X GET "https://api.elofiel.com.br/api/v1/cliente/buscar?cpf=12345678909" \
-H "X-Api-Key: $ELOFIEL_API_KEY"
Exemplo: Delphi (TNetHTTPClient)
var
Client: TNetHTTPClient;
Response: IHTTPResponse;
Cpf: string;
begin
Cpf := '12345678909';
Client := TNetHTTPClient.Create(nil);
try
Client.CustHeaders['X-Api-Key'] := ApiKey;
Response := Client.Get(
Format('https://api.elofiel.com.br/api/v1/cliente/buscar?cpf=%s', [Cpf]));
case Response.StatusCode of
200: // parse JSON, preencher tela com nome/email/telefone
;
404: // cliente novo — caminho de cadastro
;
429: // recuar / backoff
;
end;
finally
Client.Free;
end;
end;
GET /api/v1/configuracao
Retorna o resumo da configuração de fidelidade para a loja. Deve ser chamado uma única vez na inicialização do PDV/ERP para adaptar a UI ao tipo de campanha ativa.
Request
Query parameters:
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
cnpjLoja | string | Sim | CNPJ do estabelecimento |
Exemplo:
GET /api/v1/configuracao?cnpjLoja=12.345.678%2F0001-90
Response
HTTP 200 — ApiResult<ConfiguracaoResponse>
{
"sucesso": true,
"dados": {
"cnpjLoja": "12345678000190",
"nomeLoja": "Loja Central",
"campanhaAtiva": true,
"tipoCampanha": "Cashback",
"permiteCheckoutCashbackNaVenda": true,
"pontosMinimosParaResgate": null,
"permiteUsoParcialDePontos": null
}
}
Campos da resposta (dados):
| Campo | Tipo | Descrição |
|---|---|---|
campanhaAtiva | boolean | true se há campanha ativa para a loja |
tipoCampanha | string | null | "Cashback" ou "Pontos". null quando não há campanha ativa |
permiteCheckoutCashbackNaVenda | boolean | Modelo 2 — true = exibir opção de usar cashback acumulado como desconto na venda |
permiteDescontoSobreGerado | boolean | Modelo 3 — true = exibir opção de usar o cashback desta venda como desconto imediato. Chamar POST /api/v1/venda/preview para calcular o valor antes de confirmar |
pontosMinimosParaResgate | number | null | Mínimo de pontos para resgate. null para campanhas Cashback |
permiteUsoParcialDePontos | boolean | null | true = resgate parcial de pontos permitido. null para campanhas Cashback |
Possíveis erros:
| HTTP | Causa |
|---|---|
422 | CNPJ inválido ou loja não encontrada/inativa |