Manual completo para integração do PDV com o sistema de autorização assíncrona Elo365.
POST /convenio/refund (reverte uma transação já confirmada — ver seção 10).mode=qr).POST /convenio/authorize/{id}/cancel (libera o saldo reservado na hora — ver seção 10).external_sale_id e items[].cancelled no polling.O módulo de Convênio Corporativo permite que colaboradores realizem compras em estabelecimentos credenciados com desconto em folha de pagamento. A autorização é feita de forma assíncrona via QR Code, confirmada pelo colaborador no aplicativo Elo365 usando um PIN pessoal — sem necessidade de cartão físico.
status: "expired".
Todas as requisições do PDV usam um token JWT emitido pela plataforma Elo365, com o escopo convenio:authorize. O token pode ser enviado em qualquer um destes headers (escolha um):
# Enviar o token em UM destes headers Authorization: Bearer <token_da_loja> # ── ou ── X-Customtoken: <token_da_loja> # ── ou ── X-Convenio-Token: <token_da_loja> # Sempre com: Content-Type: application/json
O token é gerado pela equipe Elo365 (ou por um usuário do tenant com as permissões RBAC 10002 — Criar Tokens API — e 27008 — Emitir Token PDV de Convênio). Ele carrega o escopo convenio:authorize e está vinculado a uma pessoa/loja do tenant; o tenant (organização) é resolvido a partir do próprio token — não pelo slug da URL. Guarde o token com segurança; ele pode ser revogado/rotacionado a qualquer momento pela Elo365.
O token identifica a loja (e, consequentemente, a organização). Todos os terminais de uma mesma loja compartilham o mesmo token. O campo pdv_id no body de cada requisição identifica qual terminal específico realizou a operação.
| Granularidade | Identificador | Como informar |
|---|---|---|
| Organização / Tenant | Resolvido automaticamente pelo token | Implícito no Bearer token |
| Loja | 1 token por loja | Bearer token (emitido pela Elo365) |
| Terminal / Caixa | pdv_id | Campo no body de cada requisição |
pdv_id ("caixa-01", "caixa-02"…) para rastreabilidade nas transações.
Endpoint: GET /api/convenio/authorize/poll/:polling_token
Chamar a cada 2 segundos enquanto aguarda a confirmação do colaborador.
GET /api/convenio/authorize/poll/a3f9c2e1d4b5a6f7...64chars
Authorization: Bearer <pdv_token>
● PENDING Aguardando confirmação do colaborador
{
"success": true,
"status": "pending",
"expiresInSeconds": 142
}
✓ APPROVED Compra autorizada — campos estruturados + cupom pronto para impressão
{
"success": true,
"status": "approved",
// ── Campos estruturados (padrão TEF) ──────────────────────
"cod_retorno": "0",
"msg_retorno": "TRANSACAO APROVADA",
"tipo_pagamento": "CONVENIO CORPORATIVO",
"valor": 150.00,
"nsu": "654321",
"cod_autorizacao": "654321",
"data": "13/03/2026",
"hora": "14:52:33",
"parcelas": 1,
"doc_fiscal": "000123",
// ── Valores detalhados (camelCase) ─────────────────────────
"authorizationCode": "654321",
"transactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"grossAmount": 150.00,
"subsidyAmount": 6.00,
"netAmountPayroll": 144.00,
// ── 1ª via — imprimir e solicitar assinatura do colaborador
"via_estabelecimento": [
"----------------------------------------",
" COMPROVANTE CONVENIO CORPORATIVO ",
"----------------------------------------",
"LOJA: AYUMI SUPERMERCADOS",
"CNPJ: 12.345.678/0001-99",
"TERMINAL: CAIXA-02",
"DATA: 13/03/2026 HORA: 14:52:33",
"",
"COLABORADOR: JOAO SILVA",
"",
"CONVENIO CORPORATIVO",
"VALOR BRUTO: R$ 150,00",
"SUBSIDIO: R$ 6,00",
"DESC. FOLHA: R$ 144,00",
"PARCELAMENTO: A VISTA",
"",
"NSU: 654321",
"AUTORIZACAO: 654321",
"DOC: 000123",
"",
"TRANSACAO APROVADA",
"",
"ESTABELECIMENTO",
"CLIENTE: ______________________________",
"",
"ASSINATURA: ___________________________",
"----------------------------------------",
" OBRIGADO PELA PREFERENCIA ",
"----------------------------------------"
],
// ── 2ª via — entregar ao colaborador ──────────────────────
"via_cliente": [
"----------------------------------------",
" COMPROVANTE CONVENIO CORPORATIVO ",
"----------------------------------------",
"LOJA: AYUMI SUPERMERCADOS",
"CNPJ: 12.345.678/0001-99",
"TERMINAL: CAIXA-02",
"DATA: 13/03/2026 HORA: 14:52:33",
"",
"COLABORADOR: JOAO SILVA",
"",
"CONVENIO CORPORATIVO",
"VALOR BRUTO: R$ 150,00",
"SUBSIDIO: R$ 6,00",
"DESC. FOLHA: R$ 144,00",
"PARCELAMENTO: A VISTA",
"",
"NSU: 654321",
"AUTORIZACAO: 654321",
"DOC: 000123",
"",
"TRANSACAO APROVADA",
"",
"CLIENTE",
"----------------------------------------",
" VIA DO CLIENTE ",
"----------------------------------------"
]
}
cod_retorno, msg_retorno, nsu, cod_autorizacao, data, hora, parcelas, doc_fiscal) e as vias (via_estabelecimento, via_cliente) vêm em snake_case. Já os campos de detalhe da transação vêm em camelCase: authorizationCode, transactionId, grossAmount, subsidyAmount, netAmountPayroll. Faça o parsing pelos nomes exatos acima.
× REJECTED Colaborador recusou a compra
{
"success": true,
"status": "rejected",
"reason": "Recusado pelo colaborador"
}
✖ EXPIRED Tempo esgotado (3 minutos)
{
"success": true,
"status": "expired",
"reason": "Tempo de autorização expirado"
}
🔒 PIN_BLOCKED 3 tentativas de PIN incorretas
{
"success": true,
"status": "pin_blocked",
"reason": null
}
🚫 CANCELLED O próprio PDV cancelou a reserva pendente (ver seção 10)
{
"success": true,
"status": "cancelled",
"reason": "Cancelado pelo PDV"
}
approved, rejected, expired, pin_blocked, cancelled): o loop de polling pode ser encerrado ao receber qualquer um deles.approved); em qualquer outro estado final (rejected, expired, pin_blocked, cancelled) o saldo reservado é devolvido automaticamente.
Pseudocódigo independente de linguagem:
// ── 1. Solicitar autorização ────────────────────────────────────────── resposta = POST /api/convenio/authorize?mode=qr { employee_identifier, gross_amount, store_id, idempotency_key, ... } se resposta.success == falso: exibir_erro(resposta.denial_message) retornar // ── 2. Exibir QR Code ───────────────────────────────────────────────── exibir_imagem_base64(resposta.qr_image_base64) exibir_texto("Aguardando confirmação de " + resposta.employee_name) iniciar_countdown(resposta.expires_at) pollingToken = resposta.polling_token // guardar internamente // ── 3. Loop de polling (a cada 2 segundos) ──────────────────────────── enquanto verdadeiro: aguardar(2000ms) poll = GET /api/convenio/authorize/poll/{pollingToken} se poll.status == "pending": atualizar_countdown(poll.expiresInSeconds) continuar se poll.status == "approved": imprimir_linhas(poll.via_estabelecimento) // 1ª via — assinatura do colaborador cortar_papel() imprimir_linhas(poll.via_cliente) // 2ª via — entregue ao colaborador cortar_papel() exibir_sucesso("Autorizado! Código: " + poll.authorizationCode) finalizar_venda(poll.netAmountPayroll) retornar se poll.status == "rejected": exibir_aviso("Compra recusada pelo colaborador.") retornar se poll.status == "expired": exibir_aviso("Tempo esgotado. Solicite novamente.") retornar se poll.status == "pin_blocked": exibir_aviso("PIN bloqueado. Oriente o colaborador a contatar o RH.") retornar se poll.status == "cancelled": // reserva foi cancelada (por este PDV) — encerrar retornar // Se o operador cancelar a venda antes da confirmação: // POST /api/convenio/authorize/{resposta.pending_auth_id}/cancel → libera o saldo reservado
import java.net.http.*; import java.net.URI; import java.time.*; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class ConvenioQrClient { private static final String BASE_URL = "https://www.elo365.com.br/api"; private static final String PDV_TOKEN = System.getenv("PDV_TOKEN"); private final HttpClient http = HttpClient.newHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); /** 1. Solicita autorização e retorna o payload completo */ public JsonNode requestQrAuthorization(String cpf, double grossAmount, String pdvId) throws Exception { String body = String.format( "{\"employee_identifier\":\"%s\",\"gross_amount\":%.2f,\"pdv_id\":\"%s\",\"idempotency_key\":\"%s-%d\"}", cpf, grossAmount, pdvId, pdvId, System.currentTimeMillis() ); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/convenio/authorize?mode=qr")) .header("Authorization", "Bearer " + PDV_TOKEN) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString()); JsonNode json = mapper.readTree(resp.body()); if (!json.path("success").asBoolean()) { throw new RuntimeException("Negado: " + json.path("denial_message").asText()); } return json; } /** 2. Loop de polling — retorna o nó JSON do resultado final */ public JsonNode pollUntilDone(String pollingToken) throws Exception { HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/convenio/authorize/poll/" + pollingToken)) .header("Authorization", "Bearer " + PDV_TOKEN) .GET() .build(); while (true) { Thread.sleep(2000); HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString()); JsonNode poll = mapper.readTree(resp.body()); String status = poll.path("status").asText(); switch (status) { case "pending" -> atualizarCountdown(poll.path("expiresInSeconds").asInt()); case "approved" -> { return poll; } case "rejected" -> throw new RuntimeException("Recusado pelo colaborador."); case "expired" -> throw new RuntimeException("Tempo esgotado. Tente novamente."); case "pin_blocked" -> throw new RuntimeException("PIN bloqueado. Colaborador deve contatar o RH."); case "cancelled" -> throw new RuntimeException("Reserva cancelada."); default -> throw new RuntimeException("Status inesperado: " + status); } } } /** 3. Orquestra o fluxo completo */ public void autorizarViaQr(String cpf, double valor, String pdvId) throws Exception { JsonNode auth = requestQrAuthorization(cpf, valor, pdvId); exibirQrCode(auth.path("qr_image_base64").asText()); exibirTexto("Aguardando: " + auth.path("employee_name").asText()); JsonNode resultado = pollUntilDone(auth.path("polling_token").asText()); imprimirLinhas(resultado.path("via_estabelecimento")); cortarPapel(); imprimirLinhas(resultado.path("via_cliente")); cortarPapel(); finalizarVenda(resultado.path("netAmountPayroll").asDouble()); } private void atualizarCountdown(int segundos) { /* atualizar UI */ } private void exibirQrCode(String base64) { /* renderizar imagem */ } private void exibirTexto(String msg) { /* atualizar label */ } private void imprimirCupom(JsonNode linhas) { /* enviar para impressora */ } private void finalizarVenda(double valor) { /* encerrar a venda no PDV */ } }
using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; public class ConvenioQrClient { private static readonly string BASE_URL = "https://www.elo365.com.br/api"; private static readonly string PDV_TOKEN = Environment.GetEnvironmentVariable("PDV_TOKEN")!; private readonly HttpClient _http; public ConvenioQrClient() { _http = new HttpClient(); _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PDV_TOKEN); } /// 1. Solicita autorização e retorna o JSON completo public async Task<JsonObject> RequestQrAuthorizationAsync( string cpf, decimal grossAmount, string pdvId) { var idempKey = $"{pdvId}-{DateTime.UtcNow:yyyyMMddHHmmss}"; var payload = JsonSerializer.Serialize(new { employee_identifier = cpf, gross_amount = grossAmount, pdv_id = pdvId, idempotency_key = idempKey }); var resp = await _http.PostAsync( BASE_URL + "/convenio/authorize?mode=qr", new StringContent(payload, Encoding.UTF8, "application/json")); resp.EnsureSuccessStatusCode(); var json = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!.AsObject(); if (json["success"]?.GetValue<bool>() != true) throw new Exception($"Negado: {json["denial_message"]}"); return json; } /// 2. Loop de polling — retorna o JSON do resultado final public async Task<JsonObject> PollUntilDoneAsync(string pollingToken) { while (true) { await Task.Delay(2000); var resp = await _http.GetAsync($"{BASE_URL}/convenio/authorize/poll/{pollingToken}"); var poll = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!.AsObject(); var status = poll["status"]?.GetValue<string>(); switch (status) { case "pending": AtualizarCountdown(poll["expiresInSeconds"]?.GetValue<int>() ?? 0); break; case "approved": return poll; case "rejected": throw new Exception("Recusado pelo colaborador."); case "expired": throw new Exception("Tempo esgotado. Tente novamente."); case "pin_blocked": throw new Exception("PIN bloqueado. Colaborador deve contatar o RH."); case "cancelled": throw new Exception("Reserva cancelada."); default: throw new Exception($"Status inesperado: {status}"); } } } /// 3. Orquestra o fluxo completo public async Task AutorizarViaQrAsync(string cpf, decimal valor, string pdvId) { var auth = await RequestQrAuthorizationAsync(cpf, valor, pdvId); ExibirQrCode(auth["qr_image_base64"]!.GetValue<string>()); ExibirTexto($"Aguardando: {auth["employee_name"]}"); var resultado = await PollUntilDoneAsync(auth["polling_token"]!.GetValue<string>()); ImprimirLinhas(resultado["via_estabelecimento"]!.AsArray()); CortarPapel(); ImprimirLinhas(resultado["via_cliente"]!.AsArray()); CortarPapel(); FinalizarVenda(resultado["netAmountPayroll"]!.GetValue<decimal>()); } private void AtualizarCountdown(int segundos) { /* atualizar UI */ } private void ExibirQrCode(string base64) { /* renderizar imagem */ } private void ExibirTexto(string msg) { /* atualizar label */ } private void ImprimirCupom(JsonArray linhas) { /* enviar para impressora */ } private void FinalizarVenda(decimal valor) { /* encerrar a venda no PDV */ } }
O campo qr_image_base64 contém uma imagem PNG 256×256 pixels em Base64 com prefixo data:image/png;base64,. Pode ser exibida diretamente nas plataformas mais comuns:
<img src="data:image/png;base64,iVBORw0KGgo..." width="256" height="256" alt="QR Code Convênio" />
import base64, io from PIL import Image data = response["qr_image_base64"] b64 = data.split(",")[1] # remover prefixo img = Image.open(io.BytesIO(base64.b64decode(b64))) img.show() # ou exibir no widget
String b64 = qrImageBase64.replace("data:image/png;base64,", ""); byte[] imageBytes = Base64.getDecoder().decode(b64); ImageIcon icon = new ImageIcon(imageBytes); // Swing // — ou — BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageBytes));
string b64 = qrImageBase64.Replace("data:image/png;base64,", ""); byte[] imageBytes = Convert.FromBase64String(b64); using var ms = new MemoryStream(imageBytes); Bitmap bitmap = new Bitmap(ms); pictureBox1.Image = bitmap; // WinForms
qr_payload (a URL do deep link elo365://convenio/auth?t=...&h=...) com qualquer biblioteca de QR Code disponível no seu ambiente.
Exemplo ilustrativo da tela do PDV
Quando o status for APPROVED, o response contém as duas vias prontas para impressão — basta iterar linha a linha:
// Pseudocódigo imprimir_linhas(poll.via_estabelecimento) // 1ª via — solicitar assinatura cortar_papel() imprimir_linhas(poll.via_cliente) // 2ª via — entregar ao colaborador cortar_papel()
1ª VIA — Estabelecimento
2ª VIA — Cliente
Armazene em variáveis de ambiente ou cofre de segredos. Nunca no código-fonte ou logs. Rotacione periodicamente.
Todas as requisições devem usar HTTPS. Nunca HTTP em produção.
Mínimo 2 segundos entre chamadas. Não faça polling mais frequente do que isso.
Sempre envie idempotency_key único por venda. Isso evita cobranças duplicadas em caso de retentativa por falha de rede.
Não exiba o polling_token na tela nem em logs. É para uso interno do PDV apenas.
Configure timeout de 10 segundos nas requisições HTTP. Em caso de timeout, aguarde 3s e tente novamente (com mesmo idempotency_key).
Se o operador cancelar a venda no PDV, chame o endpoint de cancelamento (abaixo) para liberar o saldo reservado imediatamente e pare o polling. Se não chamar, o servidor libera automaticamente na expiração (3 min).
Endpoint: POST /api/convenio/authorize/{pending_auth_id}/cancel
Use quando o operador cancelar a operação antes de o colaborador confirmar (enquanto o status ainda é pending). Libera o saldo que havia sido reservado. O {pending_auth_id} é o campo pending_auth_id retornado na resposta da autorização.
POST /api/convenio/authorize/f47ac10b-58cc-4372-a567-0e02b2c3d479/cancel Authorization: Bearer <pdv_token> Content-Type: application/json { "reason": "Operador cancelou a venda" }
O campo reason é opcional (default: "Cancelado pelo PDV").
{ "success": true, "status": "cancelled" }
{ "success": false, "error": "Autorização não está pendente (status atual: approved)." }
/cancel) só cancela reservas ainda pending (antes da confirmação do colaborador). Para desfazer uma venda já aprovada, use o endpoint de estorno abaixo.
Endpoint: POST /api/convenio/refund
Reverte uma transação já aprovada (approved): devolve o limite ao colaborador, gera o lançamento de crédito e marca a transação como cancelled. Use quando o cliente desiste depois de já ter confirmado a compra no app.
POST /api/convenio/refund Authorization: Bearer <pdv_token> Content-Type: application/json { "external_sale_id": "VENDA-2026071401-0042", "reason": "Cliente desistiu da compra", "operator_id": "operador-joao", "idempotency_key": "estorno-VENDA-2026071401-0042" }
Informe transaction_id (o transactionId retornado no polling aprovado) ou external_sale_id (o id que você enviou na autorização). Campos reason, operator_id e idempotency_key são opcionais — recomendamos idempotency_key para tornar a retentativa segura.
{
"success": true,
"cancelled": true,
"already_done": false,
"data": {
"success": true,
"transactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"authorizationCode": "654321",
"grossAmount": "150.00",
"netAmountPayroll": "144.00",
"receiptLines": [
"CANCELAMENTO - Auth: 654321",
"Valor bruto: R$ 150.00",
"Valor em folha (revertido): R$ 144.00"
],
"previousAvailableLimit": 356.00,
"currentAvailableLimit": 500.00
}
}
idempotency_key, a resposta volta com cancelled: false e already_done: true — o estorno não é aplicado duas vezes.
| HTTP | Corpo | Quando |
|---|---|---|
| 400 | { "error": "Informe transaction_id ou external_sale_id." } | Nenhum identificador informado |
| 404 | { "error": "Transação não encontrada..." } | transaction_id/external_sale_id não localizado no seu tenant |
| 409 | { "error": "Transação não está aprovada ou já foi cancelada." } | A venda não está mais approved |
| 409 | { "error": "COMPETENCE_CLOSED", "message": "..." } | A competência da venda já foi fechada/exportada para a folha — o estorno precisa ser tratado pela retaguarda (RH), não pelo PDV |
convenio:authorize) do fluxo de autorização — nenhum token adicional é necessário.
| Código | Significado | Ação recomendada |
|---|---|---|
| 200 | Sucesso — verificar campo success no JSON | Processar resposta normalmente |
| 400 | Parâmetros inválidos (ex.: employee_identifier/gross_amount ausentes, ou requisição sem mode=qr) | Corrigir os dados enviados |
| 401 | Token do PDV ausente, inválido ou expirado | Renovar o token JWT junto à Elo365 |
| 403 | Token sem o escopo convenio:authorize | Solicitar um token com o escopo correto à Elo365 |
| 404 | Polling token / autorização não encontrada | Encerrar polling, mostrar timeout |
| 409 | Conflito no estorno (/convenio/refund): transação não está mais approved, ou competência já fechada (COMPETENCE_CLOSED) | Não reenviar; tratar estorno pela retaguarda (RH) |
| 500 | Erro interno do servidor | Aguardar 5s e tentar novamente (max 3x) |
A equipe Elo365 fornece, sob demanda, um tenant de teste com:
convenio:authorize);Como exercitar os cenários (todos com dados reais do tenant de teste):
| Cenário | Como reproduzir |
|---|---|
| Aprovação | O colaborador de teste confirma no app com o PIN correto |
| Rejeição | O colaborador recusa no app → status rejected |
| PIN bloqueado | Errar o PIN 3× → status pin_blocked |
| Expiração | Não confirmar em 3 minutos → status expired |
| Cancelamento | Chamar POST /convenio/authorize/{id}/cancel enquanto pending → status cancelled |
curl -X POST "https://www.elo365.com.br/api/convenio/authorize?mode=qr" \ -H "Authorization: Bearer SEU_TOKEN_PDV" \ -H "Content-Type: application/json" \ -d '{ "employee_identifier": "12345678900", "gross_amount": 150.00, "store_id": "loja-01", "pdv_id": "caixa-02", "idempotency_key": "caixa02-20260313-000042" }'
curl "https://www.elo365.com.br/api/convenio/authorize/poll/SEU_POLLING_TOKEN" \ -H "Authorization: Bearer SEU_TOKEN_PDV"
curl -X POST "https://www.elo365.com.br/api/convenio/authorize/SEU_PENDING_AUTH_ID/cancel" \ -H "Authorization: Bearer SEU_TOKEN_PDV" \ -H "Content-Type: application/json" \ -d '{ "reason": "Operador cancelou a venda" }'
curl -X POST "https://www.elo365.com.br/api/convenio/refund" \ -H "Authorization: Bearer SEU_TOKEN_PDV" \ -H "Content-Type: application/json" \ -d '{ "external_sale_id": "VENDA-2026071401-0042", "reason": "Cliente desistiu da compra", "idempotency_key": "estorno-VENDA-2026071401-0042" }'
| Tipo de Contato | Canal |
|---|---|
| Dúvidas de integração | integracoes@elo365.com.br |
| Incidentes em produção | suporte@elo365.com.br |