Visão geral #
Esta é a API do seller da Reven — a superfície pública que integradores externos consomem. Toda requisição é autenticada por X-Api-Key (uma chave por workspace) e todo corpo/resposta segue o envelope { success, data } / { success, error }.
Todos os valores são em reais (decimal, ex.: 149.90) — no request (product.value, amount) e nas respostas/webhooks (amount, feeAmount, netAmount). Não existe centavo na API do seller.
A X-Api-Key e o Webhook Secret são segredos de servidor. Guarde-os em variável de ambiente — nunca no front-end, nunca no controle de versão, nunca num app mobile.
Endpoints
| Operação | Endpoint | Auth |
|---|---|---|
| Criar cobrança PIX | POST /gateway/charges | X-Api-Key |
| Listar / conciliar | GET /gateway/transactions | X-Api-Key |
| Receber status | webhook na callbackUrl | assinado (HMAC) |
| Sacar (PIX OUT) | POST /gateway/withdrawals | X-Api-Key + assinatura dedicada |
| Consultar saque | GET /gateway/withdrawals/:id | X-Api-Key |
Credenciais & rate limit #
O admin gera uma API Key por workspace no painel. Ela vai no header de toda requisição:
X-Api-Key: sua_api_key_aqui
Barreira de flood (429)
Depois de 30 respostas 401 de credencial (header ausente ou chave inexistente) do mesmo IP em 5 minutos, as tentativas seguintes com chave ausente/inválida desse IP passam a receber 429 com Retry-After (segundos):
{ "success": false, "error": "Too many requests. Please try again later." }
Uma chave válida nunca recebe 429 por esta barreira e nunca consome o limite — mesmo compartilhando IP de saída (NAT) com outra integração que erra a chave. Tomou 429 com chave que acredita ser válida? Ela foi revogada/rotacionada: corrija e aguarde o Retry-After — não repita em loop.
Webhook Secret
Cada workspace tem também um Webhook Secret (no painel), usado para verificar a assinatura dos webhooks (§ Verificar assinatura) e assinar os saques (§ Criar saque).
Assinatura de requisição #
Distinta da assinatura de webhook. Quando requireSignature está ativo no seu workspace, além da X-Api-Key toda requisição que você envia precisa de assinatura HMAC — inclusive as de leitura.
Workspaces criados a partir de agosto/2026 já nascem com requireSignature ativo. Workspace mais antigo continua só com X-Api-Key até o dono pedir a mudança. Integrando um workspace novo? Implemente esta seção desde o começo.
X-Signature: <hmac_hex>
X-Timestamp: <epoch_ms> # dentro de ±5 min
X-Nonce: <uuid_unico> # nunca reutilizar
HMAC-SHA256 em hex, chave = sua API Key, mensagem canônica:
canonical = `${METHOD}\n${path}\n${body}\n${timestamp}\n${nonce}`
# body = "" em GET; path = pathname sem query (ex.: /api/gateway/charges)
Se o workspace não exige assinatura, omita X-Signature/X-Timestamp/X-Nonce e mantenha só a X-Api-Key. Exemplo completo em Node na § Criar cobrança.
Criar cobrança PIX #
curl -X POST 'https://app.revenpayments.com.br/api/gateway/charges' \
-H 'X-Api-Key: SUA_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"product": { "name": "Plano Pro", "value": 149.90 },
"customer": {
"name": "João Silva",
"email": "joao@exemplo.com",
"doc": "11122233344",
"docType": "cpf",
"phone": "11912345678"
},
"expiracaoSegundos": 3600,
"callbackUrl": "https://seusite.com/webhooks/pix",
"metadata": { "pedido_id": "A-1001" }
}'
Corpo do request
| Campo | Tipo | Obrig. | Observação |
|---|---|---|---|
product.name | string | sim | Nome do produto/serviço |
product.value | number (reais) | sim | > 0 e finito. Ex.: 149.90 |
customer.name | string | sim | |
customer.email | string (email) | sim | |
customer.doc | string | sim | CPF/CNPJ (mín. 11 dígitos) |
customer.docType | cpf|cnpj | não | default cpf |
customer.phone / zip / street / district / city | string | não | |
expiracaoSegundos | number | não | default 3600 — validade do QR |
callbackUrl | string (url) | não | Recebe os webhooks desta cobrança |
metadata | object | não | Ecoado nos webhooks — casa com seu pedido |
Resposta 201
{
"success": true,
"data": {
"transactionId": "b3f1…",
"pixCode": "00020126580014br.gov.bcb.pix…",
"expiresAt": "2026-07-23T13:00:00.000Z",
"amount": 149.90,
"feeAmount": 5.25,
"netAmount": 144.65
}
}
pixCode é o copia-e-cola (EMV) — gere o QR a partir dele. Erros seguem { "success": false, "error": "<mensagem>" }.
Exemplo Node — com assinatura de requisição
import { createHmac, randomUUID } from 'node:crypto'
const API = 'https://app.revenpayments.com.br/api'
const API_KEY = process.env.CHECKOUT_API_KEY!
async function createCharge(body) {
const path = '/api/gateway/charges'
const raw = JSON.stringify(body)
const ts = Date.now().toString()
const nonce = randomUUID()
const canonical = `POST\n${path}\n${raw}\n${ts}\n${nonce}`
const signature = createHmac('sha256', API_KEY).update(canonical).digest('hex')
const res = await fetch(`${API}/gateway/charges`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': API_KEY,
'X-Signature': signature, // omita se o workspace não exige assinatura
'X-Timestamp': ts,
'X-Nonce': nonce,
},
body: raw,
})
const json = await res.json()
if (!json.success) throw new Error(json.error)
return json.data
}
Rastreio de anúncio no metadata #
Chaves opcionais que a plataforma passa a ler e armazenar quando enviadas no metadata da cobrança. Como metadata sempre foi objeto livre, isto não altera o contrato — não mandar nenhuma = comportamento de hoje, byte a byte.
O envio server-side já existe. Ao pagar, a plataforma envia a conversão para os destinos que você configurar no painel (Integrações → Destinos de conversão): GA4 (Measurement Protocol), Meta (Conversions API) e TikTok (Events API). Nada é enviado sem credencial sua, e cada destino tem botão de teste.
Deduplicação: use o transactionId
Se você também dispara a compra pelo pixel do navegador, use o mesmo transactionId devolvido pelo POST /gateway/charges no campo de dedupe. Sem isso, cada venda é contada duas vezes.
| Destino | Evento | Campo de dedupe |
|---|---|---|
| GA4 (Measurement Protocol) | purchase | transaction_id |
| Meta (Conversions API) | Purchase | event_id |
| TikTok (Events API) | CompletePayment | event_id |
Identificadores de clique aceitos
Todos string. Envie só os que você tem; ausente e null equivalem. Só existem no momento do clique — se não capturar e enviar, não são recuperáveis depois.
| Chave | Origem | Como obter |
|---|---|---|
gclid | Google Ads | ?gclid= da URL |
gbraid | Google Ads (app/iOS) | ?gbraid= |
wbraid | Google Ads (web/iOS) | ?wbraid= |
fbclid | Meta | ?fbclid= |
fbp | Meta | cookie _fbp (valor cru) |
fbc | Meta | cookie _fbc (valor cru) |
ttclid | TikTok | ?ttclid= |
ttp | TikTok | cookie _ttp |
gaClientId | GA4 | cookie _ga (client id) |
clickAt | você | ISO-8601 UTC da primeira captura |
Aceitamos a forma plana ou aninhada em trackingParameters — as duas produzem o mesmo resultado:
{ "metadata": { "pedido_id": "A-1001", "gclid": "Cj0KCQ…", "fbp": "fb.1.1690000000000.1234567890" } }
{ "metadata": { "pedido_id": "A-1001", "trackingParameters": { "gclid": "Cj0KCQ…" } } }
fbp e fbc não podem ser hasheados nem inventados: mande o valor cru do cookie ou nada. E não coloque valor, moeda, produto ou dados do comprador nessas chaves — isso já vem em product/customer e a transação é a fonte da verdade.
Listar transações #
Retorna as transações do seu workspace (paginado). Cada item traz status, amount, feeAmount, netAmount, endToEndId, paidAt, metadata. Use para reconciliação/polling se não puder receber webhook.
curl 'https://app.revenpayments.com.br/api/gateway/transactions?page=1&limit=20' \
-H 'X-Api-Key: SUA_API_KEY'
Janela padrão de 30 dias. Sem from, a listagem cobre só os últimos 30 dias — e meta.total/meta.pages contam o mesmo recorte. Para período mais antigo, mande from/to (ISO-8601, opcionais).
| Query param | Default | Regra |
|---|---|---|
page | 1 | inválido → cai no default |
limit | 20 | máx. 100; inválido → default |
from | — | created_at >= from; data ilegível = ausente |
to | — | created_at < to (fechado-aberto) |
Compare status exatamente em MAIÚSCULO (ex.: status === 'PAID'). Ver § Status & erros.
Receber o status por webhook #
Se você enviou callbackUrl, o gateway faz POST nessa URL a cada mudança de status. Headers: Content-Type: application/json, User-Agent: Reven-Webhook-Dispatcher/1.0 e X-Signature. Acompanham X-Webhook-Id, X-Timestamp e X-Signature-V2 (novos e opcionais — § V2).
{
"event": "transaction.paid",
"data": {
"transactionId": "b3f1…",
"acquirerTxId": "abc123",
"endToEndId": "E1234567890202607231240abcdef123",
"status": "PAID",
"amount": 149.90,
"feeAmount": 5.25,
"netAmount": 144.65,
"productName": "Plano Pro",
"customer": { "name": "João Silva", "email": "joao@exemplo.com" },
"metadata": { "pedido_id": "A-1001" },
"paidAt": "2026-07-23T12:40:00.000Z",
"createdAt": "2026-07-23T12:00:00.000Z"
}
}
| Campo | Valores / formato |
|---|---|
event | transaction.created · transaction.paid · transaction.failed · transaction.expired |
data.status | MAIÚSCULO — PENDING/PAID/FAILED/EXPIRED/MED |
data.endToEndId | E2E do PIX no Bacen (E+31). Pode vir null antes do pagamento |
Verificar a assinatura (X-Signature) #
O X-Signature é o HMAC-SHA256 do corpo bruto (o JSON exatamente como recebido, sem re-serializar) com o Webhook Secret do workspace. Recompute e compare antes de confiar no evento:
import { createHmac, timingSafeEqual } from 'node:crypto'
const WEBHOOK_SECRET = process.env.CHECKOUT_WEBHOOK_SECRET! // do painel, por workspace
function verify(rawBody, signatureHeader) {
const expected = createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex')
const a = Buffer.from(expected), b = Buffer.from(signatureHeader || '')
return a.length === b.length && timingSafeEqual(a, b)
}
// No seu endpoint: leia o corpo CRU (string) antes de parsear.
// if (!verify(rawBody, req.headers['x-signature'])) return res.status(401).end()
Boas práticas: verifique o X-Signature sobre o corpo bruto antes de processar · responda 200 rápido e processe assíncrono · seja idempotente por transactionId · só libere o pedido em event === 'transaction.paid' (e data.status === 'PAID') · case pelo seu metadata.
Assinatura V2 do webhook (anti-replay) #
O X-Signature cobre só o corpo — e, como nada nele envelhece, uma entrega interceptada tem assinatura válida para sempre. Todo webhook passa a trazer também três headers novos. É recomendado, não obrigatório: quem só valida o X-Signature segue suportado, sem prazo de corte.
X-Webhook-Id: <uuid da entrega> # igual em todas as retentativas
X-Timestamp: <epoch_ms> # instante da assinatura
X-Signature-V2: <hmac_hex>
canonical = `${X-Timestamp}\n${X-Webhook-Id}\n${corpo bruto}`
function verifyV2(rawBody, h) {
const ts = Number(h['x-timestamp'])
if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > 5 * 60_000) return false // fora da janela
const canonical = `${h['x-timestamp']}\n${h['x-webhook-id']}\n${rawBody}`
const expected = createHmac('sha256', WEBHOOK_SECRET).update(canonical).digest('hex')
const a = Buffer.from(expected), b = Buffer.from(h['x-signature-v2'] || '')
return a.length === b.length && timingSafeEqual(a, b)
}
Rejeite X-Timestamp fora de ±5 min e guarde o X-Webhook-Id já processado (10 min bastam) para descartar repetição. Retentativa automática reusa o mesmo X-Webhook-Id; reenvio manual pedido ao suporte vem com id novo e deve ser processado.
Criar saque — cashout / PIX OUT #
Dispara um saque programático do seu saldo disponível para uma chave PIX. Diferente das cobranças, tem verificação de assinatura dedicada e sempre obrigatória — a X-Api-Key sozinha nunca saca.
- X-Api-Key do workspace identifica você (middleware).
- Allowlist de IP (opt-in) — IP fora da lista recebe
403. Sem cadastro, sem restrição. - Assinatura HMAC com o Webhook Secret (NÃO a api key) — assim uma api key vazada sozinha não saca. Headers
X-Signature/X-Timestamp/X-Nonce. - Idempotency-Key (header obrigatório) — dedup atômico. Reenviar a MESMA key devolve o MESMO saque (
200), sem debitar de novo.
Corpo do request
{ "amount": 100.00, "callbackUrl": "https://sua-loja.com/wh/saque", "pixKey": "loja@exemplo.com", "pixKeyType": "email" }
| Campo | Obrig. | Regra |
|---|---|---|
amount | sim | > 0, reais. Debitado do saldo. Pode haver mínimo configurado (default: sem mínimo) |
callbackUrl | não | recebe os webhooks withdrawal.* |
pixKey + pixKeyType | não | ambos ou nenhum. Destino livre (rotação por saque). Omitindo, usa a chave do painel |
pixKeyType canônico ∈ cpf | cnpj | email | phone | random (case-insensitive). Aliases normalizados:
| Você envia | Vira |
|---|---|
evp, aleatoria, aleatória, chave_aleatoria | random |
telefone, celular | phone |
e-mail | email |
Assinatura do saque
Igual à assinatura de requisição, mas a chave é o Webhook Secret e o Idempotency-Key entra no canônico:
canonical = `${METHOD}\n${path}\n${body}\n${timestamp}\n${nonce}\n${idempotencyKey}`
X-Signature = HMAC-SHA256(chave = webhook_secret, mensagem = canonical) # hex
import { createHmac, randomUUID } from 'node:crypto'
const API = 'https://app.revenpayments.com.br/api'
const API_KEY = process.env.CHECKOUT_API_KEY!
const WEBHOOK_SECRET = process.env.CHECKOUT_WEBHOOK_SECRET! // o mesmo do painel
async function withdraw(amount, dest) {
const path = '/api/gateway/withdrawals'
const raw = JSON.stringify({ amount, ...dest })
const ts = Date.now().toString()
const nonce = randomUUID()
const idempotencyKey = randomUUID() // reusar no retry do MESMO saque
const canonical = `POST\n${path}\n${raw}\n${ts}\n${nonce}\n${idempotencyKey}`
const signature = createHmac('sha256', WEBHOOK_SECRET).update(canonical).digest('hex')
const res = await fetch(`${API}/gateway/withdrawals`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': API_KEY,
'X-Signature': signature,
'X-Timestamp': ts,
'X-Nonce': nonce,
'Idempotency-Key': idempotencyKey,
},
body: raw,
})
const json = await res.json()
if (!json.success) throw new Error(json.error)
return json.data // { withdrawalId, status, amount, feeAmount, netAmount } — reais
}
Taxa de saque (feeAmount / netAmount)
A plataforma pode cobrar taxa por saque, descontada do valor sacado. Campos aditivos e opcionais: com taxa zero (default) valem 0 e netAmount == amount — resposta idêntica à de sempre.
| Campo | O que é |
|---|---|
amount | o valor que você pediu — e o que debita do saldo |
feeAmount | a taxa cobrada neste saque, em reais |
netAmount | o que cai na chave PIX: amount − feeAmount |
Taxa ≥ valor pedido → saque recusado com 400 + { "success": false, "error": "Valor do saque não cobre a taxa de saque" }, nada debitado. O admin pode bloquear saques da conta → 403 "Saques bloqueados por determinação administrativa."
Webhooks do saque (se enviar callbackUrl): withdrawal.created, withdrawal.paid, withdrawal.failed — assinados no X-Signature com o mesmo Webhook Secret e com os headers V2. A pixKey vem mascarada no payload.
Consultar um saque #
Retorna o saque, desde que pertença à company da sua X-Api-Key (isolamento por workspace).
{
"success": true,
"data": {
"withdrawalId": "…",
"status": "ENVIADO",
"amount": 100.00,
"pixKeyType": "email",
"createdAt": "2026-07-23T12:00:00.000Z"
}
}
Se requireSignature estiver ativo, esta leitura também exige a assinatura de requisição (§ Assinatura), assinada com a API Key.
Allowlist de IP (opcional) #
Você pode restringir as rotas de saque (POST /gateway/withdrawals e GET /gateway/withdrawals/:id) a um conjunto de IPs de saída. IP fora da lista responde 403. Sem cadastro, nenhuma restrição.
O cadastro é feito no seu painel (segurança do workspace), com a sua sessão do painel — não faz parte da superfície X-Api-Key//gateway/*. Alterar a lista exige confirmar a senha atual (step-up); sem ela, 401 STEP_UP_REQUIRED.
Status & erros #
Status de transação (sempre MAIÚSCULO)
| Status | Significa |
|---|---|
PENDING | aguardando pagamento |
PAID | pago e confirmado — único gatilho para liberar o pedido |
FAILED | falhou |
EXPIRED | PIX venceu (pixExpiresAt) — reportado mesmo antes do aviso do adquirente; deixa de contar no saldo pendente |
MED | contestado/chargeback (foi pago e revertido) — trate como não-pago |
Status de saque
| Status | Significa |
|---|---|
PENDING | criado, aguardando processamento |
ENVIADO | PIX enviado |
RECUSADO | recusado pelo liquidante |
CANCELADO | cancelado |
BLOQUEADO | retido/bloqueado |
Códigos HTTP
| Código | Quando |
|---|---|
| 401 | X-Api-Key ausente/inválida; assinatura ausente/errada; timestamp fora da janela; nonce reusado |
| 403 | workspace/company inativo; IP fora da allowlist; saques bloqueados pelo admin |
| 400 | body inválido; saldo insuficiente; abaixo do mínimo; taxa ≥ valor; falta destino/idempotency-key |
| 429 | flood de credencial inválida por IP — respeite o Retry-After |
| 503 | verificação de nonce indisponível em produção (raro) |
Todo erro segue o envelope { "success": false, "error": "<mensagem>" }.
Checklist de integração #
X-Api-KeyeWebhook Secretguardados como segredo (env), nunca no cliente.POST /gateway/chargesgerandopixCodee exibindo o QR/copia-e-cola.- Valores tratados em reais (não centavos) no request e nas respostas/webhooks.
- Endpoint de
callbackUrlverifica oX-Signature(HMAC do corpo bruto), responde200e é idempotente portransactionId. - (Recomendado)
X-Signature-V2verificado,X-Timestampdentro de ±5 min eX-Webhook-Iddeduplicado. statuscomparado em MAIÚSCULO (PAIDetc.); pedido liberado só emtransaction.paid.metadatacasa o pagamento com o pedido interno;endToEndIdguardado para conciliação bancária.- (Se exigido) assinatura HMAC de requisição implementada e testada na janela de ±5 min.
- Saque: assinatura com o Webhook Secret (não a api key) incluindo o
Idempotency-Keyno canônico; key reusada no retry; (opcional) IPs de saída na allowlist.