🔗 Guia de Integração Técnica

Convênio Corporativo via QR Code

Manual completo para integração do PDV com o sistema de autorização assíncrona Elo365.

📄 Versão: 1.2 📅 Data: 2026-07-14 👥 Público: Equipe técnica do PDV 🌐 Protocolo: HTTPS · REST · JSON

1 Visão Geral

🆕
Novidades da v1.2:
  • Novo endpoint de estorno de venda aprovada pelo PDV: POST /convenio/refund (reverte uma transação já confirmada — ver seção 10).
🆕
Novidades da v1.1 (alinhamento com a implementação atual):
  • O fluxo síncrono (sem QR) foi descontinuado — hoje só existe o fluxo QR assíncrono (mode=qr).
  • Novo endpoint cancelar reserva pendente: POST /convenio/authorize/{id}/cancel (libera o saldo reservado na hora — ver seção 10).
  • A autorização reserva o saldo do colaborador no momento do QR (reserva em duas fases). Se a venda não for concluída, cancele para liberar.
  • Novos campos opcionais no request: external_sale_id e items[].
  • Autenticação aceita 3 headers alternativos e novo estado final 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.

1
PDV envia dados do colaborador e valor da compra
2
Servidor valida elegibilidade e retorna QR Code + polling token
3
PDV exibe o QR Code na tela e inicia o loop de polling
4
Colaborador escaneia o QR com o app Elo365 Mobile
5
App exibe detalhes da compra → colaborador digita o PIN
6
PDV recebe aprovação, imprime cupom e finaliza a venda
ℹ️
TTL de 3 minutos: O colaborador tem até 3 minutos para escanear o QR e confirmar com PIN. Após esse prazo, a autorização expira automaticamente e o PDV recebe status: "expired".

2 Autenticação do PDV

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

Como o token é emitido

O token é gerado pela equipe Elo365 (ou por um usuário do tenant com as permissões RBAC 10002Criar Tokens API — e 27008Emitir 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.

Um token por loja — não por terminal

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.

GranularidadeIdentificadorComo informar
Organização / TenantResolvido automaticamente pelo tokenImplícito no Bearer token
Loja1 token por lojaBearer token (emitido pela Elo365)
Terminal / Caixapdv_idCampo no body de cada requisição
Uma loja com 50 terminais usa 1 único token. Cada terminal envia seu próprio pdv_id ("caixa-01", "caixa-02"…) para rastreabilidade nas transações.
⚠️
Segurança: Armazene o token da loja em variável de ambiente ou cofre de segredos — nunca no código-fonte. Rotacione periodicamente e solicite um novo token à equipe Elo365 em caso de suspeita de comprometimento.

3 Solicitar Autorização via QR Code

Endpoint: POST /api/convenio/authorize?mode=qr

Base URL: https://www.elo365.com.br  —  a API não leva o slug da organização no caminho: ela é identificada pelo token do PDV enviado no header Authorization. Exemplo completo: https://www.elo365.com.br/api/convenio/authorize?mode=qr

Request Body

{
  "employee_identifier": "123.456.789-00",  // CPF do colaborador
  "gross_amount":        150.00,             // Valor bruto da compra
  "store_id":            "aea2bab7-a938-4f16-9875-d776c1ce8a33",  // UUID da loja (opcional)
  "pdv_id":              "caixa-03",         // ID do caixa (opcional)
  "operator_id":         "operador-joao",    // Operador (opcional)
  "external_sale_id":    "VENDA-2026071401-0042",  // ID da venda no PDV (opcional)
  "receipt_number":      "000123456",        // N.º cupom (opcional)
  // ► Itens da compra — exibidos ao colaborador no app (opcional)
  "items": [
    { "ean": "7891234567890", "description": "ARROZ 5KG", "quantity": 1, "unit_price": 25.90, "total_price": 25.90 },
    { "ean": "7899876543210", "description": "FEIJAO 1KG",  "quantity": 2, "unit_price": 8.50,  "total_price": 17.00 }
  ],
  "idempotency_key":     "pdv-caixa03-2026071401-000123456"  // Recomendado
}

Campos do Request

CampoTipoObrigat.Descrição
employee_identifierstringSimCPF do colaborador (com ou sem formatação) ou ID interno
gross_amountnumberSimValor bruto total da compra em R$ (ex.: 150.00)
store_idstringNãoIdentificador da loja no sistema
pdv_idstringNãoIdentificador do caixa/terminal
operator_idstringNãoIdentificador do operador de caixa
external_sale_idstringNãoIdentificador da venda no sistema do PDV (usado para rastreio/conciliação)
receipt_numberstringNãoNúmero do cupom fiscal
itemsarrayNãoItens da compra — exibidos ao colaborador na tela de confirmação do app. Cada item: ean, description, quantity, unit_price, total_price
idempotency_keystringNãoChave única por venda — evita duplicação em retentativas
ℹ️
Nomes de campo: o servidor aceita tanto snake_case (gross_amount) quanto camelCase (grossAmount). Recomendamos snake_case.
Recomendação — Idempotência: Sempre envie idempotency_key com um valor único por transação (ex.: PDV-ID + data + número-cupom). Em caso de falha de rede e retentativa, o servidor retornará a mesma reserva pendente sem criar duplicata.

Response — Sucesso (HTTP 200)

{
  "success":         true,
  "mode":            "qr",
  "pending_auth_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",

  // ► Imagem PNG do QR Code — exibir diretamente na tela
  "qr_image_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",

  // ► Deep link (fallback caso prefira gerar o QR localmente)
  "qr_payload":      "elo365://convenio/auth?t=eyJhbGci...&h=a1b2c3",

  // ► Token opaco para polling — NÃO expor ao cliente
  "polling_token":   "a3f9c2e1d4b5a6f7...64 chars hex...",

  "expires_at":      "2026-03-13T15:03:00.000Z",  // ISO 8601 UTC
  "employee_name":   "João Silva",
  "gross_amount":    150.00,
  "net_amount_payroll": 144.00,
  "subsidy_amount":  6.00
}

Campos do Response

CampoTipoDescrição
qr_image_base64stringImagem PNG 256×256px em Base64 — exibir diretamente na tela do PDV
qr_payloadstringURL do deep link elo365:// — usar como fallback para gerar QR no PDV
polling_tokenstringToken opaco (64 chars hex) para consultar o status. Não expor ao cliente.
expires_atstringHorário de expiração em UTC ISO 8601 (TTL: 3 minutos)
employee_namestringNome do colaborador para exibir na tela
net_amount_payrollnumberValor que será descontado em folha (após subsídio)
subsidy_amountnumberSubsídio concedido pela empresa ao colaborador

Respostas de Negação

denial_codeSignificadoAção recomendada
ACCOUNT_NOT_FOUNDColaborador sem conta convênioInformar ao operador
ACCOUNT_BLOCKEDConta bloqueada pelo RHOrientar colaborador a contatar o RH
ACCOUNT_SUSPENDEDConta suspensaOrientar colaborador a contatar o RH
LIMIT_EXCEEDEDLimite disponível insuficienteExibir saldo disponível se retornado
POLICY_INACTIVEPolítica convênio inativaInformar ao operador
PIN_NOT_CONFIGUREDColaborador não configurou PINOrientar colaborador a abrir o app

4 Consultar Status (Polling)

Endpoint: GET /api/convenio/authorize/poll/:polling_token

Chamar a cada 2 segundos enquanto aguarda a confirmação do colaborador.

Request

GET /api/convenio/authorize/poll/a3f9c2e1d4b5a6f7...64chars
Authorization: Bearer <pdv_token>

Responses Possíveis

● 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             ",
    "----------------------------------------"
  ]
}
⚠️
Atenção à nomenclatura na resposta aprovada: os campos estilo TEF (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"
}

5 Máquina de Estados

Inicial
PENDING
Aguardando o colaborador
Final ✓
APPROVED
PIN correto, saldo debitado
Final
REJECTED
Colaborador recusou
Final
EXPIRED
TTL de 3 min atingido
Final
PIN_BLOCKED
3 PINs errados
Final
CANCELLED
PDV cancela a reserva
ℹ️
Estados finais (approved, rejected, expired, pin_blocked, cancelled): o loop de polling pode ser encerrado ao receber qualquer um deles.
⚠️
Reserva de saldo em duas fases: quando a autorização é criada (modo QR), o saldo do colaborador é reservado imediatamente. A reserva vira consumo definitivo apenas na aprovação (approved); em qualquer outro estado final (rejected, expired, pin_blocked, cancelled) o saldo reservado é devolvido automaticamente.

6 Implementação do Polling no PDV

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

Exemplo em Java

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 */ }
}

Exemplo em C# / .NET

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 */ }
}

7 Exibição do QR Code

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:

Web / HTML

<img
  src="data:image/png;base64,iVBORw0KGgo..."
  width="256"
  height="256"
  alt="QR Code Convênio"
/>

Python

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

Java

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));

C# / .NET

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
ℹ️
Fallback local: Se preferir gerar o QR no próprio PDV, use o campo qr_payload (a URL do deep link elo365://convenio/auth?t=...&h=...) com qualquer biblioteca de QR Code disponível no seu ambiente.

8 Tela Recomendada Durante o Processo

CONVÊNIO CORPORATIVO

Colaborador:João Silva
Valor bruto:R$ 150,00
Subsídio empresa:R$ 6,00
Desconto em folha:R$ 144,00

[QR Code]
256 × 256 px
Escaneie com o app Elo365
⏱ 2:18
[CANCELAR]

Exemplo ilustrativo da tela do PDV

9 Impressão do Cupom (2 vias)

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

----------------------------------------
   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 — 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
----------------------------------------

10 Segurança e Boas Práticas

🔒
Token JWT

Armazene em variáveis de ambiente ou cofre de segredos. Nunca no código-fonte ou logs. Rotacione periodicamente.

🔐
HTTPS Obrigatório

Todas as requisições devem usar HTTPS. Nunca HTTP em produção.

Intervalo de Polling

Mínimo 2 segundos entre chamadas. Não faça polling mais frequente do que isso.

🔄
Idempotência

Sempre envie idempotency_key único por venda. Isso evita cobranças duplicadas em caso de retentativa por falha de rede.

👁
Polling Token

Não exiba o polling_token na tela nem em logs. É para uso interno do PDV apenas.

🌎
Timeout de Rede

Configure timeout de 10 segundos nas requisições HTTP. Em caso de timeout, aguarde 3s e tente novamente (com mesmo idempotency_key).

🚫
Cancelamento pelo Operador

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).

Cancelar reserva pendente

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").

Resposta — sucesso

{ "success": true, "status": "cancelled" }

Resposta — quando a autorização não está mais pendente

{ "success": false, "error": "Autorização não está pendente (status atual: approved)." }
⚠️
Reserva pendente ≠ venda aprovada: este endpoint (/cancel) só cancela reservas ainda pending (antes da confirmação do colaborador). Para desfazer uma venda já aprovada, use o endpoint de estorno abaixo.

Estornar venda aprovada

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.

Resposta — sucesso

{
  "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
  }
}
ℹ️
Idempotência: em uma retentativa com a mesma idempotency_key, a resposta volta com cancelled: false e already_done: true — o estorno não é aplicado duas vezes.

Respostas de erro

HTTPCorpoQuando
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
⚠️
Estorno é sempre total (a transação inteira). Não há estorno de valor parcial de uma venda já aprovada — se o cliente devolve só parte dos itens, isso deve ser tratado como uma nova operação/ajuste pela retaguarda.
ℹ️
Escopo: o estorno usa o mesmo token e escopo (convenio:authorize) do fluxo de autorização — nenhum token adicional é necessário.

11 Códigos de Erro HTTP

CódigoSignificadoAção recomendada
200Sucesso — verificar campo success no JSONProcessar resposta normalmente
400Parâmetros inválidos (ex.: employee_identifier/gross_amount ausentes, ou requisição sem mode=qr)Corrigir os dados enviados
401Token do PDV ausente, inválido ou expiradoRenovar o token JWT junto à Elo365
403Token sem o escopo convenio:authorizeSolicitar um token com o escopo correto à Elo365
404Polling token / autorização não encontradaEncerrar polling, mostrar timeout
409Conflito 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)
500Erro interno do servidorAguardar 5s e tentar novamente (max 3x)
ℹ️
Rate limit: não há um limite rígido publicado hoje. Ainda assim, respeite o intervalo mínimo de 2 segundos entre chamadas de polling — polling agressivo pode ser bloqueado no futuro sem aviso.

12 Ambiente de Homologação

A equipe Elo365 fornece, sob demanda, um tenant de teste com:

  • um token de PDV dedicado (escopo convenio:authorize);
  • uma ou mais contas de colaborador de teste com convênio ativo, limite configurado e PIN já cadastrado (o PIN é definido pelo colaborador no app/portal — não há PIN mágico/sandbox);
  • a base URL de teste, informada no onboarding.

Como exercitar os cenários (todos com dados reais do tenant de teste):

CenárioComo reproduzir
AprovaçãoO colaborador de teste confirma no app com o PIN correto
RejeiçãoO colaborador recusa no app → status rejected
PIN bloqueadoErrar o PIN 3× → status pin_blocked
ExpiraçãoNão confirmar em 3 minutos → status expired
CancelamentoChamar POST /convenio/authorize/{id}/cancel enquanto pending → status cancelled
⚠️
Não existem, hoje, prefixo de token especial, PIN de sandbox nem header de simulação de cenário. Os testes usam o fluxo real em um tenant isolado. Solicite as credenciais de teste à equipe de integração da Elo365: integracoes@elo365.com.br

13 Exemplos Completos em cURL

1. Solicitar autorização QR

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"
  }'

2. Polling de status (repetir a cada 2s)

curl "https://www.elo365.com.br/api/convenio/authorize/poll/SEU_POLLING_TOKEN" \
  -H "Authorization: Bearer SEU_TOKEN_PDV"

3. (Opcional) Cancelar reserva pendente — se o operador desistir da venda

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" }'

4. (Opcional) Estornar venda JÁ APROVADA — se o cliente desistir DEPOIS de confirmar

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"
  }'

14 Suporte

Tipo de ContatoCanal
Dúvidas de integraçãointegracoes@elo365.com.br
Incidentes em produçãosuporte@elo365.com.br
📢
Para incidentes em produção, inclua no e-mail: tenant ID, polling token ou pending_auth_id, horário do evento (UTC) e payload da requisição (sem dados sensíveis).