API v1 · cobranças Pix
Documentação da API
Com a API da ValidraPay, o seu sistema ou o seu bot gera cobranças Pix na Conta de pagamento do vendedor, mantida pela instituição de pagamento parceira, acompanha cada uma e recebe um aviso assinado quando ela muda de estado.
Como o dinheiro anda
O comprador paga ao próprio vendedor: o recebedor que aparece no aplicativo de pagamento é o titular da Conta. Nem a ValidraPay nem o integrador recebem, guardam ou repassam o valor, e nenhum campo da API troca ou esconde o recebedor.
O caminho de uma venda
POST /v1/chargesgera a cobrança e, em geral, já devolveactive, com o copia e cola e o QR na mesma resposta.- Se vier
processing,GET /v1/charges/{id}atéactive: aí vêm o copia e cola e o QR. - Você mostra o copia e cola e o QR ao comprador.
- Chega o evento
charge.paidno seu endereço de notificação. - Você consulta a cobrança, confere
paide o valor, e só então libera o produto.
Autenticação
Conceito
Toda chamada leva uma chave de acesso, que identifica uma única Conta: a de quem criou a chave. Não há login, sessão nem token temporário: a mesma chave vale até ser revogada. Ela é usada só pelo seu servidor, nunca por navegador, aplicativo de celular ou qualquer código que rode no aparelho do comprador.
Chave de acesso: Client ID e Client Secret
O titular cria a chave no portal, em API, Chaves de acesso, com o código do aplicativo autenticador, e escolhe os escopos (e, quando a conta usa lista de IPs, os IPs autorizados). Cada chave tem duas partes:
| Parte | Formato | Para que serve |
|---|---|---|
| Client ID | vpk_live_ + 26 caracteres (35 no total) | Parte pública. Aparece no portal e em GET /v1/me. É o usuário do Basic. |
| Client Secret | 52 caracteres de A-Z e 2-7 | Parte secreta. É a senha do Basic. |
| Chave inteira | Client ID + _ + Client Secret (88 caracteres) | O que vai no Bearer. |
Client ID vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X
Client Secret EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFIC
Chave inteira vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X_EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFICO Client Secret aparece uma única vez, logo depois da criação. Guarde-o num cofre de segredos ou numa variável de ambiente do servidor. Se perder, o segredo não pode ser mostrado de novo: o titular revoga a chave e cria outra. Nunca coloque a chave em código, repositório, log, planilha, print, e-mail ou mensagem.
Bearer ou Basic
Mande a chave em um cabeçalho Authorization, num destes dois formatos. Dois cabeçalhos, esquema desconhecido ou Basic malformado dão 401.
| Formato | Valor do cabeçalho |
|---|---|
| Bearer | Bearer <chave inteira> |
| Basic | Basic base64(<Client ID>:<Client Secret>), base64 padrão (RFC 7617) |
Authorization: Bearer vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X_EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFICComo calcular o Basic: separe a chave inteira no último _; o que vem antes é o Client ID e o que vem depois é o Client Secret. Junte os dois com : (dois-pontos, não sublinhado) e codifique o texto em base64.
Texto a codificar vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X:EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFIC
Cabeçalho Authorization: Basic dnBrX2xpdmVfMDFKQTdQOVE0TjZNOEswSDJGNEQ2QjhaMFg6RVhFTVBMT0ZJQ1RJQ0lPTkFPVVNFRVhFTVBMT0ZJQ1RJQ0lPTkFPVVNFRVhFTVBMT0ZJQw==# Bearer: a chave inteira
curl -s https://app.validrapay.com/v1/me \
-H "Authorization: Bearer ${VALIDRA_API_KEY}"
# Basic montado à mão
BASIC=$(printf '%s:%s' "${VALIDRA_CLIENT_ID}" "${VALIDRA_CLIENT_SECRET}" | base64 | tr -d '\n')
curl -s https://app.validrapay.com/v1/me \
-H "Authorization: Basic ${BASIC}"
# Basic montado pelo curl (usuário = Client ID, senha = Client Secret)
curl -s https://app.validrapay.com/v1/me \
-u "${VALIDRA_CLIENT_ID}:${VALIDRA_CLIENT_SECRET}"// Node.js 18 ou mais novo (fetch nativo). A chave vem de variável de ambiente do servidor.
const key = process.env.VALIDRA_API_KEY; // chave inteira (88 caracteres)
// Bearer
const bearer = { Authorization: `Bearer ${key}` };
// Basic: separe no ÚLTIMO "_" e junte com ":"
const cut = key.lastIndexOf("_");
const clientId = key.slice(0, cut); // vpk_live_<key_id>
const clientSecret = key.slice(cut + 1); // 52 caracteres
const basic = {
Authorization: "Basic " + Buffer.from(`${clientId}:${clientSecret}`, "ascii").toString("base64"),
};
const res = await fetch("https://app.validrapay.com/v1/me", { headers: bearer });
console.log(res.status, await res.json());import base64
import os
import requests
key = os.environ["VALIDRA_API_KEY"] # chave inteira (88 caracteres)
# Bearer
bearer = {"Authorization": f"Bearer {key}"}
# Basic: separe no ÚLTIMO "_" e junte com ":"
client_id, _, client_secret = key.rpartition("_")
token = base64.b64encode(f"{client_id}:{client_secret}".encode("ascii")).decode("ascii")
basic = {"Authorization": f"Basic {token}"}
res = requests.get("https://app.validrapay.com/v1/me", headers=bearer, timeout=15)
print(res.status_code, res.json())Escopos
A chave só alcança as rotas dos escopos que recebeu:
| Escopo | Nome no portal | Libera |
|---|---|---|
charges:write | Gerar cobrança | POST /v1/charges |
charges:read | Consultar cobranças | GET /v1/charges/{id} e GET /v1/charges/by-external-id/{external_id} |
webhooks:write | Gerenciar notificações (webhooks) | GET, PUT e DELETE /v1/webhook-endpoint e POST /v1/webhook-endpoint/test |
balance:read | Consultar saldo | GET /v1/balance |
withdrawals:write | Enviar Pix | POST /v1/payouts/preview e POST /v1/payouts (em verificação) |
withdrawals:read | Consultar Pix enviados | GET /v1/payouts e GET /v1/payouts/{id} (em verificação) |
transfers:write | Ainda não oferecido na tela de criação | POST /v1/transfers/preview e POST /v1/transfers (ainda não disponível) |
transfers:read | Ainda não oferecido na tela de criação | GET /v1/transfers e GET /v1/transfers/{id} (ainda não disponível) |
med:read | Ainda não oferecido na tela de criação | GET /v1/med/infractions e o detalhe (ainda não disponível) |
med:write | Ainda não oferecido na tela de criação | As rotas de evidência, relato e envio do MED (ainda não disponível) |
GET /v1/meaceita qualquer chave válida, sem escopo específico.- Chave sem o escopo da rota recebe 403
forbidden. - A lista de cobranças (
GET /v1/charges) e o cancelamento usam os escopos de cobrança:charges:readecharges:write. statement:readaparece na tela de criação, mas nenhuma rota da API o usa hoje.withdrawals:writeetransfers:writemovem dinheiro. Só são concedidos no portal, com o código do aplicativo autenticador; a chave passa a ter um segredo de assinatura e todoPOST,PUTeDELETEdela vai assinado (veja Assinatura do pedido). A exigência de lista de IPs depende da configuração da conta (veja IPs autorizados).- Chave criada por um representante autorizado da Conta só recebe os escopos que as permissões dadas pelo titular liberam, e perde o escopo na hora em que a permissão é retirada. Ela só enxerga as cobranças, os envios e as contestações ligados ao que o próprio representante criou.
URL base
https://app.validrapay.com/v1Em breve também https://api.validrapay.com/v1; quando estiver ativo, ele será anunciado aqui. Só HTTPS: sem TLS a chave passaria em claro.
- Corpo em JSON UTF-8, de no máximo 16 KB. Campo que a API não conhece, no pedido, é recusado (422); na resposta e nas notificações, ignore o que não conhecer.
- Datas em RFC 3339, UTC, com
Ze precisão de segundos:2026-10-22T14:35:00Z. Converta para o horário de Brasília só na hora de mostrar.
Cabeçalhos obrigatórios
| Cabeçalho | Quando | Regra |
|---|---|---|
Authorization | Toda chamada | Bearer ou Basic (veja Autenticação). |
Content-Type | Pedido com corpo | application/json; se vier charset, tem de ser utf-8. Outro tipo dá 415. |
Idempotency-Key | Todo POST | 8 a 128 caracteres A-Z a-z 0-9 _ -, guardada por 72 horas. Derive da operação (por exemplo chg-<external_id>) e repita a mesma ao tentar de novo. |
Cabeçalhos da resposta
| Cabeçalho | Significado |
|---|---|
X-Request-Id | Identificador da chamada, igual ao request_id dos erros. Registre-o e informe-o ao suporte. Se você mandar um X-Request-Id com 8 a 128 caracteres A-Z a-z 0-9 . _ -, a API usa o seu. |
RateLimit-Limit | Limite de chamadas da janela de 60 s. |
RateLimit-Remaining | Quantas ainda cabem na janela. |
RateLimit-Reset | Segundos até a janela recomeçar. |
Retry-After | Segundos de espera, em 429, 503 e request_in_progress. |
Location | Em 201 de POST /v1/charges: /v1/charges/chg_.... |
Idempotent-Replayed | true quando a resposta é a repetição de um pedido já feito com a mesma Idempotency-Key. |
Limites e ambiente
Limite de chamadas
- 120 chamadas por minuto por chave, em janela fixa de 60 s. O teste de notificação tem um limite próprio de 5 por minuto.
- Toda resposta a uma chave válida traz
RateLimit-Limit,RateLimit-RemainingeRateLimit-Reset(a 401 não traz). - Acima do limite: 429
rate_limited, comRetry-After. Espere e repita com a mesmaIdempotency-Key.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 17
Retry-After: 17
X-Request-Id: req_9c1e7a3b5d2f4e6a8b0c2d4e6f8a1b3cAmbiente
- Esta página descreve a produção: chave
vpk_live_,environment=productione dinheiro real, na Conta do vendedor. - Se a Conta ainda não puder gerar cobrança pela API,
POST /v1/chargesresponde 403account_not_enabled; confira emGET /v1/me, campocharges.enabled. - Existe um ambiente de testes (sandbox), sem dinheiro, com chave
vpk_test_. Ele não é aberto: é liberado a pedido, por contato@validrapay.com, com o endereço e as instruções. Chave de um ambiente dá 401 no outro.
Valores
- Dinheiro sempre em texto decimal, com ponto e duas casas:
"49.90","1000.00". Número JSON (49.9), vírgula ou centavos inteiros são recusados (422). - Valor mínimo por cobrança:
"0.51". Abaixo dele, 422 comerrors[].code=below_minimum. - A Conta tem limite de valor por cobrança. Acima dele, 422 com
errors[].code=above_limit. - A API não calcula nem desconta tarifas do
amount. As tarifas são as mostradas ao titular no portal.
IPs autorizados
A lista de IPs autorizados depende da configuração da conta. Confira em GET /v1/me: api_key.ip_allowlist vem null quando a lista não existe.
- Com lista: chamada de outro endereço recebe 403
ip_not_allowed, e o envio de Pix e a transferência exigem a lista preenchida. O IP conferido é o da conexão que chega à ValidraPay (o IP público de saída do seu servidor); umX-Forwarded-Formandado por você não muda nada. Se o IP do seu servidor mudar, peça ao titular que atualize a lista no portal antes. - Sem lista: nenhum IP é conferido, e o envio de Pix e a transferência do titular pela API ficam dentro dos limites da API, que
money_outde GET /v1/me devolve (por dia, por dia a destinos novos e por hora). Acima deles, 422daily_cap_exceededounew_destination_cap_exceededcomlimit_scope: "api", ou 429rate_limited; para valores maiores, use o portal. O titular recebe e-mail a cada envio e um aviso quando a chave é usada a partir de uma rede nova.
Segredos e prefixos
| Prefixo | O que é | Quem tem |
|---|---|---|
vpk_live_ | Chave de API de produção, no Authorization. | Toda chave. |
vpw_live_ | Segredo das notificações: confere a assinatura dos eventos que a ValidraPay envia (veja Verificar a assinatura). | Quem tem endereço de notificação cadastrado. |
vps_ | Segredo de assinatura de pedido: assina cada POST da chave, no cabeçalho Validra-Request-Signature, para provar que o pedido de envio de Pix saiu de você (veja Assinatura do pedido). | Só chave com escopo de envio (withdrawals:write). Bot só de cobrança não tem e não precisa. |
Erros
Formato
Todo erro vem com Content-Type: application/problem+json, no formato da RFC 7807, mais um code estável que é o que o seu código deve olhar.
{
"type": "urn:validra:error:validation_error",
"title": "Pedido inválido",
"status": 422,
"detail": "Um ou mais campos são inválidos.",
"code": "validation_error",
"request_id": "req_9c1e7a3b5d2f4e6a8b0c2d4e6f8a1b3c",
"errors": [
{
"field": "amount",
"code": "below_minimum",
"message": "O valor mínimo é 0.51."
}
]
}| Campo | Descrição |
|---|---|
type | urn:validra:error:<code>. |
title | Título curto, em português. |
status | Status HTTP, repetido no corpo. |
detail | Explicação em português, para registro e para o vendedor; nunca para o comprador. |
code | Código estável (tabela abaixo). |
request_id | O mesmo valor do cabeçalho X-Request-Id. |
errors | Só em validation_error: lista de { field, code, message }. Nunca repete o valor enviado. |
charge_id | Cobrança ligada ao erro, em external_id_already_used e charge_rejected. |
Códigos de campo em errors[].code: required, not_allowed, invalid_format, invalid_type, too_short, too_long, out_of_range, below_minimum e above_limit.
Como tratar
- Decida pelo
code. Se aparecer um que você não conhece, decida pelo status HTTP. - Pode repetir (com a mesma
Idempotency-Key, depois deRetry-After): erro de rede, 429rate_limited, 409request_in_progress, 500internal_errore 503temporarily_unavailable. A repetição nunca gera uma segunda cobrança. - Corrija antes: 400, 413, 415 e 422 indicam pedido errado; 401 e 403 indicam chave, escopo, IP ou Conta; repetir igual dá o mesmo erro.
- A API não emite 502 nem 504; se aparecerem, vieram de um intermediário: trate como 503.
- Registre o
X-Request-Id, nunca a chave nem dados do comprador.
async function call(path, init = {}) {
const res = await fetch(`${process.env.VALIDRA_BASE_URL}${path}`, init);
const body = await res.json();
if (res.ok) return body;
// Registre sempre o X-Request-Id (nunca a chave nem dados do comprador).
console.error(res.status, body.code, res.headers.get("x-request-id"));
switch (body.code) {
case "rate_limited":
case "request_in_progress":
case "temporarily_unavailable":
case "internal_error":
// Repita o MESMO pedido, com a MESMA Idempotency-Key, depois de Retry-After.
throw Object.assign(new Error(body.code), { retryAfter: res.headers.get("retry-after") });
default:
// 400, 401, 403, 404, 409, 413, 415, 422: corrija antes de tentar de novo.
throw Object.assign(new Error(body.code), { problem: body });
}
}Códigos
| HTTP | code | O que fazer |
|---|---|---|
| 400 | malformed_request | O corpo não é JSON válido em UTF-8. Corrija o pedido. |
| 400 | idempotency_key_required | Mande Idempotency-Key em todo POST. |
| 400 | idempotency_key_invalid | Use um só cabeçalho, com 8 a 128 caracteres A-Z, a-z, 0-9, _ ou -. |
| 401 | unauthorized | Chave ausente, errada, revogada ou de outro ambiente; ou dois cabeçalhos Authorization. |
| 403 | forbidden | A chave não tem o escopo da rota. |
| 403 | ip_not_allowed | Chamada de IP fora da lista da chave (só quando a conta usa lista). Peça ao titular para incluir o IP. |
| 403 | account_not_ready | A Conta ainda não pode cobrar: o titular precisa concluir o cadastro no portal. |
| 403 | account_unavailable | A Conta não pode gerar cobranças agora. Avise o titular; os canais de atendimento estão no portal, em Ajuda. |
| 403 | account_not_enabled | Esta Conta ainda não pode gerar cobrança pela API; confira charges.enabled em GET /v1/me. |
| 404 | not_found | Cobrança inexistente, de outra Conta ou com id fora do formato (sem diferença entre os casos), ou rota inexistente. |
| 409 | idempotency_key_reused | A mesma Idempotency-Key foi usada com outro corpo. |
| 409 | request_in_progress | Um pedido com a mesma chave ainda está em curso; repita depois de Retry-After. |
| 409 | external_id_already_used | Já existe cobrança com esse external_id (vem o charge_id). Consulte-a ou use outro valor. |
| 409 | webhook_endpoint_not_configured | Não há endereço de notificação cadastrado para a Conta. |
| 413 | payload_too_large | Corpo acima de 16 KB. |
| 415 | unsupported_media_type | Mande Content-Type: application/json (charset, se vier, utf-8). |
| 422 | validation_error | Campo inválido; veja errors[]. |
| 422 | charge_rejected | A cobrança não pôde ser registrada (vem o charge_id). Revise o valor ou gere outra tentativa, com novo external_id. |
| 429 | rate_limited | Limite de chamadas; espere Retry-After. |
| 500 | internal_error | Falha da ValidraPay. Repita com a mesma Idempotency-Key. |
| 503 | temporarily_unavailable | Nada foi criado. Repita com a mesma Idempotency-Key depois de Retry-After. |
Códigos das demais funções
Aparecem só nas rotas da parte Mais funções. O significado de cada um está na seção da rota.
| HTTP | code | Onde |
|---|---|---|
| 401 | signature_required | Todo POST de chave com segredo de assinatura. |
| 401 | signature_invalid | Idem. |
| 401 | signature_expired | Idem. |
| 403 | payouts_not_enabled | Pix enviado. |
| 403 | transfers_not_enabled | Transferência entre Contas. |
| 403 | transfer_not_allowed | Transferência entre Contas. |
| 403 | ip_allowlist_required | PUT e DELETE /v1/webhook-endpoint (só quando a conta usa lista de IPs). |
| 404 | preview_not_found | Pix enviado e transferência. |
| 404 | webhook_endpoint_not_configured | GET, DELETE /v1/webhook-endpoint (no teste, é 409). |
| 409 | charge_not_cancellable | Cancelar cobrança. |
| 409 | preview_expired | Pix enviado e transferência. |
| 409 | preview_consumed | Pix enviado e transferência. |
| 409 | preview_not_ready | Pix enviado e transferência. |
| 409 | med_not_open | MED. |
| 422 | destination_unavailable | Pix enviado e transferência. |
| 422 | amount_mismatch | Pix enviado e transferência. |
| 422 | cooldown_active | Pix enviado e transferência. |
| 422 | below_minimum | Pix enviado e transferência. |
| 422 | above_maximum | Pix enviado e transferência. |
| 422 | daily_cap_exceeded | Pix enviado e transferência. |
| 422 | new_destination_cap_exceeded | Pix enviado e transferência. |
| 422 | insufficient_balance_for_fee | Pix enviado e transferência. |
| 422 | representative_allowance_exceeded | Pix enviado e transferência, chave de representante. |
| 422 | paused | Pix enviado e transferência. |
| 422 | account_not_ready | Pix enviado e transferência (aqui com status 422, não 403). |
| 422 | cursor_invalid | Lista de cobranças e lista de contestações. |
| 422 | evidence_refused | MED. |
| 503 | balance_unavailable | Saldo. |
| 503 | preview_pending | Pix enviado e transferência. |
Integração passo a passo para bots
O caminho mais curto para um bot de WhatsApp ou Telegram que só cobra. Siga a ordem e evite as armadilhas abaixo, todas já vistas em integrações reais.
- Chave do bot. Peça ao titular uma chave só com
charges:writeecharges:read(maiswebhooks:write, se o bot for cadastrar o próprio endereço de notificação). Sem escopo de envio de Pix. - Gere a cobrança com POST /v1/charges,
amountde no mínimo"0.51"e umexternal_idseu. Guarde oid(chg_...) que a resposta devolve. - Espere o QR com GET /v1/charges/{id} só enquanto a cobrança estiver em
processing, uma consulta a cada 1 a 2 segundos. Emactive, mandepix_copy_pasteao comprador. - Confirme o pagamento pelo evento charge.paid, com a assinatura conferida, e consulte a cobrança uma vez para conferir
paide o valor antes de liberar o produto.
Armadilhas
| Armadilha | O que fazer |
|---|---|
Consultar a cobrança com o id do seu sistema (por exemplo cv_123) dá 404. | O id da cobrança é sempre o chg_... devolvido no POST. Se o bot só tem o próprio identificador, mande-o como external_id e consulte por GET /v1/charges/by-external-id/{external_id}. |
A lista (GET /v1/charges) traz qr_code_png_base64: null. | É de propósito: a lista nunca traz a imagem. O QR vem no GET individual, com a cobrança em active. |
| Consultar em loop, sem pausa, até o limite. | Consulte no máximo a cada 1 a 2 segundos e só enquanto processing. O limite é de 120 pedidos por minuto por chave; acima dele, 429 rate_limited com Retry-After. Acompanhe RateLimit-Remaining (veja Limites). |
| Descobrir o pagamento consultando a cobrança de tempos em tempos. | Use o evento charge.paid. A consulta serve para conferir o pagamento depois do evento e como rede de segurança se ele demorar. |
| Depender da imagem pronta para mostrar o QR. | Gere o PNG no próprio bot a partir de pix_copy_paste, com qualquer biblioteca de QR Code: é mais robusto e o comprador também pode usar o copia e cola. |
Cobrança abaixo de "0.51". | É recusada com 422, errors[].code = below_minimum. |
| Chave do bot com escopo de envio de Pix. | Bot que só cobra não precisa de withdrawals:write. Com esse escopo a chave passa a exigir a assinatura de todo POST, PUT e DELETE, inclusive gerar cobrança e trocar o endereço de notificação, com o segredo vps_... (veja Segredos e prefixos) e, quando a conta usa lista, a lista de IPs. |
Gerar cobrança
Gera uma cobrança Pix imediata na Conta do vendedor dono da chave. Cabeçalhos: Authorization, Content-Type: application/json e Idempotency-Key.
Corpo
| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
amount | texto | sim | Decimal com duas casas, mínimo "0.51". |
external_id | texto | sim | O seu identificador desta tentativa: 1 a 64 caracteres A-Z a-z 0-9 _ -, único na Conta para sempre. Nova tentativa do mesmo pedido usa outro valor (pedido-1042-2). |
expires_in | inteiro | não | Segundos para pagar, de 300 a 86400, contados do registro. Padrão: 3600. Para venda por bot, 1800 costuma bastar. |
description | texto | não | 1 a 140 caracteres, que o comprador vê no aplicativo de pagamento. Texto neutro, sem caracteres de controle. Para não mandar, omita o campo (null é recusado). |
payer | objeto | não | { "name": ..., "document": ... }, os dois juntos; CPF (11 dígitos) ou CNPJ (14), só números, com dígito verificador válido. Nunca volta em resposta, notificação ou registro. Mande só se o seu fluxo já tiver esses dados com base para isso; para não mandar, omita o campo. |
Nunca ponha dado pessoal do comprador (nome, telefone, CPF, e-mail) em external_id ou description.
curl -s https://app.validrapay.com/v1/charges \
-H "Authorization: Bearer ${VALIDRA_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: chg-pedido-1042-1" \
-d '{
"amount": "49.90",
"external_id": "pedido-1042-1",
"expires_in": 1800,
"description": "Pedido 1042"
}'const res = await fetch("https://app.validrapay.com/v1/charges", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VALIDRA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "chg-pedido-1042-1", // derive do seu pedido; repita a mesma em erro de rede
},
body: JSON.stringify({
amount: "49.90", // texto, nunca número
external_id: "pedido-1042-1",
expires_in: 1800,
description: "Pedido 1042",
}),
});
const charge = await res.json(); // 201: em geral "active", já com o QRimport os
import requests
res = requests.post(
"https://app.validrapay.com/v1/charges",
headers={
"Authorization": f"Bearer {os.environ['VALIDRA_API_KEY']}",
"Idempotency-Key": "chg-pedido-1042-1",
},
json={ # requests manda Content-Type: application/json
"amount": "49.90",
"external_id": "pedido-1042-1",
"expires_in": 1800,
"description": "Pedido 1042",
},
timeout=15,
)
charge = res.json() # 201: em geral "active", já com o QRResposta 201
O POST espera o registro da cobrança na instituição por até 8 segundos. Em geral ele conclui nesse tempo e a resposta já vem active, com pix_copy_paste e qr_code_png_base64: mande ao comprador sem outra consulta. A resposta traz sempre o id (chg_...) e o cabeçalho Location.
{
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"external_id": "pedido-1042-1",
"status": "active",
"amount": "49.90",
"description": "Pedido 1042",
"txid": "VPK3M9Q2X7A00000000000000731",
"pix_copy_paste": "00020101021226680014br.gov.bcb.pix2546pix.example.invalid/qr/v2/7d3c9a1e5b2f4c8e9a01520400005303986540549.905802BR5916VENDEDOR EXEMPLO6009SAO PAULO62070503***63042E7B",
"qr_code_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAbgAAAG4CAIAAAC4qGXkAAAO...(encurtado)",
"expires_at": "2026-10-22T15:00:01Z",
"created_at": "2026-10-22T14:30:00Z",
"updated_at": "2026-10-22T14:30:01Z",
"paid_at": null,
"paid_after_expiration": false,
"payment": null,
"amount_refunded": "0.00",
"cancelled_at": null,
"cancel_reason": null,
"cancellation": null,
"paid_after_cancellation": false
}Se o registro demorar mais, a resposta vem processing: a cobrança está sendo registrada e ainda não tem QR.
{
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"external_id": "pedido-1042-1",
"status": "processing",
"amount": "49.90",
"description": "Pedido 1042",
"txid": "VPK3M9Q2X7A00000000000000731",
"pix_copy_paste": null,
"qr_code_png_base64": null,
"expires_at": null,
"created_at": "2026-10-22T14:30:00Z",
"updated_at": "2026-10-22T14:30:00Z",
"paid_at": null,
"paid_after_expiration": false,
"payment": null,
"amount_refunded": "0.00",
"cancelled_at": null,
"cancel_reason": null,
"cancellation": null,
"paid_after_cancellation": false
}Nesse caso, consulte com GET /v1/charges/{id}, usando o id da resposta, a cada 1 a 3 segundos até active. Não crie outra cobrança para isso. Se não virar active em 60 segundos, abandone a tentativa e gere outra com novo external_id. Se a instituição recusar o registro durante a espera, a resposta traz o estado real: cancelled, com cancel_reason system, sem QR.
Repetir sem duplicar
- Mesma
Idempotency-Keycom o mesmo corpo: 201 com a mesma cobrança, no estado atual, eIdempotent-Replayed: true. - Mesma
Idempotency-Keycom outro corpo: 409idempotency_key_reused. - O
external_idfica gasto quando a resposta traz um id de cobrança: 201, 409external_id_already_usede 422charge_rejected. Nos outros erros ele continua livre.
Consultar cobrança
Devolve a cobrança no estado atual. Serve para esperar o QR, para confirmar o pagamento antes de liberar o produto e como rede de segurança quando a notificação demorar. Fora da espera do processing, consulte no máximo uma vez a cada 5 segundos por cobrança.
| Parâmetro | Onde | Formato |
|---|---|---|
charge_id | caminho | chg_ + 32 caracteres hexadecimais minúsculos, como devolvido na geração. Guarde-o como texto, sem interpretar. Outro formato dá 404. |
curl -s https://app.validrapay.com/v1/charges/chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607 \
-H "Authorization: Bearer ${VALIDRA_API_KEY}"async function getCharge(chargeId) {
const res = await fetch(`https://app.validrapay.com/v1/charges/${encodeURIComponent(chargeId)}`, {
headers: { Authorization: `Bearer ${process.env.VALIDRA_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).code);
return res.json();
}
// Espera o QR: consulte a cada 2 s, por até 60 s. Não repita o POST.
async function waitActive(chargeId) {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
const charge = await getCharge(chargeId);
if (charge.status === "active" && charge.pix_copy_paste) return charge;
if (charge.status !== "processing" && charge.status !== "active") return charge;
await new Promise((r) => setTimeout(r, 2000));
}
return null; // abandone e crie outra tentativa com novo external_id
}import os
import time
from urllib.parse import quote
import requests
AUTH = {"Authorization": f"Bearer {os.environ['VALIDRA_API_KEY']}"}
def get_charge(charge_id: str) -> dict:
res = requests.get(f"https://app.validrapay.com/v1/charges/{quote(charge_id, safe='')}", headers=AUTH, timeout=15)
res.raise_for_status()
return res.json()
def wait_active(charge_id: str) -> dict | None:
"""Consulta a cada 2 s, por até 60 s. Não repita o POST."""
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
charge = get_charge(charge_id)
if charge["status"] == "active" and charge["pix_copy_paste"]:
return charge
if charge["status"] not in ("processing", "active"):
return charge
time.sleep(2)
return None # abandone e crie outra tentativa com novo external_id{
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"external_id": "pedido-1042-1",
"status": "active",
"amount": "49.90",
"description": "Pedido 1042",
"txid": "VPK3M9Q2X7A00000000000000731",
"pix_copy_paste": "00020101021226680014br.gov.bcb.pix2546pix.example.invalid/qr/v2/7d3c9a1e5b2f4c8e9a01520400005303986540549.905802BR5916VENDEDOR EXEMPLO6009SAO PAULO62070503***63042E7B",
"qr_code_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAbgAAAG4CAIAAAC4qGXkAAAO...(encurtado)",
"expires_at": "2026-10-22T15:00:01Z",
"created_at": "2026-10-22T14:30:00Z",
"updated_at": "2026-10-22T14:30:01Z",
"paid_at": null,
"paid_after_expiration": false,
"payment": null,
"amount_refunded": "0.00",
"cancelled_at": null,
"cancel_reason": null,
"cancellation": null,
"paid_after_cancellation": false
}Em active vêm pix_copy_paste (o copia e cola; mande ao comprador numa mensagem só dele, sem outro texto) e qr_code_png_base64 (PNG do QR, RGB de 8 bits, 440 x 440 px, preto em fundo branco; para usar como imagem, prefixe data:image/png;base64,). Se vier active com pix_copy_paste nulo, o que é raro, consulte de novo a cada 2 segundos, até 3 vezes.
{
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"external_id": "pedido-1042-1",
"status": "paid",
"amount": "49.90",
"description": "Pedido 1042",
"txid": "VPK3M9Q2X7A00000000000000731",
"pix_copy_paste": "00020101021226680014br.gov.bcb.pix2546pix.example.invalid/qr/v2/7d3c9a1e5b2f4c8e9a01520400005303986540549.905802BR5916VENDEDOR EXEMPLO6009SAO PAULO62070503***63042E7B",
"qr_code_png_base64": null,
"expires_at": "2026-10-22T15:00:01Z",
"created_at": "2026-10-22T14:30:00Z",
"updated_at": "2026-10-22T14:42:40Z",
"paid_at": "2026-10-22T14:42:40Z",
"paid_after_expiration": false,
"payment": {
"end_to_end_id": "E00000000202610221442AB12CD34EF5",
"amount": "49.90",
"paid_at": "2026-10-22T14:42:38Z"
},
"amount_refunded": "0.00",
"cancelled_at": null,
"cancel_reason": null,
"cancellation": null,
"paid_after_cancellation": false
}Consultar pelo external_id
Devolve o mesmo objeto cobrança, achado pelo seu identificador. Útil depois de um 409 external_id_already_used ou quando o seu sistema perdeu o id. Inexistente ou de outra Conta: 404.
curl -s https://app.validrapay.com/v1/charges/by-external-id/pedido-1042-1 \
-H "Authorization: Bearer ${VALIDRA_API_KEY}"Dados da chave e da conta
Mostra a chave usada (só o Client ID, nunca o segredo), se a Conta pode gerar cobranças agora, a situação do endereço de notificação e quem usa a chave (principal). É o jeito mais simples de testar a conexão. Não traz nome nem documento do titular.
| Campo | Descrição |
|---|---|
environment | production em produção. |
api_key | prefix (o Client ID), name, scopes, integrator, integrator_observed (o integrador visto no User-Agent, ou null), ip_allowlist (vazia quando a chave não tem lista; null quando a conta não usa lista de IPs) e money_out (limites da API para chave de envio do titular sem lista: daily_cap, new_destination_daily_cap e per_hour; senão, null). |
charges.enabled | Se a Conta pode gerar cobranças agora. Quando false, reason traz account_not_ready, account_unavailable ou account_not_enabled (veja Erros). |
webhook_endpoint | configured, status (active ou suspended), host e secret_fingerprint: os 8 primeiros caracteres hexadecimais do SHA-256 do segredo vpw_live_... inteiro, para conferir com o que você guardou. Na troca do segredo, previous_secret_fingerprint e previous_secret_expires_at mostram o anterior. É o endereço do usuário que criou a chave (veja Endpoint de notificação). |
principal | Quem usa a chave. role = holder (titular da Conta, com permissions = null) ou representative (representante autorizado, com a lista das permissões ativas que o titular concedeu). |
// chave do titular
"principal": { "role": "holder", "permissions": null }
// chave de representante autorizado
"principal": { "role": "representative", "permissions": ["create_charges", "view_own_sales"] }curl -s https://app.validrapay.com/v1/me \
-H "Authorization: Bearer ${VALIDRA_API_KEY}"{
"environment": "production",
"api_key": {
"prefix": "vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X",
"name": "Bot de vendas",
"scopes": ["charges:read", "charges:write", "webhooks:write"],
"integrator": null,
"ip_allowlist": ["203.0.113.10"]
},
"charges": {
"enabled": true,
"reason": null
},
"webhook_endpoint": {
"configured": true,
"status": "active",
"host": "loja.example.com",
"secret_fingerprint": "3fa8c21d",
"previous_secret_fingerprint": null,
"previous_secret_expires_at": null
},
"principal": {
"role": "holder",
"permissions": null
}
}Testar notificação
Manda um evento ping, assinado como os demais, ao endereço cadastrado e devolve se ele respondeu 2xx. Uma tentativa só, sem reentrega, com resposta em até 15 segundos. Corpo vazio (ou {}). Use uma Idempotency-Key nova a cada teste: repetir a mesma devolve o resultado anterior, sem mandar outro ping. Limite: 5 por minuto por chave. Sem endereço cadastrado: 409 webhook_endpoint_not_configured.
curl -s -X POST https://app.validrapay.com/v1/webhook-endpoint/test \
-H "Authorization: Bearer ${VALIDRA_API_KEY}" \
-H "Idempotency-Key: teste-endpoint-$(date +%s)"{
"event_id": "evt_01JA7Q9X2M4K8R3T5V6W7Y8Z9A",
"delivered": true,
"status_code": 204,
"duration_ms": 182,
"error": null
}{
"event_id": "evt_01JA7QA3C5E7G9J1K3M5P7R9T1",
"delivered": false,
"status_code": 500,
"duration_ms": 95,
"error": "non_2xx"
}Valores de error: timeout, connection_failed, tls_error, non_2xx, redirect_not_followed, address_not_allowed, response_too_large ou null.
Objeto cobrança
O mesmo objeto volta na geração, nas consultas e dentro dos eventos, sempre com estes campos, nesta ordem.
| Campo | Tipo | Descrição |
|---|---|---|
id | texto | chg_ + 32 hexadecimais minúsculos. |
external_id | texto | null | O external_id mandado na geração. |
status | texto | Estado (veja Estados da cobrança). |
amount | texto | Valor, por exemplo "49.90". |
description | texto | null | O texto mandado, ou null. |
txid | texto | Identificador da cobrança no padrão do Pix (26 a 35 caracteres). |
pix_copy_paste | texto | null | O copia e cola; null enquanto processing. |
qr_code_png_base64 | texto | null | PNG do QR em base64. Só em active; nos eventos, sempre null. |
expires_at | data | null | Fim do prazo de pagamento; null enquanto processing. |
created_at | data | Geração. |
updated_at | data | Última mudança; use para ordenar eventos da mesma cobrança. |
paid_at | data | null | Quando o pagamento foi confirmado e a cobrança virou paid. |
paid_after_expiration | booleano | true quando a confirmação chegou com a cobrança já expired. |
payment | objeto | null | end_to_end_id, amount e paid_at (liquidação do Pix). Sem dados do pagador. |
amount_refunded | texto | Total devolvido ao pagador; "0.00" sem devolução. |
cancelled_at | data | null | Quando a cobrança foi cancelada. |
cancel_reason | texto | null | system quando o registro da cobrança não se concluiu; seller quando o vendedor cancelou (veja Cancelar cobrança). |
cancellation | objeto | null | Pedido de cancelamento aberto ou o último que falhou: { status: "requested" | "failed", requested_at, failure_code }. null sem pedido. |
paid_after_cancellation | booleano | true quando o Pix foi confirmado depois de o vendedor cancelar a cobrança. |
Estados da cobrança
processing → active → (cancelling →) paid | expired | cancelled
| Estado | Significado |
|---|---|
processing | Sendo registrada, ainda sem QR. Consulte até mudar. |
active | Aguardando pagamento; tem copia e cola e QR. |
paid | Paga e confirmada. |
expired | Venceu sem pagamento confirmado. Um Pix liquidado ainda pode chegar depois e levá-la a paid, com paid_after_expiration: true. |
cancelling | Pedido de cancelamento em andamento (veja Cancelar cobrança). A cobrança ainda pode ser paga até a remoção ser confirmada. |
cancelled | O registro não se concluiu (cancel_reason: system) ou o vendedor cancelou (seller). Se um Pix ainda chegar depois do cancelamento do vendedor, ela vai a paid com paid_after_cancellation: true. |
partially_refunded | Paga, com parte do valor devolvida ao pagador pelo vendedor, no portal. |
refunded | Paga e devolvida por inteiro ao pagador pelo vendedor, no portal. |
Trate qualquer status que não conheça como "não pago" e consulte de novo.
Webhooks
Conceito
Quando uma cobrança muda de estado, a ValidraPay faz um POST no endereço de notificação, com o evento em JSON e uma assinatura que prova a origem. Cada usuário da Conta tem no máximo um endereço: o do titular recebe os eventos de toda a Conta; o de um representante autorizado, só os dos objetos que ele mesmo criou.
Como cadastrar o endereço
Você mesmo cadastra, troca e remove o endereço pela API, com chave que tenha lista de IPs quando a conta usa lista (veja Endpoint de notificação), ou o titular o cadastra no portal. O segredo de assinatura (vpw_live_...) é entregue uma única vez; guarde-o no cofre, como a chave. Depois, confira o cadastro com GET /v1/me e com o teste de notificação.
Requisitos do endereço:
https://, na porta 443, com nome de host (IP é recusado) que aponte só para endereço público;- certificado válido de autoridade pública, TLS 1.2 ou superior;
- sem usuário e senha, sem
?e sem#, até 2048 caracteres; sem token nem dado pessoal no caminho; - redirecionamentos não são seguidos.
Confirme sempre antes de liberar o produto. O evento avisa; quem decide é a consulta. Ao receber charge.paid, chame GET /v1/charges/{id} e libere só se vierem status = paid, payment não nulo, payment.amount igual ao valor do pedido e o id de uma cobrança desse pedido. Nunca libere por comprovante enviado pelo comprador, por cabeçalho da notificação ou por corpo sem assinatura válida.
Evento de cobrança paga
charge.paid é mandado quando o Pix é confirmado por consulta à instituição. O corpo é um envelope com o retrato da cobrança no momento da mudança, que não muda nas reentregas.
| Campo | Descrição |
|---|---|
id | evt_ + 26 caracteres; único por evento. Use para deduplicar. |
type | Tipo do evento, por exemplo charge.paid. |
api_version | v1. |
environment | production; confira antes de usar. |
created_at | Quando o evento foi gerado. |
data.charge | O objeto cobrança, com qr_code_png_base64 sempre null. |
| Cabeçalho | Descrição |
|---|---|
Validra-Signature | t=<unix>,v1=<hex>. O único que vale para decidir (veja Verificar a assinatura). |
Validra-Event-Id | Informativo, não assinado; igual ao id do corpo. |
Validra-Event-Type | Informativo, não assinado; decida pelo type do corpo verificado. |
Validra-Delivery-Attempt | Informativo, não assinado; número da tentativa, de 1 a 8. |
POST /webhooks/validrapay HTTP/1.1
Host: loja.example.com
Content-Type: application/json
User-Agent: ValidraPay-Notificacoes/1
Validra-Signature: t=1792680161,v1=<64 hexadecimais>
Validra-Event-Id: evt_01JA7S4D2F6H8K0M2P4R6T8V0X
Validra-Event-Type: charge.paid
Validra-Delivery-Attempt: 1
{
"id": "evt_01JA7S4D2F6H8K0M2P4R6T8V0X",
"type": "charge.paid",
"api_version": "v1",
"environment": "production",
"created_at": "2026-10-22T14:42:41Z",
"data": {
"charge": {
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"external_id": "pedido-1042-1",
"status": "paid",
"amount": "49.90",
"description": "Pedido 1042",
"txid": "VPK3M9Q2X7A00000000000000731",
"pix_copy_paste": "00020101021226680014br.gov.bcb.pix2546pix.example.invalid/qr/v2/7d3c9a1e5b2f4c8e9a01520400005303986540549.905802BR5916VENDEDOR EXEMPLO6009SAO PAULO62070503***63042E7B",
"qr_code_png_base64": null,
"expires_at": "2026-10-22T15:00:01Z",
"created_at": "2026-10-22T14:30:00Z",
"updated_at": "2026-10-22T14:42:40Z",
"paid_at": "2026-10-22T14:42:40Z",
"paid_after_expiration": false,
"payment": {
"end_to_end_id": "E00000000202610221442AB12CD34EF5",
"amount": "49.90",
"paid_at": "2026-10-22T14:42:38Z"
},
"amount_refunded": "0.00",
"cancelled_at": null,
"cancel_reason": null,
"cancellation": null,
"paid_after_cancellation": false
}
}
}Pode chegar mais de uma vez e, raramente, depois de charge.expired (então paid_after_expiration = true).
Outros eventos
| type | Quando |
|---|---|
charge.paid | Pix confirmado (seção anterior). |
charge.expired | A cobrança venceu sem pagamento confirmado; mandado de 2 a 5 minutos depois de expires_at. |
charge.cancelled | O registro da cobrança não se concluiu (cancel_reason: system) ou o cancelamento pedido pelo vendedor foi confirmado (seller). |
charge.cancellation_failed | O pedido de cancelamento não foi confirmado; a cobrança continua active e pagável. Pode pedir de novo. |
ping | Só do teste de notificação, com data vazio. Não muda nada. |
payout.pending_approval, payout.settled, payout.rejected, payout.failed, payout.cancelled, payout.refunded_by_recipient | Pix enviado pela API (em verificação; veja Consultar Pix enviados). data.payout, sem dados do recebedor. |
transfer.pending_approval, transfer.settled, transfer.rejected, transfer.failed, transfer.cancelled | Transferência entre Contas pela API (ainda não disponível). data.transfer, sem dados do destino. |
{
"id": "evt_01JA7S5E3G7J9M1P3R5T7W9Y1A",
"type": "charge.expired",
"api_version": "v1",
"environment": "production",
"created_at": "2026-10-22T15:03:12Z",
"data": {
"charge": {
"id": "chg_7a2d4b6c8e0f1a3b5c7d9e1f2a4b6c8d",
"external_id": "pedido-1044-1",
"status": "expired",
"amount": "97.00",
"...": "demais campos do objeto cobrança"
}
}
}{
"id": "evt_01JA7S6F4H8K0N2Q4S6V8X0Z2B",
"type": "charge.cancelled",
"api_version": "v1",
"environment": "production",
"created_at": "2026-10-22T14:31:05Z",
"data": {
"charge": {
"id": "chg_3b5d7f9a1c2e4a6b8d0f2b4d6f8a0c1e",
"external_id": "pedido-1045-1",
"status": "cancelled",
"pix_copy_paste": null,
"cancelled_at": "2026-10-22T14:31:05Z",
"cancel_reason": "system",
"...": "demais campos do objeto cobrança"
}
}
}{
"id": "evt_01JA7Q9X2M4K8R3T5V6W7Y8Z9A",
"type": "ping",
"api_version": "v1",
"environment": "production",
"created_at": "2026-10-22T14:00:00Z",
"data": {}
}- Devolução ao pagador não gera evento: a mudança aparece na consulta, em
statuseamount_refunded. - Tipo de evento que você não conhece: responda 2xx e ignore.
Verificar a assinatura
O cabeçalho Validra-Signature traz o instante da assinatura (t, em segundos Unix) e uma ou mais assinaturas v1, calculadas com HMAC-SHA256 sobre o corpo exatamente como foi mandado.
mensagem = t + "." + corpo_bruto
assinatura = hex( HMAC-SHA256( chave = segredo vpw_live_... inteiro (UTF-8), mensagem ) )
válida = assinatura é igual a algum v1 do cabeçalho E |agora − t| ≤ 300 s- Leia o corpo bruto, byte a byte, antes de qualquer conversão de JSON.
- Separe
te osv1do cabeçalho. Confiratcom^[0-9]{1,12}$e cadav1com^[0-9a-f]{64}$. Ignore esquemas que não conhecer. - Recuse se
testiver a mais de 300 segundos do seu relógio. - Calcule o HMAC e compare em tempo constante. Aceite se qualquer
v1conferir: na troca do segredo vêm dois (o do segredo novo primeiro), por 24 horas. - Assinatura inválida: responda 401 e descarte o corpo.
const crypto = require("node:crypto");
const express = require("express");
const TOLERANCIA_SEGUNDOS = 300;
// corpoBruto: Buffer exatamente como chegou. segredo: o vpw_live_... inteiro.
function assinaturaValida(corpoBruto, cabecalho, segredo) {
let t = null;
const v1 = [];
for (const parte of String(cabecalho || "").split(",")) {
const i = parte.indexOf("=");
if (i < 0) continue;
const nome = parte.slice(0, i).trim();
const valor = parte.slice(i + 1).trim();
if (nome === "t") t = valor;
else if (nome === "v1") v1.push(valor); // na troca do segredo vêm dois
}
if (!t || !/^[0-9]{1,12}$/.test(t)) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCIA_SEGUNDOS) return false;
const esperado = crypto
.createHmac("sha256", Buffer.from(segredo, "utf8"))
.update(t + ".")
.update(corpoBruto)
.digest();
return v1.some((v) => /^[0-9a-f]{64}$/.test(v) && crypto.timingSafeEqual(esperado, Buffer.from(v, "hex")));
}
const app = express();
// Corpo bruto nesta rota (express.raw), nunca express.json(): a assinatura é sobre os bytes.
app.post("/webhooks/validrapay", express.raw({ type: "application/json" }), async (req, res) => {
if (!assinaturaValida(req.body, req.get("Validra-Signature"), process.env.VALIDRA_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const evento = JSON.parse(req.body.toString("utf8"));
await enfileirarUmaVez(evento); // chave única: evento.id
res.status(204).end(); // responda rápido; processe depois
});import hashlib
import hmac
import json
import os
import re
import time
from flask import Flask, request
TOLERANCIA_SEGUNDOS = 300
app = Flask(__name__)
def assinatura_valida(corpo_bruto: bytes, cabecalho: str, segredo: str) -> bool:
"""corpo_bruto: bytes exatamente como chegaram. segredo: o vpw_live_... inteiro."""
t = None
v1 = []
for parte in (cabecalho or "").split(","):
nome, _, valor = parte.strip().partition("=")
if nome == "t":
t = valor
elif nome == "v1":
v1.append(valor) # na troca do segredo vêm dois
if t is None or not re.fullmatch(r"[0-9]{1,12}", t):
return False
if abs(time.time() - int(t)) > TOLERANCIA_SEGUNDOS:
return False
esperado = hmac.new(segredo.encode("utf-8"), t.encode("ascii") + b"." + corpo_bruto, hashlib.sha256).hexdigest()
return any(
re.fullmatch(r"[0-9a-f]{64}", v) is not None and hmac.compare_digest(esperado, v)
for v in v1
)
@app.post("/webhooks/validrapay")
def webhook():
corpo = request.get_data() # corpo bruto, antes de qualquer parse
if not assinatura_valida(corpo, request.headers.get("Validra-Signature", ""), os.environ["VALIDRA_WEBHOOK_SECRET"]):
return "", 401
evento = json.loads(corpo)
enfileirar_uma_vez(evento) # chave única: evento["id"]
return "", 204Vetor de teste
Para o seu teste unitário, com o relógio fixado em t. O segredo é fictício e nunca é emitido pela ValidraPay. O corpo tem exatamente 142 bytes; com um espaço a mais, ou com o relógio 301 segundos depois de t, a verificação tem de falhar.
segredo vpw_test_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRST
t 1791986400
corpo {"id":"evt_01JA7Q9X2M4K8R3T5V6W7Y8Z9A","type":"ping","api_version":"v1","environment":"sandbox","created_at":"2026-10-14T14:00:00Z","data":{}}
v1 b092d0b88bb0bc7eb7a2cf92028417596eb9d7ac97a8ff4296de884a55bbb99fEntregas e retentativas
- Responda 2xx em até 10 segundos e processe depois, numa fila. Resposta lenta conta como falha.
- Sem 2xx (ou com tempo esgotado, falha de TLS ou de conexão), a ValidraPay tenta de novo, até 8 tentativas no total: a primeira na hora e as seguintes 30 s, 2 min, 10 min, 30 min, 2 h, 6 h e 24 h depois da anterior, com variação de até 10% (cerca de 33 horas no total).
- 410 encerra as tentativas daquele evento; três 410 seguidos suspendem o endereço (
webhook_endpoint.status=suspendedemGET /v1/me). - O mesmo evento pode chegar mais de uma vez e fora de ordem. Guarde o
idcom restrição de unicidade por no mínimo 33 horas, ignore repetidos e ordene eventos da mesma cobrança pordata.charge.updated_at. Libere o produto uma vez só. - Se a notificação demorar, consulte a cobrança: a consulta é a rede de segurança.
Estado em produção
O estado de cada função vale para todas as Contas ao mesmo tempo: não há liberação Conta a Conta nem pedido de ativação. Nada nesta parte muda o caminho do dinheiro: o que sai da Conta sai só por ordem do titular (ou de representante autorizado por ele), da própria Conta, mantida pela instituição de pagamento parceira.
| Função | Rotas | Estado |
|---|---|---|
| Gerar e consultar cobrança | POST /v1/charges, GET /v1/charges/{id} | Disponível |
| Cancelar cobrança | POST /v1/charges/{id}/cancel | Disponível |
| Listar cobranças | GET /v1/charges | Disponível |
| Saldo | GET /v1/balance | Disponível |
| Dados da chave e da conta | GET /v1/me | Disponível |
| Endpoint de notificação | GET, PUT e DELETE /v1/webhook-endpoint | Disponível (PUT e DELETE exigem chave com lista de IPs quando a conta usa lista) |
| Pix enviado | /v1/payouts* | Em verificação (espera de 24 horas em toda chave nova com escopo de envio; envio de representante espera aprovação) |
| Transferência entre Contas | /v1/transfers* | Ainda não disponível (403 transfers_not_enabled) |
| MED | /v1/med/infractions* | Ainda não disponível (404 not_found) |
| Devolução ao pagador | Sem rota | Ainda não disponível (o titular devolve pelo portal) |
Cancelar cobrança
Disponível em produção. Assíncrono: cancelling e depois cancelled, ou o evento charge.cancellation_failed.
Pede o cancelamento de uma cobrança active. Corpo vazio (ou {}), com Idempotency-Key. O cancelamento é assíncrono: a resposta vem na hora, com a cobrança em cancelling, e a ValidraPay pede a remoção da cobrança à instituição em seguida. Confirmada a remoção, ela vira cancelled (cancel_reason: seller) e sai o evento charge.cancelled. Cancelar não debita nada da Conta.
| Situação | Resposta |
|---|---|
active, sem pedido aberto | 202, cobrança em cancelling |
cancelling (pedido já aberto) | 202, a mesma cobrança, sem segundo pedido |
cancelled | 200, a cobrança |
processing, paid, partially_refunded, refunded, expired, ou pagamento em confirmação | 409 charge_not_cancellable, com charge_status |
| Cobrança de outra Conta, ou de outro usuário para a chave de representante | 404 not_found |
- O pagamento vale. Enquanto a remoção não for confirmada, a cobrança ainda pode ser paga: chega
charge.paide ela vai apaid. Se um Pix for confirmado depois docancelled, ela vai apaidcompaid_after_cancellation: true. Nos dois casos, entregue o produto ou devolva o valor ao pagador pelo portal. - Se a remoção não for confirmada depois das novas tentativas, a cobrança continua
active,cancellation.statusvirafailede saicharge.cancellation_failed. Você pode pedir de novo. - O cancelamento continua aceito mesmo quando a Conta não pode gerar cobranças agora.
- Repetir com a mesma
Idempotency-Keydevolve o estado atual, com o status HTTP da primeira resposta eIdempotent-Replayed: true.
curl -s -X POST https://app.validrapay.com/v1/charges/chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607/cancel \
-H "Authorization: Bearer ${VALIDRA_API_KEY}" \
-H "Idempotency-Key: cancel-pedido-1042-1"{
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"status": "cancelling",
"...": "demais campos do objeto cobrança",
"cancellation": {
"status": "requested",
"requested_at": "2026-10-22T14:33:00Z",
"failure_code": null
},
"paid_after_cancellation": false
}Listar cobranças
Disponível em produção. A lista nunca traz a imagem do QR; ela vem no GET individual.
Lista as cobranças da Conta, das mais recentes para as mais antigas. A chave de representante só vê as cobranças que o próprio representante criou.
| Parâmetro | Regra |
|---|---|
created_from | Início da janela, RFC 3339 com fuso (2026-10-01T00:00:00Z). Padrão: 30 dias antes de created_to. |
created_to | Fim da janela, no mesmo formato. Padrão: agora. A janela tem no máximo 31 dias, e created_from vem antes de created_to. |
status | Repetível (status=paid&status=refunded): processing, active, paid, expired, cancelled, partially_refunded, refunded. Outro valor dá 422. |
limit | De 1 a 100. Padrão: 50. |
cursor | O next_cursor da página anterior, devolvido sem mudança e com os mesmos filtros. Cursor alterado, de outra chave ou com outro filtro: 422 cursor_invalid; recomece sem cursor. |
Cada item é o objeto cobrança, com qr_code_png_base64 sempre null, mais created_by: para a chave do titular, { "role": "holder" | "representative", "client_id" } de quem gerou a cobrança; para a chave de representante, null. next_cursor vem null na última página.
curl -s -G https://app.validrapay.com/v1/charges \
-H "Authorization: Bearer ${VALIDRA_API_KEY}" \
--data-urlencode "created_from=2026-10-01T00:00:00Z" \
--data-urlencode "created_to=2026-10-22T00:00:00Z" \
--data-urlencode "status=paid" \
--data-urlencode "status=refunded" \
--data-urlencode "limit=50"{
"items": [
{
"id": "chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607",
"external_id": "pedido-1042-1",
"status": "paid",
"amount": "49.90",
"description": "Pedido 1042",
"txid": "VPK3M9Q2X7A00000000000000731",
"pix_copy_paste": "00020101021226680014br.gov.bcb.pix2546pix.example.invalid/qr/v2/7d3c9a1e5b2f4c8e9a01520400005303986540549.905802BR5916VENDEDOR EXEMPLO6009SAO PAULO62070503***63042E7B",
"qr_code_png_base64": null,
"expires_at": "2026-10-22T15:00:01Z",
"created_at": "2026-10-22T14:30:00Z",
"updated_at": "2026-10-22T14:42:40Z",
"paid_at": "2026-10-22T14:42:40Z",
"paid_after_expiration": false,
"payment": {
"end_to_end_id": "E00000000202610221442AB12CD34EF5",
"amount": "49.90",
"paid_at": "2026-10-22T14:42:38Z"
},
"amount_refunded": "0.00",
"cancelled_at": null,
"cancel_reason": null,
"cancellation": null,
"paid_after_cancellation": false,
"created_by": { "role": "holder", "client_id": "cli_3h5p2w6t" }
}
],
"next_cursor": "<texto opaco: devolva como recebeu>"
}Saldo
Disponível em produção.
A resposta depende de quem usa a chave; confira sempre o campo kind. Limite próprio: 12 consultas por minuto por chave.
Chave do titular: saldo da Conta
kind = account_balance: o saldo lido da instituição de pagamento parceira, que mantém a Conta, com a hora da leitura (read_at). A ValidraPay não calcula esse número. available e blocked vêm como a instituição informou. Enquanto a leitura está em andamento, a resposta é 202 { "status": "pending" } com Retry-After: consulte de novo depois desse tempo. Se a leitura falhar, 503 balance_unavailable.
{
"kind": "account_balance",
"status": "ready",
"available": "820.90",
"blocked": "0.00",
"read_at": "2026-10-22T14:30:05Z",
"source": "institution"
}HTTP/1.1 202 Accepted
Content-Type: application/json
Retry-After: 2
{ "status": "pending" }Chave de representante: quanto você ainda pode pedir de Pix
kind = representative_allowance. Não é saldo e não mostra o saldo da Conta: é quanto você ainda pode pedir de Pix, calculado a partir das suas vendas (sales_total) menos o que você já enviou (sent_total), desde since. Os campos available, blocked e read_at não existem nessa forma. Cada pedido de Pix seu ainda espera a aprovação de quem responde pela Conta.
{
"kind": "representative_allowance",
"available_to_send": "310.00",
"sales_total": "1250.00",
"sent_total": "940.00",
"since": "2026-10-01",
"computed_at": "2026-10-22T14:30:05Z"
}Pix enviado (saque): regras
Em verificação em produção. As rotas estão ligadas. Toda chave nova com escopo de envio espera 24 horas antes do primeiro envio (422 cooldown_active), e o envio de representante espera a aprovação do titular. Se a função for desligada, as rotas respondem 403 payouts_not_enabled, sem consultar a chave Pix.
O Pix enviado sai da Conta do próprio titular, por ordem dada com a chave de API dele, para uma chave Pix confirmada antes do envio. Funciona em duas etapas: a prévia consulta a chave Pix e mostra quem vai receber; o envio usa a prévia. O destino pode ser o próprio titular ou outra pessoa, se o envio a terceiros estiver ligado. Pagar a terceiro não é repasse: receber dinheiro em nome de outra pessoa e repassar, ou usar a Conta como conta de passagem, é proibido.
A chave de API
- Escopo
withdrawals:write(enviar) ewithdrawals:read(consultar). O escopo de envio só é concedido no portal, com o código do aplicativo autenticador, e, quando a conta usa lista de IPs, só para chave com lista preenchida (até 10 entradas; chamada de outro IP: 403ip_not_allowed). Sem lista, valem os limites da API (veja IPs autorizados). - Com o escopo, a chave recebe um segredo de assinatura
vps_...(56 caracteres), mostrado uma única vez. TodoPOST,PUTeDELETEdessa chave, inclusive gerar cobrança, vai assinado (veja Assinatura do pedido). - Espera de 24 horas na chave depois da criação com escopo de envio, da concessão do escopo, da inclusão de um IP (quando há lista) e da troca do segredo de assinatura. Também vale a espera de 24 horas da Conta depois de uma mudança de segurança. Na espera: 422
cooldown_active, comretry_after(segundos) euntil. - A chave precisa ter sido criada por um usuário ativo da Conta, com o aplicativo autenticador ativo; sem isso, 403
forbiddenou 422account_not_ready. - Revogar a chave no portal recusa na hora os envios dela que ainda não foram transmitidos.
Limites
| Limite | Valor | Acima dele |
|---|---|---|
| Por envio | De R$ 5,00 a R$ 1.000,00 | 422 below_minimum ou above_maximum |
| Por dia, por Conta | R$ 3.000,00, ou R$ 10.000,00 a partir de 2.000 vendas pagas e confirmadas | 422 daily_cap_exceeded, com renews_at (e, ao titular, daily_cap e daily_used) |
| Por dia, para destino que ainda não recebeu da Conta | R$ 1.000,00 somados | 422 new_destination_cap_exceeded |
| Envios por chave | 30 por hora | 429 rate_limited, com Retry-After |
| Prévias por Conta | 20 por hora, somadas portal e API | 429 rate_limited, com Retry-After |
- O limite do dia é um só por Conta: soma portal e API, todos os usuários da Conta e as transferências entre Contas.
- Os limites da instituição de pagamento parceira valem por cima; vale o menor. Nenhum limite é liberado manualmente.
Tarifa e saldo
- A tarifa do Pix enviado é a da tabela da sua Conta (
feena prévia e no envio; na regra geral, R$ 0,50). Ela é debitada pela instituição, além do valor enviado; a ValidraPay não retém, não reserva e não debita nada. - O envio só é transmitido se o saldo disponível, lido da instituição, cobrir o valor mais a tarifa. Senão, 422
insufficient_balance_for_fee, commax_sendable(o máximo que você pode enviar agora). Sem leitura de saldo disponível: 503temporarily_unavailable.
Representante autorizado
O pedido feito com chave de representante fica em pending_approval até ser aprovado, envio a envio, por quem responde pela Conta; aprovado, passa de novo por todas as conferências. Ele também respeita o limite próprio do representante (422 representative_allowance_exceeded, com max_amount e fee; veja Saldo).
Outras recusas
- Envios da Conta pausados: 422
paused. Os canais de atendimento estão no portal, em Ajuda. - Envio a terceiro ainda não ligado: 422
destination_unavailable,reason=THIRD_PARTY_NOT_ENABLED. - Antes de transmitir, a ValidraPay consulta a chave Pix de novo: se o recebedor mudou, o envio é recusado (
rejected,reason: destination_changed).
Assinatura do pedido
Chave com segredo de assinatura (vps_...) manda em todo POST, PUT e DELETE o cabeçalho Validra-Request-Signature, um HMAC-SHA256 do pedido. O GET não é assinado. Este segredo é diferente do segredo das notificações (vpw_...) e nunca vai em log, URL ou mensagem.
mensagem = t + "." + MÉTODO + "." + CAMINHO + "." + IDEMPOTENCY_KEY + "." + hex(SHA-256(corpo_bruto))
v1 = hex( HMAC-SHA256( chave = segredo vps_... inteiro (UTF-8), mensagem ) )
cabeçalho = Validra-Request-Signature: t=<unix>,v1=<v1>t: instante em segundos Unix.MÉTODOem maiúsculas.CAMINHO: o caminho e a query exatamente como vão na linha do pedido, sem host e sem normalizar (/v1/payouts/preview).IDEMPOTENCY_KEY: o valor do cabeçalhoIdempotency-Key. Corpo vazio: o SHA-256 de zero bytes. Hexadecimal sempre em minúsculas.PUTeDELETEnão usamIdempotency-Key: nelesIDEMPOTENCY_KEYé vazio, mesmo que você mande o cabeçalho (a mensagem fica com dois pontos seguidos entre o caminho e o hash do corpo). ODELETEvai sem corpo.- Assine os bytes que você vai mandar, não um JSON montado de novo depois.
| HTTP | code | Quando |
|---|---|---|
| 401 | signature_required | Cabeçalho ausente. |
| 401 | signature_invalid | Mais de um cabeçalho, t fora de ^[0-9]{1,12}$, nenhum ou mais de um v1, v1 fora de ^[0-9a-f]{64}$, ou HMAC que não confere. Esquemas desconhecidos (v2) são ignorados. |
| 401 | signature_expired | t a mais de 300 segundos do relógio do servidor. Acerte o relógio (NTP). |
A assinatura é conferida antes do registro de idempotência. Repetir o mesmo pedido (mesma Idempotency-Key, mesmo corpo) devolve o mesmo resultado, nunca um segundo envio; trocar a Idempotency-Key, o corpo ou o caminho invalida a assinatura. Na troca do segredo, feita no portal, o anterior vale por até 24 horas (salvo troca marcada como "comprometido", que o invalida na hora).
const crypto = require("node:crypto");
// segredo: o vps_... inteiro, mostrado uma vez na criação da chave (guarde no cofre).
function assinar(segredo, metodo, caminho, idempotencyKey, corpo) {
const t = Math.floor(Date.now() / 1000);
const hashCorpo = crypto.createHash("sha256").update(corpo).digest("hex");
const mensagem = [t, metodo.toUpperCase(), caminho, idempotencyKey, hashCorpo].join(".");
const v1 = crypto.createHmac("sha256", Buffer.from(segredo, "utf8")).update(mensagem, "utf8").digest("hex");
return `t=${t},v1=${v1}`;
}
const caminho = "/v1/payouts/preview";
const idem = "pvw-pedido-77-1"; // a mesma em toda repetição deste pedido
const corpo = JSON.stringify({ pix_key: "+5511999990000", amount: "10.00" }); // estes bytes vão no POST
const res = await fetch("https://app.validrapay.com" + caminho, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VALIDRA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idem,
"Validra-Request-Signature": assinar(process.env.VALIDRA_SIGNING_SECRET, "POST", caminho, idem, corpo),
},
body: corpo,
});import hashlib
import hmac
import json
import os
import time
import requests
def assinar(segredo: str, metodo: str, caminho: str, idempotency_key: str, corpo: bytes) -> str:
"""segredo: o vps_... inteiro. corpo: exatamente os bytes que vão no pedido."""
t = int(time.time())
mensagem = ".".join((str(t), metodo.upper(), caminho, idempotency_key, hashlib.sha256(corpo).hexdigest()))
v1 = hmac.new(segredo.encode("utf-8"), mensagem.encode("utf-8"), hashlib.sha256).hexdigest()
return f"t={t},v1={v1}"
caminho = "/v1/payouts/preview"
idem = "pvw-pedido-77-1"
corpo = json.dumps({"pix_key": "+5511999990000", "amount": "10.00"}, separators=(",", ":")).encode("utf-8")
res = requests.post(
"https://app.validrapay.com" + caminho,
headers={
"Authorization": f"Bearer {os.environ['VALIDRA_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": idem,
"Validra-Request-Signature": assinar(os.environ["VALIDRA_SIGNING_SECRET"], "POST", caminho, idem, corpo),
},
data=corpo, # nunca json=: os bytes enviados têm de ser os assinados
timeout=30,
)Vetor de teste
Gerado com o código da API. O segredo é fictício e nunca é emitido pela ValidraPay.
segredo vps_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRST
t 1792670400
método POST
caminho /v1/payouts/preview
Idempotency-Key idem-0001-vetor
corpo {"pix_key":"+5511999990000","amount":"10.00"}
SHA-256(corpo) 005fa76bb584dd1a7c38723abb9dedabde88001accb23e28188543707d9ef70f
cabeçalho t=1792670400,v1=c24a5ff9a8081456ce1e2d1f7fb2911e231bfba47ff4bef5b8326ba10231d4a3
Corpo vazio, com a query como enviada:
caminho /v1/payouts?x=%2F
corpo (vazio; SHA-256 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)
cabeçalho t=1792670400,v1=970389988e66a2e9d5056bdca1060f80137cb3cac7730dcc3729f210af9bba7fPrévia do Pix
Consulta a chave Pix e devolve o recebedor como o diretório do Pix informou, com o documento mascarado, a tarifa e se o envio cabe nos limites e no saldo. Cabeçalhos: Authorization, Content-Type: application/json, Idempotency-Key e Validra-Request-Signature. Mostre o recebedor a quem vai mandar o Pix antes do envio.
| Campo | Tipo | Regra |
|---|---|---|
pix_key | texto | A chave Pix do destino. Chave inválida: 422 validation_error. |
amount | texto | Valor com duas casas, maior que zero ("150.00"). |
{
"pix_key": "+5511999990000",
"amount": "150.00"
}{
"id": "pvw_5d1e3c7a9b2f4e6081a3c5e7f9b1d3a5",
"status": "ready",
"recipient": {
"name": "FORNECEDOR EXEMPLO LTDA",
"document_masked": "12.***.***/0001-**",
"institution_name": "INSTITUICAO EXEMPLO",
"ispb": "00000000"
},
"ownership": "third_party",
"new_destination": true,
"amount": "150.00",
"fee": "0.50",
"total": "150.50",
"checks": {
"within_limits": true,
"balance_covers_fee": true,
"max_sendable": "820.40"
},
"expires_at": "2026-10-22T14:45:00Z"
}| Campo | Descrição |
|---|---|
id | pvw_ + 32 hexadecimais. Use no envio. |
recipient | name, document_masked, institution_name e ispb do recebedor. Nunca o documento completo. |
ownership | own (o próprio titular) ou third_party. |
new_destination | true se o destino ainda não recebeu da Conta (conta no limite de destino novo). |
fee | Tarifa do envio; total = amount + fee. |
checks | within_limits, balance_covers_fee (null sem leitura de saldo, e sempre null para representante) e max_sendable. São só um aviso: a conferência que vale é a do envio. |
expires_at | A prévia vale por 10 minutos. |
- Chave Pix inexistente ou destino que não pode receber: 422
destination_unavailable, comreason:KEY_NOT_FOUND,DESTINATION_NOT_VERIFIED,THIRD_PARTY_NOT_ENABLED,DESTINATION_NOT_ALLOWED,LOOKUP_FAILEDouACCOUNT_UNAVAILABLE. Sem dado do recebedor. - Consulta ainda em andamento: 503
preview_pendingcomRetry-After; repita com a mesmaIdempotency-Keye o mesmo corpo. - Mesma
Idempotency-Keycom outra chave Pix ou outro valor: 409idempotency_key_reused.
Enviar o Pix
Dá a ordem do envio com a prévia. Cabeçalhos iguais aos da prévia, com Idempotency-Key nova (derive do seu pedido e repita a mesma em erro de rede).
| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
preview_id | texto | sim | O id da prévia, da mesma chave. |
amount | texto | sim | Igual ao da prévia; diferente dá 422 amount_mismatch. |
description | texto | não | Até 140 caracteres, texto neutro. |
{
"preview_id": "pvw_5d1e3c7a9b2f4e6081a3c5e7f9b1d3a5",
"amount": "150.00",
"description": "Pagamento fornecedor"
}Resposta 202 com o envio em processing (ou pending_approval, para representante) e o cabeçalho Location: /v1/payouts/pay_.... O resultado chega pela consulta e pelas notificações payout.*.
{
"id": "pay_8b0d2f4a6c8e0a2c4e6a8c0e2a4c6e8a",
"status": "processing",
"reason": null,
"amount": "150.00",
"fee": "0.50",
"recipient": {
"name": "FORNECEDOR EXEMPLO LTDA",
"document_masked": "12.***.***/0001-**",
"institution_name": "INSTITUICAO EXEMPLO"
},
"ownership": "third_party",
"new_destination": true,
"end_to_end_id": null,
"created_at": "2026-10-22T14:36:10Z",
"updated_at": "2026-10-22T14:36:10Z"
}| HTTP | code | Significado |
|---|---|---|
| 404 | preview_not_found | Prévia inexistente, de outra chave ou de outra Conta (sem diferença entre os casos). |
| 409 | preview_expired | A prévia venceu; faça outra. |
| 409 | preview_consumed | A prévia já foi usada num envio. |
| 409 | preview_not_ready | A consulta da prévia ainda não terminou. |
| 422 | amount_mismatch | Valor diferente do da prévia. |
As recusas de limite, saldo, espera, pausa e Conta estão em Regras; nesses casos nenhum envio é criado. O registro de idempotência guarda só a resposta de sucesso: depois de uma recusa, repetir o pedido executa as conferências de novo.
{
"type": "urn:validra:error:daily_cap_exceeded",
"title": "Limite do dia atingido",
"status": 422,
"detail": "O envio passa do limite de envios do dia desta conta.",
"code": "daily_cap_exceeded",
"request_id": "req_9c1e7a3b5d2f4e6a8b0c2d4e6f8a1b3c",
"renews_at": "2026-10-23T03:00:00Z",
"daily_cap": "3000.00",
"daily_used": "2950.00"
}Consultar Pix enviados
Um envio pelo id (pay_ + 32 hexadecimais), ou a lista da Conta, dos mais recentes para os mais antigos, com limit de 1 a 100 (padrão 20) e cursor = o next_cursor da página anterior. A resposta é { "items": [...], "next_cursor" }. Inexistente ou de outra Conta: 404. A chave de representante só vê os envios que ele pediu.
curl -s "https://app.validrapay.com/v1/payouts?limit=20" \
-H "Authorization: Bearer ${VALIDRA_API_KEY}"
# próxima página: o next_cursor da resposta anterior
curl -s "https://app.validrapay.com/v1/payouts?limit=20&cursor=pay_8b0d2f4a6c8e0a2c4e6a8c0e2a4c6e8a" \
-H "Authorization: Bearer ${VALIDRA_API_KEY}"| status | Significado |
|---|---|
processing | Recebido e em conferência ou em transmissão. |
pending_approval | Pedido de representante esperando aprovação. |
settled | Liquidado; end_to_end_id preenchido. |
rejected | Recusado antes da transmissão; veja reason. |
failed | Não concluído pela instituição; veja reason. |
cancelled | Não transmitido; veja reason. |
refunded_by_recipient | O recebedor devolveu o valor inteiro. |
partially_refunded_by_recipient | O recebedor devolveu parte do valor. |
Valores de reason (só em rejected, failed e cancelled): amount_below_minimum, amount_above_maximum, withdrawal_daily_cap_exceeded, new_destination_cap_exceeded, destination_changed, destination_not_allowed, third_party_not_enabled, destination_not_verified, destination_unavailable, account_unavailable, account_not_ready, payouts_paused, payout_not_sent, payout_not_completed, payout_not_approved, insufficient_balance_for_fee e api_key_revoked. Trate valor desconhecido como payout_not_completed.
Notificações
Envio feito pela API gera payout.pending_approval, payout.settled, payout.rejected, payout.failed, payout.cancelled e payout.refunded_by_recipient, assinados como os eventos de cobrança. O evento não traz o recebedor: consulte o envio antes de agir.
{
"id": "evt_01JA7T2B4D6F8H0K2M4P6R8T0V",
"type": "payout.settled",
"api_version": "v1",
"environment": "production",
"created_at": "2026-10-22T14:36:14Z",
"data": {
"payout": {
"id": "pay_8b0d2f4a6c8e0a2c4e6a8c0e2a4c6e8a",
"status": "settled",
"reason": null,
"amount": "150.00",
"end_to_end_id": "E00000000202610221436AB12CD34EF5",
"created_at": "2026-10-22T14:36:10Z",
"updated_at": "2026-10-22T14:36:13Z"
}
}
}Transferência entre Contas
Ainda não disponível em produção. Todas as rotas /v1/transfers* respondem 403 transfers_not_enabled. O texto abaixo descreve como a função vai funcionar.
Envia dinheiro da sua Conta para a Conta de outro titular mantida na mesma instituição e operada pela ValidraPay, ativa e aprovada. Nunca para a conta da própria ValidraPay. Segue as regras do Pix enviado: chave lista de IPs conforme a configuração da conta, pedido assinado, espera de 24 horas, mesmos limites (somados ao Pix enviado), mesma tarifa, saldo para o valor e a tarifa e aprovação por envio para representante. Conta de origem que não pode transferir: 403 transfer_not_allowed.
Prévia
Informe o destino pelo ID de cliente do titular (cli_ + 8 caracteres), pelo CPF/CNPJ dele (só dígitos) ou pelo e-mail cadastrado dele, com exatamente uma das três chaves. A resposta mostra só o nome e o documento mascarados, nunca o e-mail (tpv_..., válida por 10 minutos). Destino inexistente, que não pode receber ou recusado dá sempre a mesma resposta, 422 destination_unavailable, sem motivo. Limite: 10 prévias por hora por Conta, somadas portal e API.
{
"destination": { "client_id": "cli_7k2m6q4x" },
"amount": "80.00"
}{
"destination": { "document": "<CPF ou CNPJ do titular, só dígitos>" },
"amount": "80.00"
}{
"destination": { "email": "<e-mail cadastrado do titular>" },
"amount": "80.00"
}{
"id": "tpv_2c4e6a8c0e2a4c6e8a0c2e4a6c8e0a2c",
"status": "ready",
"recipient": {
"name_masked": "Maria S. O.",
"document_masked": "***.456.789-**"
},
"new_destination": false,
"amount": "80.00",
"fee": "0.50",
"total": "80.50",
"checks": {
"within_limits": true,
"balance_covers_fee": true,
"max_sendable": "820.40"
},
"expires_at": "2026-10-22T14:50:00Z"
}Envio e consulta
POST /v1/transfers com { "preview_id", "amount", "description"? }, como no Pix enviado: 202 com trf_... em processing ou pending_approval e Location. A consulta e a lista seguem a forma do Pix enviado (sem os estados de devolução pelo recebedor). Notificações: transfer.pending_approval, transfer.settled, transfer.rejected, transfer.failed e transfer.cancelled.
{
"id": "trf_9a1c3e5b7d9f1b3d5f7a9c1e3b5d7f9a",
"status": "processing",
"reason": null,
"amount": "80.00",
"fee": "0.50",
"recipient": {
"name_masked": "Maria S. O.",
"document_masked": "***.456.789-**"
},
"new_destination": false,
"end_to_end_id": null,
"created_at": "2026-10-22T14:41:02Z",
"updated_at": "2026-10-22T14:41:02Z"
}MED: contestações de Pix recebido
Ainda não disponível em produção. As rotas não existem em produção (404 not_found); o titular trata a contestação no portal. O texto abaixo descreve como a função vai funcionar.
Quando um Pix recebido na Conta é contestado pelo Mecanismo Especial de Devolução, a contestação aparece aqui depois de liberada para a Conta. Quem analisa e decide é a instituição de pagamento parceira; pela API você consulta e manda as evidências dentro do prazo. Nada aqui movimenta a Conta.
- Lista e detalhe. A lista aceita
limit(1 a 100, padrão 20) ecursor(onext_cursoranterior; inválido dá 422cursor_invalid). O detalhe do titular traz o prazo, as evidências já enviadas, o que ainda pode ser feito e os limites (limits). A chave de representante vê só as contestações das cobranças que ele criou, com o mínimo necessário para mandar evidências. - Evidência.
POST .../evidencecomkind(DELIVERY_PROOF,CONVERSATION,INVOICEouOTHER),content_type(application/pdf,image/pngouimage/jpeg, conferido pelo conteúdo),content_base64efilename(opcional). Por padrão, até 5 arquivos de até 2 MiB cada (os limites vigentes vêm emlimits, no detalhe); o corpo desta rota aceita até 3 MiB. Resposta 201 com a evidência, sem o conteúdo. - Relato.
POST .../statementcomtext(por padrão, até 4.000 caracteres; o corpo segue o limite geral de 16 KB). Um novo relato troca o seu relato anterior ainda não enviado. - Retirar.
DELETE .../evidence/{evidence_id}antes do envio: 204. - Enviar.
POST .../submit, corpo vazio: sela as evidências e avisa a equipe da ValidraPay, que as encaminha à instituição. As evidências enviadas por representante são revisadas por quem responde pela Conta antes de seguir para a instituição. POSTpedeIdempotency-Keye, se a chave tiver segredo de assinatura,Validra-Request-Signature; oDELETEda evidência também vai assinado, comIDEMPOTENCY_KEYvazio. Não há recusa da contestação pela API.
| HTTP | code | Quando |
|---|---|---|
| 404 | not_found | Contestação inexistente, de outra Conta, de outra cobrança ou ainda não liberada (sempre a mesma resposta). |
| 409 | med_not_open | Prazo vencido ou envio já concluído. |
| 422 | evidence_refused | Arquivo ou texto fora dos limites, com reason: kind, content_type, too_large, too_many, empty, invalid_base64, integrity ou statement_too_long. |
{
"kind": "DELIVERY_PROOF",
"filename": "comprovante-entrega.pdf",
"content_type": "application/pdf",
"content_base64": "JVBERi0xLjQK...(encurtado)"
}{
"evidence": {
"id": "5b7d9f1a-3c5e-4a7b-9d1f-3a5c7e9b1d3f",
"kind": "DELIVERY_PROOF",
"filename": "comprovante-entrega.pdf",
"content_type": "application/pdf",
"size_bytes": 48213,
"sealed": false,
"created_at": "2026-10-22T15:02:11.482190+00:00",
"submitted_by": "seller"
}
}{
"text": "O produto foi entregue em 20/10 pelo bot; a conversa vai anexada."
}Endpoint de notificação pela API
Disponível em produção. Quando a conta usa lista de IPs, PUT e DELETE exigem chave com lista (403 ip_allowlist_required).
O endereço é do usuário que criou a chave: o do titular (scope: account) recebe os eventos de toda a Conta; o de um representante (scope: own) recebe só os eventos das cobranças, envios e transferências que ele mesmo criou. O GET mostra host, impressões e estado, nunca a URL inteira nem o segredo; sem endereço, 404 webhook_endpoint_not_configured.
PUT: cadastrar, trocar, rotacionar ou reativar
Corpo { "url": "https://...", "rotate_secret": false }, com os requisitos de endereço de Webhooks. Não usa Idempotency-Key. O secret (vpw_live_...) só vem quando é novo:
- Sem endereço: cria; 201 com
secret. - URL diferente: troca; segredo novo, válido na hora; 200 com
secret. - Mesma URL com
rotate_secret: true: segredo novo, e os dois assinam por 24 horas; 200 comsecret. - Mesma URL, sem rotação: 200 sem
secret(perdeu o segredo: rotacione). - Endereço suspenso: o
PUTcom a mesma URL o reativa. - URL fora dos requisitos: 422
validation_error(campourl).
DELETE
Remove o endereço do usuário da chave e descarta as entregas pendentes dele: 204.
Proteções em produção
- Quando a conta usa lista de IPs,
PUTeDELETEexigem chave com lista preenchida; sem ela, 403ip_allowlist_required. - No máximo 10
PUT/DELETEpor hora por chave e 3 trocas de URL por dia por usuário; acima, 429rate_limitedcomRetry-After. - Chave com segredo de assinatura (
vps_...):PUTeDELETEvão assinados (veja Assinatura do pedido), comIDEMPOTENCY_KEYvazio; sem a assinatura, 401signature_required, e o pedido não conta no limite. - Cada mudança gera um e-mail de segurança ao dono do endereço (e ao titular, quando o dono é representante), com o host, a ação, o Client ID da chave, o IP mascarado e o link "não fui eu", que remove o endereço e revoga as chaves.
curl -s -X PUT https://app.validrapay.com/v1/webhook-endpoint \
-H "Authorization: Bearer ${VALIDRA_API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "url": "https://loja.example.com/webhooks/validrapay", "rotate_secret": false }'{
"scope": "account",
"host": "loja.example.com",
"url_fingerprint": "7c1e9a3b",
"status": "active",
"secret_fingerprint": "3fa8c21d",
"previous_secret_fingerprint": null,
"previous_secret_expires_at": null,
"created_at": "2026-10-22T14:00:00Z",
"updated_at": "2026-10-22T14:00:00Z",
"secret": "vpw_live_<mostrado só nesta resposta>"
}O que ainda não existe
Estas funções não estão disponíveis na API em produção:
| Função | Situação |
|---|---|
| Extrato | Não há rota de extrato. O titular consulta o extrato no portal. |
| Devolução ao pagador | Não há devolução pela API. A devolução é feita pelo titular, no portal. |
| Pix por QR Code, por dados bancários, TED e pagamento de boletos | Não estão disponíveis. A única saída de Pix pela API é o Pix por chave. |
| Divisão de pagamento (split) | Não está disponível. |
| Recusar a contestação de MED pela API | Não há. Pela API só se consulta e se mandam evidências (veja MED). |
| Criptoativos | A API não converte nem envia valores para criptoativos. |
| Transferência entre Contas e MED pela API | As rotas existem, mas estão desligadas em produção (veja Estado em produção). O titular trata a contestação de MED no portal. |
Rotas que não estão nesta página não existem em produção.