Pagora
Referência da API
Cobrar por Pix e ser avisado quando o dinheiro cai. Três rotas e um webhook.
A chave
Base: https://api.pagora.com.br. Toda rota /v1 pede a chave
no header Authorization. Ela é criada no painel e aparece em claro uma vez.
curl https://api.pagora.com.br/v1/charges \
-H 'Authorization: Bearer sk_live_...'
| Prefixo | O que é |
|---|---|
sk_live_ | Opera a loja de verdade. |
sk_test_ | Mesma API, sem mover dinheiro. Em conta de produção responde 403. |
Enquanto a loja não for aprovada, toda rota /v1 responde
403 com um campo status: pending_kyc,
in_review, rejected ou blocked.
Convenções
Campo terminado em Cents é inteiro em centavos. R$ 100,00 é
10000. Fração e string são recusadas com 400.
Campo terminado em At é ISO 8601 em UTC, com milissegundos e
sufixo Z: "2026-07-16T13:00:00.000Z".
Id de cobrança tem prefixo ch_ e não é sequencial.
Criar cobrança
O corpo inteiro, com tudo que existe:
curl -X POST https://api.pagora.com.br/v1/charges \
-H 'Authorization: Bearer sk_live_...' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: pedido-4711' \
-d '{
"amountCents": 10000,
"description": "Pedido 4711",
"postbackUrl": "https://seu-sistema.com/pagora/aviso",
"pagador": {
"nome": "Ana Souza",
"documento": "00000000000",
"email": "ana@exemplo.com",
"celular": "11999998888"
},
"beneficiario": {
"nome": "Loja do Cliente ME",
"documento": "00000000000000"
}
}'
| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
amountCents | inteiro | sim | Centavos, maior que zero. Hoje o mínimo é R$ 5,00. |
pagador.nome | texto | sim | Como está no documento. Até 120 caracteres. |
pagador.documento | texto | sim | CPF ou CNPJ, com ou sem pontuação. CNPJ alfanumérico aceito. |
pagador.email | texto | sim | Precisa ter @, até 254 caracteres. |
pagador.celular | texto | sim | Com DDD, 10 ou 11 dígitos. Pontuação é ignorada. |
beneficiario.documento | texto | só se você revende | CPF ou CNPJ de quem vendeu. Dígito verificador é conferido. |
beneficiario.nome | texto | não | Até 120 caracteres. |
description | texto | não | Livre. Volta na criação, na consulta e na lista. |
postbackUrl | texto | não | https://, até 2048 caracteres. O aviso vai pra ele e pros endereços cadastrados no painel; se a URL for a mesma de um deles, não repetimos. IP privado, localhost e URL com credencial são recusados. |
metodo | texto | não | Omita. A conta emite só Pix. |
Idempotency-Key é header, opcional. Mesma chave e mesmo valor devolve a
cobrança da primeira chamada. Mesma chave e valor diferente devolve 422.
A resposta, inteira · 201
{
"id": "ch_kkD4vGQxN3m1sJb2aZ7cUg",
"status": "pending",
"brCode": "00020126...6304ABCD",
"amountCents": 10000,
"feeBps": 150,
"feeFixedCents": 0,
"feeCents": 150,
"netCents": 9850,
"description": "Pedido 4711",
"postbackUrl": "https://seu-sistema.com/pagora/aviso",
"pagador": null,
"endToEndId": null,
"qrImageUrl": "https://.../qr.png",
"metodo": "pix",
"linhaDigitavel": null,
"boletoUrl": null,
"createdAt": "2026-07-16T13:00:00.000Z",
"expiresAt": "2026-07-16T14:00:00.000Z",
"paidAt": null
}
| Campo | O que é |
|---|---|
id | O id da cobrança. Sua chave de conciliação. |
brCode | O copia-e-cola. Desenhe o QR a partir dele. |
status | pending · paid · expired. |
netCents | O que cai no saldo. Use este, não refaça a conta. |
feeCents | A taxa da venda. feeBps e feeFixedCents são as duas partes dela. |
expiresAt | Até quando o QR pode ser pago. Não há parâmetro de validade. |
endToEndId | Número do comprovante no banco. Só vem quando status é paid; nos outros é null. |
pagador | null em cobrança criada pela API. |
qrImageUrl | Conveniência. Pode vir vazia. |
metodo linhaDigitavel boletoUrl | "pix" e dois null. Não condicione código a eles. |
Consultar
Mesmo formato da resposta acima. Depois de paga: status: "paid" e
paidAt preenchido.
Listar
{ "data": [ { ...igual à resposta acima... } ], "hasMore": true }
| Parâmetro | Valor |
|---|---|
limit | 1 a 100. Padrão 50. |
starting_after | O id do último item recebido. É o cursor. |
status | pending · paid · expired |
created_after | ISO 8601 com Z. Inclusive. |
created_before | ISO 8601 com Z. Inclusive. |
Enquanto hasMore for true, repita mandando
starting_after = o id do último item.
Webhook
POST na sua URL quando a venda é paga. Responda qualquer 2xx em
até 10 segundos. Sem 2xx, são 6 tentativas no total, esperando
10s, 20s, 40s, 80s, 160s entre elas: cerca de 5 minutos do começo ao fim.
Dá pra cadastrar mais de um endereço no painel, o seu sistema e o do seu parceiro por exemplo. Cada venda paga vira um aviso pra cada um, e cada endereço tem o seu próprio segredo: trocar o de um não mexe nos outros.
A postbackUrl da cobrança soma com esses endereços: o aviso
vai pra ela e pra todos eles. Se a postbackUrl for igual a um endereço
cadastrado, esse endereço não recebe duas vezes.
{
"type": "charge.paid",
"createdAt": "2026-07-16T18:20:31.554Z",
"data": {
"chargeId": "ch_HuKkN92_2Bx_ZdDH8vxE-w",
"status": "paid",
"amountCents": 10000,
"feeCents": 150,
"netCents": 9850,
"endToEndId": "E31872495202607161820Yk7mQ2xLp4t",
"paidAt": "2026-07-16T18:20:31.554Z"
}
}
| Header | Valor |
|---|---|
User-Agent | pagora-webhooks/1 |
X-Pagora-Timestamp | segundos desde a época Unix |
X-Pagora-Signature | sha256=<64 hex> |
| Sua resposta | O que fazemos |
|---|---|
2xx | Entregue. Acabou. |
429 ou 5xx | Tentamos de novo. |
4xx (fora o 429) | Paramos na hora. Nenhuma retentativa. |
3xx | Paramos na hora: não seguimos redirect. |
| nenhuma (timeout, DNS, conexão) | Tentamos de novo. |
A URL precisa ser https://. Uma entrega pode chegar duas vezes,
então guarde o chargeId já processado e ignore repetição.
Conferir a assinatura · opcional
const { createHmac, timingSafeEqual } = require('node:crypto')
// corpoCru = os BYTES exatos recebidos, ANTES de qualquer JSON.parse
function avisoValido(segredo, headers, corpoCru) {
const ts = headers['x-pagora-timestamp']
const esperada = 'sha256=' + createHmac('sha256', segredo)
.update(`${ts}.${corpoCru}`).digest('hex')
const a = Buffer.from(esperada)
const b = Buffer.from(headers['x-pagora-signature'] ?? '')
if (a.length !== b.length || !timingSafeEqual(a, b)) return false
// recusa aviso com mais de 5 min de diferença
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false
return true
}
O segredo é de cada endereço, fica no painel, na mesma
linha da URL, e aparece em claro uma vez. Sem conferir assinatura, trate o aviso como gatilho
e confirme com GET /v1/charges/:id.
Erros
{ "error": "amountCents inválido: precisa ser um inteiro de centavos maior que zero" }
Quando a recusa é de regra, vem junto um motivo. Faça o if no
motivo, nunca na mensagem.
| Código | Quando |
|---|---|
400 | Campo faltando, tipo errado, valor fora do formato. |
401 | Chave ausente, inválida ou revogada. |
403 | Loja não aprovada (vem com status), operação fora da chave (vem com motivo), ou sk_test_ em conta de produção. |
404 | Não existe pra você. Com message, quem não existe é a rota. |
415 | Falta Content-Type: application/json. |
422 | Pedido certo, regra não deixa. Ver a tabela abaixo. |
429 | Passou de 1200 por minuto, por conta. Vem com Retry-After: 60. |
5xx | Problema do nosso lado. Repita o mesmo pedido, com a mesma Idempotency-Key. |
motivo | O que fazer |
|---|---|
valor_abaixo_do_minimo | Vem com minimoCents e pedidoCents. Leia o minimoCents em vez de fixar o número. |
valor_acima_do_teto | Vem com tetoCents e pedidoCents. Divida o pedido. |
pagador_incompleto | Faltou um dos quatro campos do pagador. Vem com a lista do que falta. |
pagador_invalido | Um campo do pagador veio e não serve. Vem com campo. |
postback_url_invalida | A postbackUrl não passou. A cobrança não nasce. |
metodo_nao_suportado | A conta emite só Pix. |
beneficiario_invalido | Documento do beneficiário não passa no dígito verificador. |
idempotencia_conflito | Mesma chave com valor diferente. Vem com gravadoCents e pedidoCents. |
adquirente_recusou | Recusa final. Repetir dá o mesmo resultado. |
fora_do_escopo_da_chave | Saldo, devolução e saque ficam no painel. A chave só cria e consulta cobrança. |
No 5xx vem um reqId. Mande ele no chamado.
O documento dos exemplos é inválido de propósito. Um CPF real copiado
daqui chega em produção como se fosse de alguém. Troque pelo documento de quem está
comprando, senão a resposta é 400.