pagyou/API 2026-08-15

Gateway Pix · API HTTP/JSON

Receba e envie Pix com integridade contábil por construção.

Uma API pequena, previsível e idempotente. Cobranças por QR dinâmico, saques para chave, saldo por balde, extrato que fecha com o saldo, webhooks assinados com re-envio pelo próprio cliente — e uma conta que você administra sem abrir chamado.

Base URL
https://api.pagyou.com
Formato
JSON UTF-8 · datas RFC 3339 (UTC) · ids UUID
Valores
inteiros na unidade menor — 1050 = R$ 10,50
Contrato
OpenAPI 3.1 gerado do mesmo binário que atende

Autenticação

Toda chamada leva a chave no cabeçalho Authorization: Bearer. Chaves têm prefixo visível (pk_live_a1b2c3d4…) para você identificá-las no painel sem revelá-las; o restante é aleatório de 256 bits e só o hash fica do nosso lado. A chave em claro aparece uma única vez, quando é criada.

Escopos

Cada chave carrega os escopos que pode usar. Uma chave de dashboard não precisa poder criar saque.

EscopoPermite
charges:writecriar cobranças e pedir devolução
charges:readconsultar e listar cobranças
payouts:writecriar saques
payouts:readconsultar e listar saques
balance:readsaldo
statement:readextrato
account:manageadministrar a própria conta: chaves, allowlist, webhooks, entregas. É a raiz da sua conta — quem tem este escopo pode emitir chave com qualquer escopo, inclusive os que a própria chave não tem. Trate-a como a senha da conta e não a entregue a terceiros.

Allowlist de IP opcional

Você pode restringir as chamadas com chave a faixas de IP (self-service). Uma chave válida vinda de IP fora da lista recebe 403 ip_not_allowed com o IP que vimos — e do nosso lado vira evento de segurança, porque ninguém acerta uma chave de 256 bits por acidente de um servidor errado. A API recusa que você se tranque fora: ligar a lista sem cobrir o próprio IP é 409 would_lock_out.

Chaves de teste e de produção. Hoje a plataforma opera integrada ao sandbox do liquidante — dinheiro simulado. As chaves emitidas são pk_test_. Quando a operação real for ligada, você emite chaves pk_live_ na mesma conta; nada mais muda na integração.

Convenções

  • Dinheiro é inteiro. Todo valor é um inteiro na menor unidade da moeda (amount: 1050 = R$ 10,50). Nunca float, nunca string decimal. Cada conta e cada operação é mono-moeda (currency, padrão BRL).
  • Datas em RFC 3339 com fuso UTC (2026-08-15T14:04:06Z). Identificadores são UUID; a sua referência (external_ref) é texto livre até 128 caracteres, único por conta. description vai até 200 (cobrança) ou 140 (saque, o que cabe no que o recebedor vê), e pix_key até 77 — os limites do Pix. Passar deles é 422, nunca truncamento em silêncio.
  • Campos desconhecidos são erro (400 invalid_json): errar o nome de um campo não passa em silêncio.
  • Campos ausentes são omitidos na resposta (não vêm como null), exceto onde a documentação diz o contrário. Novos campos podem aparecer a qualquer momento — ignore o que não conhece.
  • Máquinas de estado só avançam. Um paid nunca volta a pending. Eventos podem chegar fora de ordem ou repetidos: trate pelo estado, não pela ordem.
  • Toda resposta traz request_id nos erros (e no cabeçalho X-Request-Id). Cite-o ao falar com o suporte.

Idempotência

Toda escrita é idempotente pela sua referência: external_ref. Repetir a mesma chamada — retry de rede, replay de fila, deploy no meio — devolve o mesmo recurso, com o mesmo id, sem efeito colateral. Repetir external_ref com corpo diferente é recusado com 409 idempotency_conflict, e a mensagem traz os dois pedidos lado a lado, para você achar o bug em minutos.

Regra de ouro dos saques. Diante de timeout ou 202 processing, nunca crie um novo saque com outra referência "para garantir". Consulte GET /v1/payouts/{id} ou espere o webhook. Repetir com o mesmo external_ref é sempre seguro.

Erros

Erros são JSON com um objeto error: code (estável, para o seu código), message (para humanos, em português), origin (de quem é a falha) e request_id.

HTTPcodeQuando
400invalid_jsoncorpo malformado ou campo desconhecido
401unauthorizedchave ausente, inválida ou revogada
403ip_not_allowed · scope_deniedIP fora da allowlist (traz o IP visto) · chave sem o escopo
404not_foundrecurso inexistente ou de outra conta
409idempotency_conflict · already_exists · last_manage_key · would_lock_out · refund_already_requested · charge_disputedconflitos de estado
422validation_error · invalid_input · insufficient_funds · invalid_state · unsupported_key_type · amount_too_large · amount_below_fee · payer_required · refund_unsupported · refund_exceeds_charge · códigos do liquidantepedido válido em forma, recusado em conteúdo
429rate_limitedcota excedida — veja Retry-After
503liquidator_unavailable · no_liquidatorindisponibilidade do lado do liquidante — do nosso lado já há alerta; tente em instantes

Erros 5xx nunca significam que dinheiro se moveu sem registro: a operação ou aconteceu inteira, ou não aconteceu. Diante de dúvida, consulte o recurso pelo external_ref.

De quem é a falha

Todo erro traz origin, e todo saque falhado traz failure_origin — o mesmo vocabulário, na resposta e no webhook. Ele existe para que você não precise adivinhar a culpa pela semântica do código, e para que a resposta diga o que fazer:

originSignificaO que fazer
cliento pedido precisa mudar — validação, saldo, chave Pix, idempotência, conta do recebedorcorrija e reenvie; reenviar igual falha de novo
liquidatoro parceiro bancário caiu, recusou, demorou ou segurou para análise — não é problema do seu códigonuma resposta síncrona (503), repita em instantes com o mesmo external_ref; num saque, não reenvie — consulte pelo id, o desfecho chega por webhook
pagyoué nosso, e é bug — do nosso lado já há alertaabra chamado citando o request_id (ou o id do saque)

Programe contra code; use origin para decidir quem age. Um mesmo código tem sempre a mesma origem.

Rate limits

Limites são por conta, separados em leitura (GET) e escrita, no modelo token bucket: uma rajada até o burst passa inteira; depois, rate_per_sec por segundo. São generosos por desenho — a integridade do dinheiro está nas constraints, não na cota — e são parâmetro comercial: cliente de volume é cadastrado com limites altos.

Toda resposta autenticada traz X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Class. Ao estourar: 429 rate_limited com Retry-After em segundos. Aplique backoff em vez de martelar; e prefira webhooks a polling — é a leitura que tem a cota mais curta.

Consulte os seus limites em GET /v1/account/limits. Nós somos avisados quando você chega a 80% — normalmente ligamos antes de você bater no teto.

Paginação

Listagens devolvem data (mais recentes primeiro) e next_cursor. Repita a chamada com cursor=<next_cursor> até vir null. O cursor é opaco e estável — não o interprete. limit vai de 1 a 200 (padrão 50). As entregas de webhook paginam por before_id.

POST /v1/charges charges:write

Cria um Pix dinâmico e devolve o payload do QR (emv, o "copia-e-cola"). Você exibe o QR ou o texto; quando o pagador paga, a cobrança vira paid, o saldo disponível recebe o valor líquido da tarifa e você recebe charge.paid.

Corpo

external_refobrigatóriostring — sua referência única; chave de idempotência.
amountobrigatóriointeiro > 0 na unidade menor.
currencyBRL (padrão).
descriptiontexto exibido ao pagador quando o liquidante suporta.
expires_in_secondspadrão 3600. O liquidante pode limitar (o sandbox limita a 600 s) — vale o expires_at devolvido.
payeropcional: {name, document, phone, email} de quem vai pagar — CPF/CNPJ válido, telefone com DDD. Quando o liquidante da sua conta exige, a falta dele é 422 payer_required. Numa repetição da mesma external_ref, vale o pagador da primeira chamada.

Resposta 201

id, external_ref, amount, currency, fee (a tarifa que será descontada), status (pending), emv, expires_at. Repetir com o mesmo external_ref e valor devolve a mesma cobrança, também com 201.

O objeto cobrança

A mesma projeção em toda parte: no GET, na listagem e no data do webhook. Campos ausentes são omitidos.

statuspending paid expired cancelled failed refunded
feetarifa descontada do valor bruto ao pagar. Líquido = amount − fee.
e2e_idEndToEndId do Pix recebido — o identificador que o pagador vê no extrato dele.
paid_at · refunded_at · refund_requested_atmarcos temporais; refund_requested_at aparece quando você pediu a devolução e a confirmação ainda não chegou (o valor já está reservado).
retentionquando há retenção de risco configurada: amount retido, release_at previsto, released_at quando liberou. O saldo retido aparece no balde held.
pending→paid→refunded pending → expired · cancelled · failed

GET /v1/charges · /v1/charges/{id} charges:read

Filtros: status, external_ref, limit, cursor. Consulta individual por id. Um id de outra conta é 404 — nunca 403: não confirmamos existência do que não é seu.

POST /v1/charges/{id}/refund charges:write

Devolução de uma cobrança paga, iniciada por você — parcial ou total. Corpo vazio devolve o que resta; {"amount": X} devolve X (na unidade menor). O necessário é reservado do seu saldo disponível antes de falarmos com o liquidante — sem saldo, 422 insufficient_funds e nada acontece. Confirmação assíncrona: 202 {"status":"refund_requested"}, depois charge.partially_refunded a cada parcial e charge.refunded quando a cobrança fechar (o campo refunded do objeto mostra o acumulado). Um pedido em voo por vez: repetir o mesmo valor é retry seguro; valor diferente antes do desfecho, 409 refund_already_requested; acima do que resta, 422 refund_exceeds_charge; cobrança sob disputa (MED aberto), 409 charge_disputed — aguarde o desfecho.

POST /v1/payouts payouts:write

Envia Pix para uma chave. O valor mais a tarifa saem do saldo disponível de forma atômica — mil pedidos simultâneos contra saldo para um pagam um; os outros recebem 422 insufficient_funds. O saque nasce requested e passa a processing assim que o liquidante aceita; o desfecho vem por webhook e pela consulta.

external_refobrigatóriosua referência única.
amountobrigatóriointeiro > 0 na unidade menor.
pix_keyobrigatórioCPF, CNPJ, e-mail, telefone (+55…) ou chave aleatória (EVP).
pix_key_typeCPF · CNPJ · EMAIL · PHONE · EVP. Opcional quando o formato é inequívoco (e-mail tem @, chave aleatória é UUID, 11 dígitos é CPF, 14 é CNPJ). Telefone precisa do campo ou do DDI (+5511999998888): 11 dígitos sem + são CPF, e não adivinhamos para onde vai dinheiro de terceiro.
descriptionaté 140 caracteres, exibido ao recebedor.
Limites: external_ref 128 · pix_key 77 · description 140.

Respostas

201 saque aceito (status requested ou processing). 202 {"id":…,"status":"processing"}: o saque existe e segue em voo — o liquidante respondeu de forma ambígua ou estava indisponível; ele pode ter saído, ou vai sair. Consulte pelo id; não refaça com outra referência. 422 recusa definitiva com o código de falha (insufficient_funds, pix_key_not_found, amount_not_allowed, liquidity_unavailable…), sem débito.

O objeto saque

requested→processing→completed processing → failed (crédito de volta) · completed → returned (devolvido pelo recebedor; crédito de volta) rejected só acontece de forma síncrona, na criação.

failure_code/failure_reason/failure_origin aparecem em failed (e failure_code também em rejected e returned). failure_origin diz de quem é a falha — na leitura e no webhook. Códigos estáveis: pix_key_not_found, pix_key_invalid, recipient_bank_rejected, recipient_account_invalid, amount_not_allowed, recipient_timeout, liquidity_unavailable, processing_failed, request_rejected. liquidity_unavailable não é o seu saldo (esse é o 422 insufficient_funds da criação): é indisponibilidade momentânea do lado do liquidante — repita mais tarde, com nova referência. Toda falha devolve o valor e a tarifa ao saldo disponível na mesma transação em que é registrada.

GET /v1/payouts · /v1/payouts/{id} payouts:read

Filtros: status, external_ref, limit, cursor.

GET /v1/balance balance:read

Um item por moeda, com os baldes do seu saldo. Só available pode sair em saque.

availabledisponível para saque e devolução.
blockedreservado por saques em andamento e devoluções pedidas — volta a available se falharem.
heldretenção de risco (prazo configurado); libera para available automaticamente.
med_heldbloqueado por um MED (Mecanismo Especial de Devolução) em análise.
owedrecebível: o que você nos deve quando uma devolução ou MED superou o saldo. Zero na operação normal.

GET /v1/statement statement:read

Cada linha é um lançamento contábil real, com sinal (crédito positivo, débito negativo) e o balde de saldo afetado. A soma dos amount do balde available é exatamente o seu saldo disponível — o extrato fecha por construção. A tarifa não é um segundo lançamento: Pix entra líquido e saque sai valor+tarifa; o campo fee mostra o que foi cobrado naquela operação. summary agrega o recorte inteiro (entradas, saídas, tarifas, disponível no fim). Filtros: currency, type, bucket, from, to (RFC 3339), limit, cursor. Com format=csv a resposta é CSV e o cursor vem no cabeçalho Pagyou-Next-Cursor.

type descreve a natureza do lançamento (cash_in_paid, payout_completed, cash_in_refunded…); subject_type/subject_id apontam para a cobrança ou o saque que o originou.

Webhooks

Cadastre um endpoint https:// (self-service) e receba um POST a cada mudança de estado que importa. O data é a mesma projeção do GET — o que você recebe no push é o que veria consultando; nunca há um terceiro formato. Responda 2xx em até 10 segundos; faça o trabalho pesado depois.

Pagyou-Event-IdUUID estável entre retentativas — deduplique por ele.
Pagyou-Event-Typeo tipo (charge.paid, payout.completed…), também em type no corpo.
Pagyou-Signaturet=<unix>,v1=<hex>[,v1=<hex>] — veja assinatura.

Assinatura

HMAC-SHA256 em hexadecimal do texto "<t>." + corpo cru, com o segredo do endpoint (whsec_…). O timestamp está dentro da assinatura: um replay de uma entrega capturada expira. Verifique sobre os bytes crus do corpo, antes de qualquer parse, com comparação em tempo constante; recuse se |agora − t| passar de 5 minutos.

Durante uma rotação de segredo, o cabeçalho traz duas v1= (segredo novo e anterior). Aceite se qualquer uma conferir com o segredo que você tem — assim você troca o segredo do seu lado sem perder entrega.

Eventos

TipoQuandodata
charge.paido Pix entrou; saldo creditado (líquido)cobrança
charge.expired · charge.cancelled · charge.faileda cobrança encerrou sem pagamento. charge.expired sai pelo nosso relógio, alguns minutos depois de expires_at; um Pix confirmado depois disso ainda entra e gera charge.paidcobrança
charge.partially_refundeddevolução parcial aplicada — o campo refunded traz o acumuladocobrança
charge.refundeddevolução concluída (sua ou do liquidante)cobrança
payout.processingo liquidante aceitou o saquesaque
payout.completedo recebedor recebeusaque
payout.failedfalhou; valor e tarifa voltaram ao disponívelsaque
payout.returnedo recebedor devolveu; valor voltou ao disponívelsaque

Entrega e retentativas

Falhou (não-2xx, timeout, recusa TLS)? Tentamos de novo em 1 min, 5 min, 30 min, 2 h, 8 h, 24 h e 24 h — oito tentativas, ~2,6 dias. Depois a entrega fica exhausted: nada se perde; ela aparece em GET /v1/account/deliveries?status=exhausted e você a reenvia quando quiser, com o mesmo event_id. Também é possível reenviar uma entrega já delivered (se você a perdeu do seu lado).

Endpoint desativado (status: disabled) não recebe: as entregas pendentes dele são encerradas. Cliente sem endpoint ativo simplesmente não recebe push — o GET continua sendo a verdade.

Verificador de assinatura

Cole o segredo do endpoint, o cabeçalho Pagyou-Signature e o corpo exatamente como recebido. O cálculo roda no seu navegador (WebCrypto); nada é enviado.

Chaves de API account:manage

Você emite, lista e revoga as próprias chaves. Rotação sem downtime: crie a nova, migre, revogue a antiga. A API recusa revogar a última chave ativa com account:manage (409 last_manage_key) — você nunca fica sem acesso à própria conta.

GET /v1/account/keyslista (prefixo, rótulo, escopos, último uso). A chave em claro nunca aparece aqui.
POST /v1/account/keys{label, scopes[], live?} → 201 com key em claro, uma vez.
DELETE /v1/account/keys/{id}revoga na hora (204).

Allowlist de IP account:manage

GET /v1/account/allowlistestado (enabled) e faixas.
POST /v1/account/allowlist{cidr, description?}. IP sem máscara vira /32. Bits fora da máscara são recusados.
DELETE /v1/account/allowlist/{id}com a lista ligada, recusa remover a única faixa que cobre o seu IP atual (409 would_lock_out).
PUT /v1/account/allowlist/enabled{enabled}. Ligar exige que o IP de quem chama esteja coberto. Desligar sempre pode.
GET /v1/account/ipo teste: o IP que vemos e se passaria. Rode antes de ligar e ao migrar de infra.
Migrando de infraestrutura? Cadastre a faixa nova, confira com GET /v1/account/ip a partir da infra nova, e só então remova a antiga. Se mesmo assim ficar trancado fora, o suporte desliga o interruptor para você religar.

Endpoints de webhook account:manage

GET /v1/account/webhookslista (sem segredos).
POST /v1/account/webhooks{url, description?}, https:// obrigatório → 201 com secret uma vez.
PATCH /v1/account/webhooks/{id}{url?, description?, status?} (active | disabled).
DELETE /v1/account/webhooks/{id}apaga → 204. O histórico de entregas continua consultável; a URL pode voltar.
POST /v1/account/webhooks/{id}/rotate-secret{grace_hours?} (padrão 24, máx. 168) → segredo novo; o anterior segue válido pela janela e as entregas levam as duas assinaturas.

Entregas e reenvio account:manage

GET /v1/account/deliveriesfiltros status (pending · delivered · exhausted), endpoint_id, before_id, limit. Traz tentativas, último HTTP status e erro.
POST /v1/account/deliveries/{id}/resendrecoloca na fila (202). O event_id não muda.

Limites, tarifas e IP account:manage

GET /v1/account/limits mostra os seus limites de leitura e escrita. GET /v1/account/pricing mostra as tarifas vigentes por operação e moeda (fixed + percent_ppm, com min_fee/max_fee quando há), o refund_fee_mode das devoluções parciais e a retenção (percent_ppm, days). GET /v1/account/charge-requirements diz se a cobrança precisa de payer (conforme o liquidante da conta). GET /v1/account/ip mostra o IP de origem que a API enxerga — a primeira coisa a olhar em qualquer 403.

Console interativo

Faça chamadas reais à API daqui, com a sua chave. A chave fica só na memória desta aba (nada é gravado, nada passa por servidor nosso além da própria API — o console fala com api.pagyou.com na mesma origem). Você vê status, tempo, cabeçalhos de cota e o corpo.

Changelog

  • payer na criação de cobrança (obrigatório quando o liquidante da conta exige — 422 payer_required) e GET /v1/account/charge-requirements para saber antes.
  • GET /v1/charges/{id}/refunds: as devoluções de uma cobrança, aplicadas e o pedido em voo. DELETE /v1/account/webhooks/{id}. GET /v1/account/pricing documentado.
  • Saque: a resposta segue o estado do saque. Erro na chamada ao liquidante com o saque ainda em voo agora responde sempre 202 com o id (antes podia vir 503 ou 500 com o saque vivo). Novo código de falha liquidity_unavailable (origem liquidator): recusa definitiva, com o valor de volta ao saldo; substitui insufficient_funds como failure_code de saque.
  • Publicação em api.pagyou.com. Rate limits por conta com cabeçalhos X-RateLimit-*; 503 liquidator_unavailable quando o liquidante recusa nossa credencial; self-service completo da conta (chaves, allowlist com trava anti-autobloqueio, endpoints de webhook com rotação de segredo, entregas e reenvio); GET /v1/account/limits.
  • Extrato (GET /v1/statement, JSON/CSV). Devolução iniciada pelo cliente (POST /v1/charges/{id}/refund). Webhooks de saída assinados com retentativas e fila morta. Endpoints de leitura e listagens paginadas. Balde med_held no saldo.
© 2026 Pagyou · pagyou.com Esta documentação é servida pelo mesmo binário que atende a API — está sempre em sincronia.