Documentação da API EADPay
A API do EADPay permite consultar e criar pedidos, gerenciar produtos, cupons e webhooks, e acompanhar carrinhos abandonados a partir de qualquer sistema. Ela segue o padrão REST, recebe e devolve JSON em camelCase e usa códigos HTTP convencionais.
Todos os valores monetários são inteiros em centavos (49700 = R$ 497,00) e todas as datas seguem o padrão ISO 8601 em UTC (2026-10-31T14:02:13.530Z).
/openapi.json — importe no Postman, Insomnia ou gere clientes.
Bibliotecas oficiais: JavaScript/Node (sdk/js), PHP (sdk/php) e Python (sdk/python), além dos apps do Zapier e do Make — instruções e downloads no painel em Conta › Desenvolvedores e app.
Autenticação
Autentique cada requisição enviando a sua chave secreta no header Authorization com o esquema Bearer. As chaves começam com vt_live_ seguido de 32 caracteres.
Para gerar uma chave, acesse Painel → API → Nova chave. O token completo é exibido uma única vez; guardamos apenas um hash SHA-256 dele. Se perder a chave, revogue-a e gere outra.
A chave dá acesso total à sua conta. Use-a somente no servidor — nunca em código que roda no navegador ou em aplicativos móveis.
Requisições sem chave, com chave inválida ou revogada recebem 401 unauthorized. O campo Último uso no painel mostra quando cada chave foi utilizada pela última vez.
curl https://checkout.sualoja.com.br/api/v1/ping \
-H "Authorization: Bearer vt_live_9fK2mQx7Lr4Tz8Wb1Nc6Hp3Vd5Ys0Ae2"const res = await fetch('https://checkout.sualoja.com.br/api/v1/ping', {
headers: { Authorization: `Bearer ${process.env.VOLTRA_API_KEY}` }
});
console.log(await res.json()); // { ok: true, account: 'Minha Empresa' }import os, requests
res = requests.get(
"https://checkout.sualoja.com.br/api/v1/ping",
headers={"Authorization": f"Bearer {os.environ['VOLTRA_API_KEY']}"},
)
print(res.json()) # {'ok': True, 'account': 'Minha Empresa'}Respostas e erros
Respostas de sucesso trazem o recurso dentro de data. Listagens incluem também meta com informações de paginação.
Erros sempre têm o mesmo formato: um objeto error com code (estável, para o seu código tratar) e message (legível, em português). Erros de validação incluem fields, com a mensagem de cada campo inválido.
{ "data": { "id": "prod_T7m2Kq9XrL4pW0aZ", "name": "Método Tráfego Pro" } }
{ "data": [ … ], "meta": { "page": 1, "limit": 20, "total": 148, "pages": 8 } }
{
"error": {
"code": "validation_error",
"message": "Verifique os campos destacados.",
"fields": { "email": "Informe um e-mail válido", "document": "CPF inválido" }
}
}
| HTTP | code | Quando acontece |
|---|---|---|
| 200 / 201 | — | Requisição processada. 201 quando um recurso é criado. |
| 400 | invalid_json | O corpo da requisição não é um JSON válido. |
| 401 | invalid_api_key · unauthorized | Chave de API ausente, inválida ou revogada. |
| 404 | not_found | O recurso não existe (ou não pertence à sua conta). |
| 409 | invalid_status · slug_taken · in_use | A operação conflita com o estado atual — ex.: reembolsar um pedido que não está pago, slug já em uso ou excluir um produto usado em checkouts. |
| 413 | payload_too_large | Corpo da requisição acima do limite permitido. |
| 422 | validation_error · invalid_coupon · invalid_amount · product_unavailable | Campos inválidos (veja fields), cupom inexistente/expirado, valor abaixo de R$ 1,00 ou produto inativo. |
| 429 | rate_limited | Limite de requisições por minuto excedido. |
| 502 | refund_failed | O gateway recusou ou não respondeu ao pedido de reembolso. |
| 503 | no_gateway | Nenhum gateway ativo para o método de pagamento escolhido. |
| 500 | internal_error | Erro inesperado. Pode tentar novamente com a mesma Idempotency-Key. |
Paginação e filtros
Todas as listagens são paginadas por número de página. Os resultados vêm do mais recente para o mais antigo.
| Parâmetro | Tipo | Descrição |
|---|---|---|
| page | integer | Página desejada, começando em 1. Padrão: 1. |
| limit | integer | Itens por página. Padrão 20, máximo 100. |
| from / to | date | Intervalo de datas de criação (YYYY-MM-DD), quando o recurso suporta. |
O objeto meta informa page, limit, total (itens que atendem ao filtro) e pages. Para percorrer tudo, incremente page até ela ser maior que pages.
Rate limit
Cada chave pode fazer até 300 requisições por minuto. Todas as respostas trazem os headers abaixo; ao exceder o limite você recebe 429 rate_limited e deve aguardar até o horário indicado em X-RateLimit-Reset.
| Header | Descrição |
|---|---|
| X-RateLimit-Limit | Total de requisições permitidas na janela de 1 minuto. |
| X-RateLimit-Remaining | Requisições restantes na janela atual. |
| X-RateLimit-Reset | Momento (Unix, em segundos) em que a janela reinicia. |
Idempotência
Falhas de rede acontecem. Para reenviar com segurança uma criação de pedido sem correr o risco de cobrar o cliente duas vezes, envie o header Idempotency-Key com um valor único por operação (um UUID, ou o id do pedido no seu sistema).
Durante 24 horas, qualquer nova chamada a POST /orders com a mesma chave devolve exatamente a resposta original, sem processar um novo pagamento.
-H "Idempotency-Key: 2f6c0b1e-9a4d-4c55-b0f3-7d1e2a9c8b40"
Ambiente de testes
O Simulador EADPay é um gateway de testes que já vem instalado. Com ele você exercita todo o fluxo — aprovação, recusa, análise, fallback, PIX e boleto — sem movimentar dinheiro. Basta colocá-lo na fila de roteamento do método desejado em Painel → Gateways.
Cartões de teste
| Número | Resultado | Uso |
|---|---|---|
| 4111 1111 1111 1111 | Aprovado | Qualquer número válido (Luhn) aprova. Use qualquer validade futura e CVV. |
| 4111 1111 1115 0002 | Recusado | Simula recusa do emissor. Com a cascata em recusas ativa, o próximo gateway é tentado. |
| 4111 1111 1113 0004 | Erro de gateway | Simula indisponibilidade técnica — ideal para testar o fallback automático. |
| 4111 1111 1112 0005 | Em análise | O pedido fica waiting_payment, como numa revisão antifraude. |
| 4111 1111 0003 0009 | Emissor indisponível | Recusa com código 91 só no 1º gateway: a retentativa inteligente tenta o próximo gateway na mesma hora. |
Vale qualquer número terminado em 0002, 0004 ou 0005 que seja válido pelo algoritmo de Luhn — os da tabela já estão prontos para copiar. O erro do final 0004 acontece só na primeira tentativa da cascata, então o gateway seguinte aprova. Com cartão salvo (renovações, 1 clique, upsell): final 0003 é recusa definitiva, 0006 saldo insuficiente e 0007 saldo insuficiente que aprova na retentativa.
PIX e boleto simulados
O PIX gera um código copia e cola no padrão EMV (com CRC16 válido) e expira no prazo configurado no checkout. O boleto gera uma linha digitável fictícia de 47 dígitos e uma página imprimível em /boleto/:orderId. Para confirmar o pagamento, use o botão Simular pagamento na página do pedido, a ação Marcar como pago no painel, ou envie um postback:
curl -X POST https://checkout.sualoja.com.br/postbacks/gw_S4ndb0xVoltra01 \
-H "Content-Type: application/json" \
-d '{"transactionId":"sbx_tx_7Qm2Lr9Xc1","status":"paid"}'
O objeto Pedido
Um pedido representa uma tentativa de compra: os itens, o cliente, os valores calculados, o método de pagamento e o resultado em cada gateway. É o objeto que chega em data nos eventos order.*.
| Campo | Tipo | Descrição |
|---|---|---|
| id | string | Identificador único, prefixo ord_. |
| code | string | Código legível e sequencial, ex.: VT-100318. Pode ser usado no lugar do id. |
| checkoutId · checkoutName | string | null | Checkout de origem. null para pedidos criados com items pela API. |
| customerId · customer | string · object | Cliente e uma cópia dos dados no momento da compra: name, email, document, phone, address. |
| items | array | Itens com productId, name, price, quantity e type (main ou bump). |
| subtotal | integer | Soma dos itens, em centavos. |
| discount | integer | Desconto total (cupom + desconto do método). |
| interest | integer | Juros do parcelamento no cartão. |
| total | integer | Valor efetivamente cobrado. |
| coupon | object | null | { code, discount } quando um cupom foi aplicado. |
| method · installments | string · integer | card, pix ou boleto, e o número de parcelas. |
| status | string | Veja status do pedido. |
| gatewayId · gateway · gatewayName | string | Gateway que processou com sucesso (ou o último tentado) e o provider (pagarme, appmax, mercadopago, asaas, sandbox). |
| transactionId | string | Identificador da transação no gateway. |
| attempts | array | Cada tentativa de cobrança: gatewayId, provider, status (approved, refused, pending, error), message, latencyMs e at. |
| pix | object | null | { qrCode, expiresAt } — código copia e cola e vencimento. |
| boleto | object | null | { line, barcode, url, dueDate }. |
| card | object | null | { brand, last4, holder }. Número completo e CVV nunca são armazenados. |
| utm | object | source, medium, campaign, content, term capturados da URL do checkout. |
| source · metadata | string · object | Origem (checkout ou api) e dados livres enviados por você. |
| history | array | Linha do tempo de status: { status, at, note }. |
| createdAt · updatedAt · paidAt · refundedAt | datetime | Datas do ciclo de vida. |
{
"id": "ord_Zk81QmXr2LpV0c7N",
"code": "VT-100318",
"checkoutId": "chk_8Hq2LxP0aZ71mNcV",
"checkoutName": "Tráfego Pro — Principal",
"customerId": "cus_4Rt9WqLm2Xc8Vb1N",
"customer": {
"name": "Marina Costa",
"email": "marina@email.com",
"document": "39053344705",
"phone": "11987654321",
"address": null
},
"items": [
{ "productId": "prod_T7m2Kq9XrL4pW0aZ", "name": "Método Tráfego Pro", "price": 49700, "quantity": 1, "type": "main" },
{ "productId": "prod_Hc3Vn8Lq1Mz6Rt0Y", "name": "Kit Templates de Anúncios", "price": 3700, "quantity": 1, "type": "bump" }
],
"subtotal": 53400,
"discount": 0,
"interest": 0,
"total": 53400,
"coupon": null,
"method": "card",
"installments": 1,
"status": "paid",
"gatewayId": "gw_P4g9Rm2Lx7Qa1Zc0",
"gateway": "pagarme",
"gatewayName": "Pagar.me",
"transactionId": "or_56GXnk6T0eU88qMm",
"attempts": [
{ "gatewayId": "gw_Ap8Lm3Qx9Rz2Vc1N", "provider": "appmax", "gatewayName": "Appmax", "status": "error",
"message": "Tempo de resposta excedido", "latencyMs": 3004, "at": "2026-10-31T14:02:10.112Z" },
{ "gatewayId": "gw_P4g9Rm2Lx7Qa1Zc0", "provider": "pagarme", "gatewayName": "Pagar.me", "status": "approved",
"message": "Transação autorizada", "latencyMs": 412, "at": "2026-10-31T14:02:13.528Z" }
],
"pix": null,
"boleto": null,
"card": { "brand": "mastercard", "last4": "4018", "holder": "MARINA COSTA" },
"utm": { "source": "facebook", "medium": "cpc", "campaign": "black-friday", "content": null, "term": null },
"source": "checkout",
"metadata": {},
"history": [
{ "status": "pending", "at": "2026-10-31T14:02:09.870Z", "note": "Pedido criado" },
{ "status": "paid", "at": "2026-10-31T14:02:13.530Z", "note": "Aprovado via Pagar.me" }
],
"createdAt": "2026-10-31T14:02:09.870Z",
"updatedAt": "2026-10-31T14:02:13.530Z",
"paidAt": "2026-10-31T14:02:13.530Z",
"refundedAt": null
}
Status do pedido
| status | Significado | Evento |
|---|---|---|
| pending | Pedido criado, cobrança em processamento. | order.created |
| waiting_payment | PIX ou boleto gerado aguardando pagamento, ou cartão em análise. | order.waiting_payment |
| paid | Pagamento aprovado. Libere o acesso ou envie o produto. | order.paid |
| refused | Pagamento recusado em todos os gateways tentados. | order.refused |
| refunded | Valor devolvido ao cliente. | order.refunded |
| canceled | Pedido cancelado antes do pagamento. | order.canceled |
| chargeback | O cliente contestou a compra junto ao banco. | order.chargeback |
| expired | PIX ou boleto venceu sem pagamento. | order.expired |
Listar pedidos
Retorna os pedidos da conta, do mais recente para o mais antigo, com filtros combináveis.
| Query | Tipo | Descrição |
|---|---|---|
| status | string | Um dos status. |
| method | string | card, pix ou boleto. |
| string | E-mail exato do cliente. | |
| from · to | date | Período de criação, YYYY-MM-DD. |
| page · limit | integer | Paginação (máx. 100 por página). |
curl "https://checkout.sualoja.com.br/api/v1/orders?status=paid&method=pix&from=2026-10-01&limit=50" \
-H "Authorization: Bearer $VOLTRA_API_KEY"const params = new URLSearchParams({ status: 'paid', method: 'pix', from: '2026-10-01', limit: '50' });
const res = await fetch(`https://checkout.sualoja.com.br/api/v1/orders?${params}`, {
headers: { Authorization: `Bearer ${process.env.VOLTRA_API_KEY}` }
});
const { data, meta } = await res.json();res = requests.get(
"https://checkout.sualoja.com.br/api/v1/orders",
params={"status": "paid", "method": "pix", "from": "2026-10-01", "limit": 50},
headers={"Authorization": f"Bearer {API_KEY}"},
)
pedidos = res.json()["data"]{
"data": [ { "id": "ord_Zk81QmXr2LpV0c7N", "code": "VT-100318", "status": "paid", "total": 47215, … } ],
"meta": { "page": 1, "limit": 50, "total": 61, "pages": 2 }
}
Obter pedido
Busca um pedido pelo id (ord_…) ou pelo code (VT-100318). Útil para confirmar o status antes de liberar um acesso, mesmo que você já tenha recebido o webhook.
curl https://checkout.sualoja.com.br/api/v1/orders/VT-100318 \
-H "Authorization: Bearer $VOLTRA_API_KEY"const res = await fetch('https://checkout.sualoja.com.br/api/v1/orders/VT-100318', {
headers: { Authorization: `Bearer ${process.env.VOLTRA_API_KEY}` }
});
const { data: pedido } = await res.json();
if (pedido.status === 'paid') liberarAcesso(pedido.customer.email);pedido = requests.get(
"https://checkout.sualoja.com.br/api/v1/orders/VT-100318",
headers={"Authorization": f"Bearer {API_KEY}"},
).json()["data"]Criar cobrança
Cria um pedido e já processa o pagamento usando o roteamento de gateways configurado. Informe um checkoutId (usa o preço, os métodos e as regras daquele checkout) ou uma lista de items com produtos do catálogo.
| Campo | Tipo | Descrição |
|---|---|---|
| checkoutId | string | Checkout de referência. Alternativo a items. |
| items | array | [{ productId, quantity }]. Alternativo a checkoutId. |
| customerobrigatório | object | name, email, document (CPF/CNPJ, só dígitos), phone e address opcional. |
| methodobrigatório | string | card, pix ou boleto. |
| card | object | Para cartão: number, holder, expMonth, expYear, cvv. |
| installments | integer | Número de parcelas (padrão 1). |
| coupon | string | Código de cupom a aplicar. |
| metadata | object | Dados livres devolvidos no pedido e nos webhooks. |
Pagamentos recusados não geram erro HTTP: a resposta é 201 com status: "refused" e as tentativas em attempts. Sempre verifique o campo status.
curl -X POST https://checkout.sualoja.com.br/api/v1/orders \
-H "Authorization: Bearer $VOLTRA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-pedido-8812" \
-d '{
"checkoutId": "chk_8Hq2LxP0aZ71mNcV",
"method": "card",
"installments": 3,
"coupon": "BEMVINDO10",
"customer": {
"name": "Marina Costa",
"email": "marina@email.com",
"document": "39053344705",
"phone": "11987654321"
},
"card": {
"number": "4111111111111111",
"holder": "MARINA COSTA",
"expMonth": "09",
"expYear": "2029",
"cvv": "123"
},
"metadata": { "crmId": "8812" }
}'const res = await fetch('https://checkout.sualoja.com.br/api/v1/orders', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOLTRA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `crm-pedido-${crmId}`
},
body: JSON.stringify({
items: [{ productId: 'prod_T7m2Kq9XrL4pW0aZ', quantity: 1 }],
method: 'pix',
customer: { name: 'Marina Costa', email: 'marina@email.com', document: '39053344705' },
metadata: { crmId }
})
});
const { data } = await res.json();
enviarWhatsApp(data.customer.phone, data.pix.qrCode);res = requests.post(
"https://checkout.sualoja.com.br/api/v1/orders",
headers={"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": f"crm-pedido-{crm_id}"},
json={
"checkoutId": "chk_8Hq2LxP0aZ71mNcV",
"method": "boleto",
"customer": {"name": "Marina Costa", "email": "marina@email.com", "document": "39053344705"},
},
)
boleto = res.json()["data"]["boleto"]
print(boleto["line"], boleto["url"]){
"data": {
"id": "ord_Qm7Xr2Lp9Vc1Nz8K",
"code": "VT-100319",
"status": "waiting_payment",
"method": "pix",
"subtotal": 49700,
"discount": 2485,
"total": 47215,
"pix": {
"qrCode": "00020101021226870014br.gov.bcb.pix…6304A1F3",
"expiresAt": "2026-10-31T14:32:09.870Z"
},
…
}
}
Reembolsar pedido
Solicita o estorno integral ao gateway que aprovou o pagamento. Disponível apenas para pedidos com status paid; em outros casos a resposta é 409. Em caso de sucesso, o pedido passa para refunded e o evento order.refunded é disparado.
curl -X POST https://checkout.sualoja.com.br/api/v1/orders/VT-100318/refund \
-H "Authorization: Bearer $VOLTRA_API_KEY"
Cancelar pedido
Cancela um pedido que ainda não foi pago — por exemplo um PIX ou boleto que o cliente desistiu de pagar. O pedido passa para canceled e o evento order.canceled é enviado. Pedidos pagos devem ser reembolsados.
curl -X POST https://checkout.sualoja.com.br/api/v1/orders/VT-100319/cancel \
-H "Authorization: Bearer $VOLTRA_API_KEY"
Produtos
Produtos formam o catálogo usado pelos checkouts e pelas cobranças via API. Ao atualizar com PATCH, envie apenas os campos que quer alterar. Pedidos antigos guardam uma cópia do nome e preço, então mudanças não afetam o histórico.
| Campo | Tipo | Descrição |
|---|---|---|
| nameobrigatório | string | Nome exibido no checkout. |
| priceobrigatório | integer | Preço em centavos. |
| description | string | Descrição curta. |
| type | string | digital, physical ou service. |
| image | string | URL da imagem. |
| sku | string | Código interno. |
| deliveryUrl | string | Link de acesso entregue após a compra. |
| active | boolean | Produtos inativos não podem ser vendidos. |
curl -X POST https://checkout.sualoja.com.br/api/v1/products \
-H "Authorization: Bearer $VOLTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Planilha Financeira Inteligente","price":4700,"type":"digital",
"deliveryUrl":"https://membros.sualoja.com.br/planilha"}'
{
"data": {
"id": "prod_Lq2Xm9Rt4Vc7Nz1K",
"name": "Planilha Financeira Inteligente",
"description": "",
"price": 4700,
"type": "digital",
"image": "",
"sku": "",
"deliveryUrl": "https://membros.sualoja.com.br/planilha",
"active": true,
"createdAt": "2026-10-31T15:10:00.000Z",
"updatedAt": "2026-10-31T15:10:00.000Z"
}
}
Clientes
Clientes são criados automaticamente na primeira compra e identificados pelo e-mail. Cada novo cliente dispara o evento customer.created. A listagem aceita page e limit.
{
"id": "cus_4Rt9WqLm2Xc8Vb1N",
"name": "Marina Costa",
"email": "marina@email.com",
"document": "39053344705",
"phone": "11987654321",
"address": null,
"createdAt": "2026-09-12T18:20:41.002Z",
"updatedAt": "2026-10-31T14:02:09.870Z"
}
Checkouts
Retorna a configuração completa de cada checkout: produto, preço, order bumps, métodos e regras de pagamento, campos, aparência, elementos de conversão, pixels e redirecionamentos. O link público é /c/:slug. A edição é feita pelo painel, com prévia ao vivo.
| Campo | Descrição |
|---|---|
| slug | Identificador usado na URL. |
| price | Preço em centavos, ou null para usar o do produto. |
| bumps | Ofertas adicionais: { id, productId, title, description, price }. |
| methods | Regras de card (parcelas, juros), pix e boleto (desconto, validade). |
| fields | Campos exigidos: phone, document, address, confirmEmail. |
| appearance · elements | Visual e elementos de conversão. |
| tracking · redirects | Pixels e URLs de redirecionamento após o pagamento. |
| visits | Visitas registradas no link público. |
{
"data": {
"id": "chk_8Hq2LxP0aZ71mNcV",
"slug": "trafego-pro",
"name": "Tráfego Pro — Principal",
"productId": "prod_T7m2Kq9XrL4pW0aZ",
"price": null,
"bumps": [
{ "id": "bump_K2m9Xq", "productId": "prod_Hc3Vn8Lq1Mz6Rt0Y",
"title": "Leve também o Kit de Templates", "price": 3700 }
],
"methods": {
"card": { "enabled": true, "maxInstallments": 12, "freeInstallments": 1, "interestRate": 2.99 },
"pix": { "enabled": true, "discountPercent": 5, "expiresInMinutes": 30 },
"boleto": { "enabled": true, "discountPercent": 0, "dueDays": 3 }
},
"defaultMethod": "card",
"visits": 4821
}
}
Cupons
Cupons dão desconto percentual ou fixo sobre o subtotal (produto + order bumps). O código é salvo sempre em maiúsculas.
| Campo | Tipo | Descrição |
|---|---|---|
| codeobrigatório | string | Código digitado pelo cliente. |
| typeobrigatório | string | percent ou fixed. |
| valueobrigatório | number | Percentual (ex.: 10) ou valor em centavos. |
| maxUses | integer | null | Limite total de usos. |
| expiresAt | datetime | null | Data de expiração. |
| checkoutIds | array | Restringe a checkouts específicos (vazio = todos). |
| active | boolean | Ativa ou pausa o cupom. |
curl -X POST https://checkout.sualoja.com.br/api/v1/coupons \
-H "Authorization: Bearer $VOLTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code":"BLACK50","type":"fixed","value":5000,"maxUses":200,
"expiresAt":"2026-11-30T23:59:59.000Z"}'
Carrinhos abandonados
Quando o cliente preenche o e-mail no checkout, um lead é registrado. O ciclo de status é:
open— o cliente está preenchendo o checkout.abandoned— sem compra aprovada após o prazo configurado (30 min por padrão). Disparacart.abandoned.recovered— o cliente voltou e pagou depois de abandonar. Disparacart.recovered.converted— comprou sem chegar a abandonar.
{
"id": "lead_Vx2Lm8Qr4Tz1Nc9K",
"checkoutId": "chk_8Hq2LxP0aZ71mNcV",
"checkoutName": "Tráfego Pro — Principal",
"name": "Lucas Ferreira",
"email": "lucas.ferreira@email.com",
"phone": "21998765432",
"status": "abandoned",
"orderId": null,
"value": 49700,
"createdAt": "2026-10-31T13:10:22.000Z",
"abandonedAt": "2026-10-31T13:40:30.000Z"
}
Eventos
Todo evento enviado por webhook também fica registrado e pode ser consultado pela API — ótimo para reconciliar algo que o seu sistema perdeu. Filtre por type e pagine com page/limit. Cada item tem id, type, data e createdAt, no mesmo formato do payload do webhook.
Webhooks — visão geral
Webhooks avisam o seu sistema, em tempo real, sobre o que acontece na conta: um pagamento aprovado, um PIX gerado, um reembolso, um carrinho abandonado. Cadastre um endpoint em Painel → Webhooks (ou pela API), escolha os eventos e, opcionalmente, restrinja a checkouts específicos.
- As entregas são feitas com
POSTe corpo JSON, com timeout de 10 segundos. - Qualquer resposta
2xxé considerada sucesso. Responda rápido e processe em segundo plano. - Cada endpoint tem um
secretpróprio (whsec_…) para validar a assinatura. Ele pode ser rotacionado no painel. - O mesmo evento pode chegar mais de uma vez em casos raros: use
iddo evento para deduplicar.
Tipos de eventos
| type | Quando é disparado | data |
|---|---|---|
| order.created | Pedido criado, antes da resposta do gateway. | Pedido |
| order.waiting_payment | PIX ou boleto gerado, ou cartão em análise. | Pedido |
| order.paid | Pagamento aprovado. | Pedido |
| order.refused | Pagamento recusado em todos os gateways tentados. | Pedido |
| order.refunded | Pedido reembolsado. | Pedido |
| order.canceled | Pedido cancelado. | Pedido |
| order.chargeback | Contestação recebida do gateway. | Pedido |
| order.expired | PIX ou boleto vencido sem pagamento. | Pedido |
| customer.created | Primeira compra de um novo e-mail. | Cliente |
| cart.abandoned | Lead sem compra após o prazo de abandono. | Lead |
| cart.recovered | Cliente que abandonou voltou e pagou. | Lead |
| webhook.test | Botão “Enviar teste” no painel. | Exemplo |
Assine * para receber todos os eventos, incluindo os que forem criados no futuro.
Payload e headers
| Header | Valor |
|---|---|
| Content-Type | application/json |
| User-Agent | Voltra-Webhooks/1.0 |
| X-Voltra-Event | Tipo do evento, ex.: order.paid. |
| X-Voltra-Delivery | Id da entrega (dlv_…), igual em todas as retentativas. |
| X-Voltra-Signature | t=<unix>,v1=<hmac> |
{
"id": "evt_Np4Lx9Qm2Rt7Vc1Z",
"type": "order.paid",
"createdAt": "2026-10-31T14:02:13.531Z",
"data": {
"id": "ord_Zk81QmXr2LpV0c7N",
"code": "VT-100318",
"status": "paid",
"total": 53400,
"method": "card",
"customer": { "name": "Marina Costa", "email": "marina@email.com", … },
"items": [ … ],
…
}
}
Verificar a assinatura
Para garantir que a requisição veio do EADPay e não foi alterada, calcule um HMAC-SHA256 usando o secret do endpoint sobre a string t + "." + corpoBruto e compare com v1. Use sempre o corpo bruto (antes de qualquer parse de JSON) e uma comparação em tempo constante. Rejeite timestamps com mais de 5 minutos para evitar ataques de repetição.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhooks/voltra', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Voltra-Signature') || '';
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.VOLTRA_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest('hex');
const valid = v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
const recent = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
if (!valid || !recent) return res.status(400).send('assinatura inválida');
const event = JSON.parse(req.body);
switch (event.type) {
case 'order.paid': liberarAcesso(event.data); break;
case 'order.refunded': removerAcesso(event.data); break;
}
res.sendStatus(200);
});<?php
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_VOLTRA_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig); // ['t' => ..., 'v1' => ...]
$expected = hash_hmac('sha256', $sig['t'] . '.' . $payload, getenv('VOLTRA_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) $sig['t']) > 300) {
http_response_code(400);
exit('assinatura inválida');
}
$event = json_decode($payload, true);
if ($event['type'] === 'order.paid') {
liberar_acesso($event['data']['customer']['email']);
}
http_response_code(200);import hashlib, hmac, json, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["VOLTRA_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/voltra")
def voltra_webhook():
raw = request.get_data()
header = request.headers.get("X-Voltra-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
signed = f"{parts.get('t', '')}.".encode() + raw
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
if abs(time.time() - int(parts.get("t", 0))) > 300:
abort(400)
event = json.loads(raw)
if event["type"] == "order.paid":
liberar_acesso(event["data"]["customer"]["email"])
return "", 200Retentativas
Se o seu endpoint responder com status diferente de 2xx, demorar mais de 10 segundos ou estiver inacessível, a entrega fica como retrying e é reenviada automaticamente com backoff:
| Tentativa | Quando |
|---|---|
| 1ª | Imediatamente, no momento do evento |
| 2ª | 1 minuto depois da falha |
| 3ª | 5 minutos depois |
| 4ª | 30 minutos depois |
| 5ª | 2 horas depois |
| 6ª | 6 horas depois — se falhar, a entrega fica failed |
No painel você vê cada entrega com status, código e corpo da resposta, duração e próximo agendamento, e pode reenviar manualmente qualquer uma delas.
Formatos Discord e Slack
Além do JSON padrão, um endpoint pode usar o formato discord ou slack. Nesse caso o corpo já sai no formato de mensagem esperado por essas ferramentas — basta colar a URL do webhook do canal.
- discord:
{ content, embeds: [{ title, description, color, fields }] } - slack:
{ text, blocks }com Block Kit.
Use, por exemplo, um canal #vendas que recebe order.paid e outro #suporte que recebe order.refused e cart.abandoned.
{
"content": null,
"embeds": [{
"title": "Pagamento aprovado · VT-100318",
"description": "Marina Costa comprou Método Tráfego Pro",
"color": 1352291,
"fields": [
{ "name": "Valor", "value": "R$ 534,00", "inline": true },
{ "name": "Método", "value": "Cartão 1x", "inline": true },
{ "name": "Gateway", "value": "Pagar.me", "inline": true }
]
}]
}
Gerenciar webhooks via API
Útil para plataformas que se conectam ao EADPay e precisam registrar o próprio endpoint automaticamente. A resposta de criação inclui o secret para validação.
| Campo | Tipo | Descrição |
|---|---|---|
| urlobrigatório | string | URL HTTPS que receberá os eventos. |
| name | string | Nome para identificação no painel. |
| events | array | Lista de tipos, ou ["*"]. |
| format | string | json (padrão), discord ou slack. |
| checkoutIds | array | Filtra eventos por checkout (vazio = todos). |
| active | boolean | Pausa ou retoma as entregas. |
curl -X POST https://checkout.sualoja.com.br/api/v1/webhooks \
-H "Authorization: Bearer $VOLTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Área de membros","url":"https://membros.sualoja.com.br/hooks/voltra",
"events":["order.paid","order.refunded","order.chargeback"]}'
{
"data": {
"id": "wh_Rt8Lm2Qx9Vc4Nz1K",
"name": "Área de membros",
"url": "https://membros.sualoja.com.br/hooks/voltra",
"events": ["order.paid", "order.refunded", "order.chargeback"],
"secret": "whsec_4f9Kq2Lm8Xr1Tz7Vc3Nb6Hp0Wd5Ys2Ae",
"format": "json",
"active": true,
"checkoutIds": []
}
}
Receptor de testes
O EADPay inclui um receptor local de requisições. Qualquer chamada para /inspector/demo (ou outro nome de bucket) é registrada com método, headers e corpo, e pode ser inspecionada em Painel → Webhooks → Receptor de testes. Aponte um webhook para ele e faça uma compra no checkout para ver o evento chegando, com a assinatura, antes de integrar com o seu sistema real.
O receptor guarda as 100 requisições mais recentes. Use buckets diferentes para separar testes de projetos distintos.
Gateways suportados
Cada gateway é conectado em Painel → Gateways com as credenciais da sua própria conta. Você pode ter mais de uma conta do mesmo provider (por exemplo, dois CNPJs) e escolher o ambiente de teste ou produção.
| Provider | Métodos | Credenciais | API |
|---|---|---|---|
| pagarme | Cartão, PIX, boleto | Secret key (sk_…) e public key (pk_…) | Pagar.me Core v5 |
| appmax | Cartão, PIX, boleto | Client ID, Client secret e External ID (Painel Appmax → API → Credenciais) | Appmax API v1 com OAuth2 (produção e sandbox) |
| mercadopago | Cartão, PIX, boleto | Access token e public key | Mercado Pago Payments v1 |
| asaas | Cartão, PIX, boleto | API key | Asaas v3 (produção e sandbox) |
| sandbox | Cartão, PIX, boleto | Nenhuma | Simulador interno |
O botão Testar conexão faz uma chamada autenticada ao provider e mostra se as credenciais são válidas e o tempo de resposta. Na listagem, as credenciais aparecem mascaradas.
Cartão sem dados abertos no servidor
O número e o CVV do cartão nunca passam pelo servidor EADPay: o checkout gera um token no navegador para cada gateway da fila do cartão — Pagar.me com a public key (cadastre o domínio do checkout no dashboard Pagar.me), Appmax com o Appmax JS e o External ID, Mercado Pago com a public key. O servidor recebe só os tokens, a bandeira e os 4 últimos dígitos. Assim o fallback continua funcionando entre gateways.
Dados exigidos pelos gateways
O checkout pede automaticamente os campos que os gateways ativos exigem, mesmo que estejam desligados na configuração do checkout: CPF/CNPJ e celular (Pagar.me e Appmax) e endereço de cobrança para cartão e boleto na Pagar.me. Quando o endereço só é exigido para alguns métodos, ele aparece apenas quando o comprador escolhe um deles.
Roteamento e fallback
Para cada método — cartão, PIX e boleto — você define uma fila ordenada de gateways. Na hora do pagamento, o EADPay considera apenas os gateways ativos que suportam aquele método e segue estas regras:
| Resultado da tentativa | O que acontece |
|---|---|
| approved | Para a fila. Pedido paid. |
| pending | Para a fila. Pedido waiting_payment (PIX/boleto gerado ou análise). |
| error | Falha técnica, timeout ou indisponibilidade. Tenta o próximo se fallback estiver ativo. |
| refused | Recusa do emissor. Tenta o próximo apenas para cartão e se cascata em recusas estiver ativa. |
Se nenhuma tentativa for aprovada, o pedido termina como refused. Se a fila estiver vazia, a resposta é 503 no_gateway. Todas as tentativas ficam em attempts, e o painel mostra a taxa de aprovação de cada gateway para você ajustar a ordem.
Postbacks dos gateways
PIX e boleto são confirmados depois, de forma assíncrona. Para que o EADPay saiba quando o pagamento caiu — e também de reembolsos e chargebacks feitos no gateway — cadastre a URL de postback de cada gateway no painel do provider. A URL exata, com o id do gateway, aparece em Painel → Gateways com um botão de copiar.
| Gateway | Onde cadastrar | Eventos recomendados |
|---|---|---|
| Pagar.me | Dashboard → Configurações → Webhooks → Criar webhook | order.paid, order.payment_failed, order.canceled, charge.refunded, charge.chargedback |
| Appmax | Painel Appmax → API → Webhooks (URL HTTPS pública) | order_paid, order_approved, order_paid_by_pix, order_pix_expired, order_billet_overdue, order_refund, payment_not_authorized, order_chargeback_in_treatment |
| Mercado Pago | Suas integrações → Webhooks → Configurar notificações | Pagamentos (payment). A URL também é enviada em cada cobrança. |
| Asaas | Integrações → Webhooks | PAYMENT_CONFIRMED, PAYMENT_RECEIVED, PAYMENT_OVERDUE, PAYMENT_REFUNDED, PAYMENT_CHARGEBACK_REQUESTED |
| Simulador | Não é necessário | Aceita { "transactionId", "status" } para testes. |
O postback é convertido para um status normalizado (paid, refused, refunded, canceled, chargeback, expired, waiting_payment), o pedido correspondente é atualizado e os seus webhooks são disparados.
Confirmação na origem: os webhooks da Pagar.me e da Appmax não são assinados. Por isso, antes de alterar o pedido, o EADPay consulta o status na API do gateway (GET /orders/:id) e usa o status confirmado — um postback forjado dizendo "pago" é ignorado. Se a consulta falhar, a resposta é 503 e o gateway reenvia mais tarde; nos demais casos, 200 {"received": true}.
Preços e parcelamento
O servidor é sempre a autoridade sobre os valores. O cálculo segue esta ordem — e o checkout mostra exatamente o mesmo resultado ao cliente:
base = checkout.price ?? product.price
subtotal = base + soma(bumps selecionados)
couponDiscount = coupon.type === 'percent' ? round(subtotal * value / 100) : min(value, subtotal)
afterCoupon = subtotal - couponDiscount
methodDiscount = round(afterCoupon * methods[method].discountPercent / 100) // só PIX e boleto
net = afterCoupon - methodDiscount
// Cartão: rate = interestRate / 100
// n <= freeInstallments → parcela = round(net / n), total = net
// n > freeInstallments → parcela = round(net * rate / (1 - (1 + rate) ** -n)), total = parcela * n
Exemplos
| Cenário | Cálculo | Total |
|---|---|---|
| PIX com 5% de desconto | R$ 497,00 − R$ 24,85 | R$ 472,15 |
| Bump + cupom 10% + PIX 5% | R$ 534,00 − R$ 53,40 − R$ 24,03 | R$ 456,57 |
| Cartão 3x sem juros | 3 × R$ 165,67 (total sem acréscimo) | R$ 497,00 |
| Cartão 12x a 2,99% a.m. | 12 × R$ 49,90 | R$ 598,80 |
No pedido, discount soma cupom e desconto do método, e interest é a diferença entre o total parcelado e o valor líquido.
API pública do checkout
Estes endpoints não exigem chave e são os mesmos usados pela página do checkout. Use-os para construir um checkout próprio (headless) mantendo regras, cupons e roteamento do EADPay.
| Endpoint | Descrição |
|---|---|
| POST /api/public/checkouts/:slug/coupon | Valida um cupom: { code } → { coupon: { code, type, value } } ou 422 invalid_coupon. |
| POST /api/public/checkouts/:slug/leads | Registra/atualiza o lead: { leadId?, name, email, phone } → { leadId }. |
| POST /api/public/checkouts/:slug/orders | Cria o pedido e processa o pagamento. Retorna { order, redirectUrl, message }. |
| GET /api/public/orders/:id/status | Consulta leve para polling do PIX: { status, paidAt, redirectUrl }. |
{
"customer": {
"name": "Marina Costa",
"email": "marina@email.com",
"document": "39053344705",
"phone": "11987654321"
},
"bumps": ["bump_K2m9Xq"],
"coupon": "BEMVINDO10",
"method": "card",
"card": { "number": "4111111111111111", "holder": "MARINA COSTA", "expMonth": "09", "expYear": "2029", "cvv": "123", "installments": 3 },
"utm": { "source": "instagram", "medium": "stories", "campaign": "lancamento" },
"leadId": "lead_Vx2Lm8Qr4Tz1Nc9K"
}
Erros de validação retornam 422 com fields usando as chaves name, email, document, phone, zip, street, number, district, city, state, cardNumber, cardHolder, cardExpiry, cardCvv, installments e method.
Ping
Verifica se a chave é válida e retorna o nome da conta. Ideal para o botão “Testar conexão” da sua integração.
{ "ok": true, "account": "Escala Digital Educação" }