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.
POST devolve o Pix copia-e-cola. Idempotente por external_ref.
02Receba o webhookAssinado com HMAC e timestamp. Mesmo payload do GET.
03Envie um saqueDébito atômico do saldo disponível. Nunca paga duas vezes.
- 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.
| Escopo | Permite |
|---|---|
charges:write | criar cobranças e pedir devolução |
charges:read | consultar e listar cobranças |
payouts:write | criar saques |
payouts:read | consultar e listar saques |
balance:read | saldo |
statement:read | extrato |
account:manage | administrar 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.
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ãoBRL). - 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.descriptionvai até 200 (cobrança) ou 140 (saque, o que cabe no que o recebedor vê), epix_keyaté 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
paidnunca volta apending. Eventos podem chegar fora de ordem ou repetidos: trate pelo estado, não pela ordem. - Toda resposta traz
request_idnos erros (e no cabeçalhoX-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.
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.
| HTTP | code | Quando |
|---|---|---|
| 400 | invalid_json | corpo malformado ou campo desconhecido |
| 401 | unauthorized | chave ausente, inválida ou revogada |
| 403 | ip_not_allowed · scope_denied | IP fora da allowlist (traz o IP visto) · chave sem o escopo |
| 404 | not_found | recurso inexistente ou de outra conta |
| 409 | idempotency_conflict · already_exists · last_manage_key · would_lock_out · refund_already_requested · charge_disputed | conflitos de estado |
| 422 | validation_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 liquidante | pedido válido em forma, recusado em conteúdo |
| 429 | rate_limited | cota excedida — veja Retry-After |
| 503 | liquidator_unavailable · no_liquidator | indisponibilidade 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:
| origin | Significa | O que fazer |
|---|---|---|
client | o pedido precisa mudar — validação, saldo, chave Pix, idempotência, conta do recebedor | corrija e reenvie; reenviar igual falha de novo |
liquidator | o parceiro bancário caiu, recusou, demorou ou segurou para análise — não é problema do seu código | numa 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á alerta | abra 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ório | string — sua referência única; chave de idempotência. |
amountobrigatório | inteiro > 0 na unidade menor. |
currency | BRL (padrão). |
description | texto exibido ao pagador quando o liquidante suporta. |
expires_in_seconds | padrão 3600. O liquidante pode limitar (o sandbox limita a 600 s) — vale o expires_at devolvido. |
payer | opcional: {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.
status | pending paid expired cancelled failed refunded |
fee | tarifa descontada do valor bruto ao pagar. Líquido = amount − fee. |
e2e_id | EndToEndId do Pix recebido — o identificador que o pagador vê no extrato dele. |
paid_at · refunded_at · refund_requested_at | marcos temporais; refund_requested_at aparece quando você pediu a devolução e a confirmação ainda não chegou (o valor já está reservado). |
retention | quando há retenção de risco configurada: amount retido, release_at previsto, released_at quando liberou. O saldo retido aparece no balde held. |
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ório | sua referência única. |
amountobrigatório | inteiro > 0 na unidade menor. |
pix_keyobrigatório | CPF, CNPJ, e-mail, telefone (+55…) ou chave aleatória (EVP). |
pix_key_type | CPF · 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. |
description | até 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
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.
available | disponível para saque e devolução. |
blocked | reservado por saques em andamento e devoluções pedidas — volta a available se falharem. |
held | retenção de risco (prazo configurado); libera para available automaticamente. |
med_held | bloqueado por um MED (Mecanismo Especial de Devolução) em análise. |
owed | recebí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-Id | UUID estável entre retentativas — deduplique por ele. |
Pagyou-Event-Type | o tipo (charge.paid, payout.completed…), também em type no corpo. |
Pagyou-Signature | t=<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
| Tipo | Quando | data |
|---|---|---|
charge.paid | o Pix entrou; saldo creditado (líquido) | cobrança |
charge.expired · charge.cancelled · charge.failed | a 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.paid | cobrança |
charge.partially_refunded | devolução parcial aplicada — o campo refunded traz o acumulado | cobrança |
charge.refunded | devolução concluída (sua ou do liquidante) | cobrança |
payout.processing | o liquidante aceitou o saque | saque |
payout.completed | o recebedor recebeu | saque |
payout.failed | falhou; valor e tarifa voltaram ao disponível | saque |
payout.returned | o recebedor devolveu; valor voltou ao disponível | saque |
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/keys | lista (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/allowlist | estado (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/ip | o teste: o IP que vemos e se passaria. Rode antes de ligar e ao migrar de infra. |
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/webhooks | lista (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/deliveries | filtros status (pending · delivered · exhausted), endpoint_id, before_id, limit. Traz tentativas, último HTTP status e erro. |
POST /v1/account/deliveries/{id}/resend | recoloca 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
payerna criação de cobrança (obrigatório quando o liquidante da conta exige —422 payer_required) eGET /v1/account/charge-requirementspara 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/pricingdocumentado.- Saque: a resposta segue o estado do saque. Erro na chamada ao liquidante com o saque ainda em voo agora responde sempre
202com oid(antes podia vir503ou500com o saque vivo). Novo código de falhaliquidity_unavailable(origemliquidator): recusa definitiva, com o valor de volta ao saldo; substituiinsufficient_fundscomofailure_codede saque. - Publicação em
api.pagyou.com. Rate limits por conta com cabeçalhosX-RateLimit-*;503 liquidator_unavailablequando 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. Baldemed_heldno saldo.