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_...'
PrefixoO 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

POST/v1/charges

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"
    }
  }'
CampoTipoObrigatórioRegra
amountCentsinteirosimCentavos, maior que zero. Hoje o mínimo é R$ 5,00.
pagador.nometextosimComo está no documento. Até 120 caracteres.
pagador.documentotextosimCPF ou CNPJ, com ou sem pontuação. CNPJ alfanumérico aceito.
pagador.emailtextosimPrecisa ter @, até 254 caracteres.
pagador.celulartextosimCom DDD, 10 ou 11 dígitos. Pontuação é ignorada.
beneficiario.documentotextosó se você revendeCPF ou CNPJ de quem vendeu. Dígito verificador é conferido.
beneficiario.nometextonãoAté 120 caracteres.
descriptiontextonãoLivre. Volta na criação, na consulta e na lista.
postbackUrltextonãohttps://, 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.
metodotextonãoOmita. 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
}
CampoO que é
idO id da cobrança. Sua chave de conciliação.
brCodeO copia-e-cola. Desenhe o QR a partir dele.
statuspending · paid · expired.
netCentsO que cai no saldo. Use este, não refaça a conta.
feeCentsA taxa da venda. feeBps e feeFixedCents são as duas partes dela.
expiresAtAté quando o QR pode ser pago. Não há parâmetro de validade.
endToEndIdNúmero do comprovante no banco. Só vem quando status é paid; nos outros é null.
pagadornull em cobrança criada pela API.
qrImageUrlConveniência. Pode vir vazia.
metodo linhaDigitavel boletoUrl"pix" e dois null. Não condicione código a eles.

Consultar

GET/v1/charges/:id

Mesmo formato da resposta acima. Depois de paga: status: "paid" e paidAt preenchido.

Listar

GET/v1/charges
{ "data": [ { ...igual à resposta acima... } ], "hasMore": true }
ParâmetroValor
limit1 a 100. Padrão 50.
starting_afterO id do último item recebido. É o cursor.
statuspending · paid · expired
created_afterISO 8601 com Z. Inclusive.
created_beforeISO 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"
  }
}
HeaderValor
User-Agentpagora-webhooks/1
X-Pagora-Timestampsegundos desde a época Unix
X-Pagora-Signaturesha256=<64 hex>
Sua respostaO que fazemos
2xxEntregue. Acabou.
429 ou 5xxTentamos de novo.
4xx (fora o 429)Paramos na hora. Nenhuma retentativa.
3xxParamos 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ódigoQuando
400Campo faltando, tipo errado, valor fora do formato.
401Chave ausente, inválida ou revogada.
403Loja não aprovada (vem com status), operação fora da chave (vem com motivo), ou sk_test_ em conta de produção.
404Não existe pra você. Com message, quem não existe é a rota.
415Falta Content-Type: application/json.
422Pedido certo, regra não deixa. Ver a tabela abaixo.
429Passou de 1200 por minuto, por conta. Vem com Retry-After: 60.
5xxProblema do nosso lado. Repita o mesmo pedido, com a mesma Idempotency-Key.
motivoO que fazer
valor_abaixo_do_minimoVem com minimoCents e pedidoCents. Leia o minimoCents em vez de fixar o número.
valor_acima_do_tetoVem com tetoCents e pedidoCents. Divida o pedido.
pagador_incompletoFaltou um dos quatro campos do pagador. Vem com a lista do que falta.
pagador_invalidoUm campo do pagador veio e não serve. Vem com campo.
postback_url_invalidaA postbackUrl não passou. A cobrança não nasce.
metodo_nao_suportadoA conta emite só Pix.
beneficiario_invalidoDocumento do beneficiário não passa no dígito verificador.
idempotencia_conflitoMesma chave com valor diferente. Vem com gravadoCents e pedidoCents.
adquirente_recusouRecusa final. Repetir dá o mesmo resultado.
fora_do_escopo_da_chaveSaldo, 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.