ValidraPay
Seções da documentação

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

  1. POST /v1/charges gera a cobrança e, em geral, já devolve active, com o copia e cola e o QR na mesma resposta.
  2. Se vier processing, GET /v1/charges/{id} até active: aí vêm o copia e cola e o QR.
  3. Você mostra o copia e cola e o QR ao comprador.
  4. Chega o evento charge.paid no seu endereço de notificação.
  5. Você consulta a cobrança, confere paid e 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:

Partes da chave de acesso
ParteFormatoPara que serve
Client IDvpk_live_ + 26 caracteres (35 no total)Parte pública. Aparece no portal e em GET /v1/me. É o usuário do Basic.
Client Secret52 caracteres de A-Z e 2-7Parte secreta. É a senha do Basic.
Chave inteiraClient ID + _ + Client Secret (88 caracteres)O que vai no Bearer.
Exemplo fictício (não funciona em lugar nenhum)
Client ID      vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X
Client Secret  EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFIC
Chave inteira  vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X_EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFIC

O 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.

Formatos do cabeçalho Authorization
FormatoValor do cabeçalho
BearerBearer <chave inteira>
BasicBasic base64(<Client ID>:<Client Secret>), base64 padrão (RFC 7617)
Bearer · exemplo fictício
Authorization: Bearer vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X_EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFIC

Como 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.

Basic · exemplo fictício
Texto a codificar  vpk_live_01JA7P9Q4N6M8K0H2F4D6B8Z0X:EXEMPLOFICTICIONAOUSEEXEMPLOFICTICIONAOUSEEXEMPLOFIC
Cabeçalho          Authorization: Basic dnBrX2xpdmVfMDFKQTdQOVE0TjZNOEswSDJGNEQ2QjhaMFg6RVhFTVBMT0ZJQ1RJQ0lPTkFPVVNFRVhFTVBMT0ZJQ1RJQ0lPTkFPVVNFRVhFTVBMT0ZJQw==
curl
# 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
// 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());
Python
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:

Escopos usados pela API
EscopoNome no portalLibera
charges:writeGerar cobrançaPOST /v1/charges
charges:readConsultar cobrançasGET /v1/charges/{id} e GET /v1/charges/by-external-id/{external_id}
webhooks:writeGerenciar notificações (webhooks)GET, PUT e DELETE /v1/webhook-endpoint e POST /v1/webhook-endpoint/test
balance:readConsultar saldoGET /v1/balance
withdrawals:writeEnviar PixPOST /v1/payouts/preview e POST /v1/payouts (em verificação)
withdrawals:readConsultar Pix enviadosGET /v1/payouts e GET /v1/payouts/{id} (em verificação)
transfers:writeAinda não oferecido na tela de criaçãoPOST /v1/transfers/preview e POST /v1/transfers (ainda não disponível)
transfers:readAinda não oferecido na tela de criaçãoGET /v1/transfers e GET /v1/transfers/{id} (ainda não disponível)
med:readAinda não oferecido na tela de criaçãoGET /v1/med/infractions e o detalhe (ainda não disponível)
med:writeAinda não oferecido na tela de criaçãoAs rotas de evidência, relato e envio do MED (ainda não disponível)
  • GET /v1/me aceita 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:read e charges:write.
  • statement:read aparece na tela de criação, mas nenhuma rota da API o usa hoje.
  • withdrawals:write e transfers:write movem dinheiro. Só são concedidos no portal, com o código do aplicativo autenticador; a chave passa a ter um segredo de assinatura e todo POST, PUT e DELETE dela 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

Produção
https://app.validrapay.com/v1

Em 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 Z e 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çalhos do pedido
CabeçalhoQuandoRegra
AuthorizationToda chamadaBearer ou Basic (veja Autenticação).
Content-TypePedido com corpoapplication/json; se vier charset, tem de ser utf-8. Outro tipo dá 415.
Idempotency-KeyTodo POST8 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çalhos da resposta
CabeçalhoSignificado
X-Request-IdIdentificador 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-LimitLimite de chamadas da janela de 60 s.
RateLimit-RemainingQuantas ainda cabem na janela.
RateLimit-ResetSegundos até a janela recomeçar.
Retry-AfterSegundos de espera, em 429, 503 e request_in_progress.
LocationEm 201 de POST /v1/charges: /v1/charges/chg_....
Idempotent-Replayedtrue 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-Remaining e RateLimit-Reset (a 401 não traz).
  • Acima do limite: 429 rate_limited, com Retry-After. Espere e repita com a mesma Idempotency-Key.
429 · cabeçalhos
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_9c1e7a3b5d2f4e6a8b0c2d4e6f8a1b3c

Ambiente

  • Esta página descreve a produção: chave vpk_live_, environment = production e dinheiro real, na Conta do vendedor.
  • Se a Conta ainda não puder gerar cobrança pela API, POST /v1/charges responde 403 account_not_enabled; confira em GET /v1/me, campo charges.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 com errors[].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); um X-Forwarded-For mandado 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_out de GET /v1/me devolve (por dia, por dia a destinos novos e por hora). Acima deles, 422 daily_cap_exceeded ou new_destination_cap_exceeded com limit_scope: "api", ou 429 rate_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

Prefixos de chave e de segredo
PrefixoO 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.

422 · exemplo
{
  "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."
    }
  ]
}
Campos do erro
CampoDescrição
typeurn:validra:error:<code>.
titleTítulo curto, em português.
statusStatus HTTP, repetido no corpo.
detailExplicação em português, para registro e para o vendedor; nunca para o comprador.
codeCódigo estável (tabela abaixo).
request_idO mesmo valor do cabeçalho X-Request-Id.
errorsSó em validation_error: lista de { field, code, message }. Nunca repete o valor enviado.
charge_idCobranç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 de Retry-After): erro de rede, 429 rate_limited, 409 request_in_progress, 500 internal_error e 503 temporarily_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.
Node.js · tratamento
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

Códigos de erro
HTTPcodeO que fazer
400malformed_requestO corpo não é JSON válido em UTF-8. Corrija o pedido.
400idempotency_key_requiredMande Idempotency-Key em todo POST.
400idempotency_key_invalidUse um só cabeçalho, com 8 a 128 caracteres A-Z, a-z, 0-9, _ ou -.
401unauthorizedChave ausente, errada, revogada ou de outro ambiente; ou dois cabeçalhos Authorization.
403forbiddenA chave não tem o escopo da rota.
403ip_not_allowedChamada de IP fora da lista da chave (só quando a conta usa lista). Peça ao titular para incluir o IP.
403account_not_readyA Conta ainda não pode cobrar: o titular precisa concluir o cadastro no portal.
403account_unavailableA Conta não pode gerar cobranças agora. Avise o titular; os canais de atendimento estão no portal, em Ajuda.
403account_not_enabledEsta Conta ainda não pode gerar cobrança pela API; confira charges.enabled em GET /v1/me.
404not_foundCobrança inexistente, de outra Conta ou com id fora do formato (sem diferença entre os casos), ou rota inexistente.
409idempotency_key_reusedA mesma Idempotency-Key foi usada com outro corpo.
409request_in_progressUm pedido com a mesma chave ainda está em curso; repita depois de Retry-After.
409external_id_already_usedJá existe cobrança com esse external_id (vem o charge_id). Consulte-a ou use outro valor.
409webhook_endpoint_not_configuredNão há endereço de notificação cadastrado para a Conta.
413payload_too_largeCorpo acima de 16 KB.
415unsupported_media_typeMande Content-Type: application/json (charset, se vier, utf-8).
422validation_errorCampo inválido; veja errors[].
422charge_rejectedA cobrança não pôde ser registrada (vem o charge_id). Revise o valor ou gere outra tentativa, com novo external_id.
429rate_limitedLimite de chamadas; espere Retry-After.
500internal_errorFalha da ValidraPay. Repita com a mesma Idempotency-Key.
503temporarily_unavailableNada 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.

Códigos de erro das demais funções
HTTPcodeOnde
401signature_requiredTodo POST de chave com segredo de assinatura.
401signature_invalidIdem.
401signature_expiredIdem.
403payouts_not_enabledPix enviado.
403transfers_not_enabledTransferência entre Contas.
403transfer_not_allowedTransferência entre Contas.
403ip_allowlist_requiredPUT e DELETE /v1/webhook-endpoint (só quando a conta usa lista de IPs).
404preview_not_foundPix enviado e transferência.
404webhook_endpoint_not_configuredGET, DELETE /v1/webhook-endpoint (no teste, é 409).
409charge_not_cancellableCancelar cobrança.
409preview_expiredPix enviado e transferência.
409preview_consumedPix enviado e transferência.
409preview_not_readyPix enviado e transferência.
409med_not_openMED.
422destination_unavailablePix enviado e transferência.
422amount_mismatchPix enviado e transferência.
422cooldown_activePix enviado e transferência.
422below_minimumPix enviado e transferência.
422above_maximumPix enviado e transferência.
422daily_cap_exceededPix enviado e transferência.
422new_destination_cap_exceededPix enviado e transferência.
422insufficient_balance_for_feePix enviado e transferência.
422representative_allowance_exceededPix enviado e transferência, chave de representante.
422pausedPix enviado e transferência.
422account_not_readyPix enviado e transferência (aqui com status 422, não 403).
422cursor_invalidLista de cobranças e lista de contestações.
422evidence_refusedMED.
503balance_unavailableSaldo.
503preview_pendingPix 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.

  1. Chave do bot. Peça ao titular uma chave só com charges:write e charges:read (mais webhooks:write, se o bot for cadastrar o próprio endereço de notificação). Sem escopo de envio de Pix.
  2. Gere a cobrança com POST /v1/charges, amount de no mínimo "0.51" e um external_id seu. Guarde o id (chg_...) que a resposta devolve.
  3. Espere o QR com GET /v1/charges/{id} só enquanto a cobrança estiver em processing, uma consulta a cada 1 a 2 segundos. Em active, mande pix_copy_paste ao comprador.
  4. Confirme o pagamento pelo evento charge.paid, com a assinatura conferida, e consulte a cobrança uma vez para conferir paid e o valor antes de liberar o produto.

Armadilhas

Armadilhas comuns na integração de bots
ArmadilhaO 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

POST/v1/chargesescopo: charges:write

Gera uma cobrança Pix imediata na Conta do vendedor dono da chave. Cabeçalhos: Authorization, Content-Type: application/json e Idempotency-Key.

Corpo

Campos do corpo de gerar cobrança
CampoTipoObrigatórioRegra
amounttextosimDecimal com duas casas, mínimo "0.51".
external_idtextosimO 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_ininteironãoSegundos para pagar, de 300 a 86400, contados do registro. Padrão: 3600. Para venda por bot, 1800 costuma bastar.
descriptiontextonão1 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).
payerobjetonã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
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"
  }'
Node.js
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 QR
Python
import 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 QR

Resposta 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.

201 · active
{
  "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.

201 · processing
{
  "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-Key com o mesmo corpo: 201 com a mesma cobrança, no estado atual, e Idempotent-Replayed: true.
  • Mesma Idempotency-Key com outro corpo: 409 idempotency_key_reused.
  • O external_id fica gasto quando a resposta traz um id de cobrança: 201, 409 external_id_already_used e 422 charge_rejected. Nos outros erros ele continua livre.

Consultar cobrança

GET/v1/charges/{charge_id}escopo: charges:read

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âmetros de consultar cobrança
ParâmetroOndeFormato
charge_idcaminhochg_ + 32 caracteres hexadecimais minúsculos, como devolvido na geração. Guarde-o como texto, sem interpretar. Outro formato dá 404.
curl
curl -s https://app.validrapay.com/v1/charges/chg_0f3c9a1e5b2f4c8e9a01d2b3c4e5f607 \
  -H "Authorization: Bearer ${VALIDRA_API_KEY}"
Node.js · consultar e esperar o QR
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
}
Python · consultar e esperar o QR
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
200 · active
{
  "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.

200 · paid
{
  "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

GET/v1/charges/by-external-id/{external_id}escopo: charges:read

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
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

GET/v1/meescopo: qualquer

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.

Campos de GET /v1/me
CampoDescrição
environmentproduction em produção.
api_keyprefix (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.enabledSe a Conta pode gerar cobranças agora. Quando false, reason traz account_not_ready, account_unavailable ou account_not_enabled (veja Erros).
webhook_endpointconfigured, 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).
principalQuem 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).
principal · os dois casos
// chave do titular
"principal": { "role": "holder", "permissions": null }

// chave de representante autorizado
"principal": { "role": "representative", "permissions": ["create_charges", "view_own_sales"] }
curl
curl -s https://app.validrapay.com/v1/me \
  -H "Authorization: Bearer ${VALIDRA_API_KEY}"
200
{
  "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

POST/v1/webhook-endpoint/testescopo: webhooks:write

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
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)"
200 · entregue
{
  "event_id": "evt_01JA7Q9X2M4K8R3T5V6W7Y8Z9A",
  "delivered": true,
  "status_code": 204,
  "duration_ms": 182,
  "error": null
}
200 · não entregue
{
  "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.

Campos do objeto cobrança
CampoTipoDescrição
idtextochg_ + 32 hexadecimais minúsculos.
external_idtexto | nullO external_id mandado na geração.
statustextoEstado (veja Estados da cobrança).
amounttextoValor, por exemplo "49.90".
descriptiontexto | nullO texto mandado, ou null.
txidtextoIdentificador da cobrança no padrão do Pix (26 a 35 caracteres).
pix_copy_pastetexto | nullO copia e cola; null enquanto processing.
qr_code_png_base64texto | nullPNG do QR em base64. Só em active; nos eventos, sempre null.
expires_atdata | nullFim do prazo de pagamento; null enquanto processing.
created_atdataGeração.
updated_atdataÚltima mudança; use para ordenar eventos da mesma cobrança.
paid_atdata | nullQuando o pagamento foi confirmado e a cobrança virou paid.
paid_after_expirationbooleanotrue quando a confirmação chegou com a cobrança já expired.
paymentobjeto | nullend_to_end_id, amount e paid_at (liquidação do Pix). Sem dados do pagador.
amount_refundedtextoTotal devolvido ao pagador; "0.00" sem devolução.
cancelled_atdata | nullQuando a cobrança foi cancelada.
cancel_reasontexto | nullsystem quando o registro da cobrança não se concluiu; seller quando o vendedor cancelou (veja Cancelar cobrança).
cancellationobjeto | nullPedido de cancelamento aberto ou o último que falhou: { status: "requested" | "failed", requested_at, failure_code }. null sem pedido.
paid_after_cancellationbooleanotrue quando o Pix foi confirmado depois de o vendedor cancelar a cobrança.

Estados da cobrança

processing → active → (cancelling →) paid | expired | cancelled

Estados da cobrança
EstadoSignificado
processingSendo registrada, ainda sem QR. Consulte até mudar.
activeAguardando pagamento; tem copia e cola e QR.
paidPaga e confirmada.
expiredVenceu sem pagamento confirmado. Um Pix liquidado ainda pode chegar depois e levá-la a paid, com paid_after_expiration: true.
cancellingPedido de cancelamento em andamento (veja Cancelar cobrança). A cobrança ainda pode ser paga até a remoção ser confirmada.
cancelledO 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_refundedPaga, com parte do valor devolvida ao pagador pelo vendedor, no portal.
refundedPaga 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.

Campos do envelope do evento
CampoDescrição
idevt_ + 26 caracteres; único por evento. Use para deduplicar.
typeTipo do evento, por exemplo charge.paid.
api_versionv1.
environmentproduction; confira antes de usar.
created_atQuando o evento foi gerado.
data.chargeO objeto cobrança, com qr_code_png_base64 sempre null.
Cabeçalhos da notificação
CabeçalhoDescrição
Validra-Signaturet=<unix>,v1=<hex>. O único que vale para decidir (veja Verificar a assinatura).
Validra-Event-IdInformativo, não assinado; igual ao id do corpo.
Validra-Event-TypeInformativo, não assinado; decida pelo type do corpo verificado.
Validra-Delivery-AttemptInformativo, não assinado; número da tentativa, de 1 a 8.
charge.paid · pedido completo
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

Tipos de evento
typeQuando
charge.paidPix confirmado (seção anterior).
charge.expiredA cobrança venceu sem pagamento confirmado; mandado de 2 a 5 minutos depois de expires_at.
charge.cancelledO registro da cobrança não se concluiu (cancel_reason: system) ou o cancelamento pedido pelo vendedor foi confirmado (seller).
charge.cancellation_failedO pedido de cancelamento não foi confirmado; a cobrança continua active e pagável. Pode pedir de novo.
pingSó 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_recipientPix 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.cancelledTransferência entre Contas pela API (ainda não disponível). data.transfer, sem dados do destino.
charge.expired · corpo (trecho)
{
  "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"
    }
  }
}
charge.cancelled · corpo (trecho)
{
  "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"
    }
  }
}
ping · corpo
{
  "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 status e amount_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.

Algoritmo
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
  1. Leia o corpo bruto, byte a byte, antes de qualquer conversão de JSON.
  2. Separe t e os v1 do cabeçalho. Confira t com ^[0-9]{1,12}$ e cada v1 com ^[0-9a-f]{64}$. Ignore esquemas que não conhecer.
  3. Recuse se t estiver a mais de 300 segundos do seu relógio.
  4. Calcule o HMAC e compare em tempo constante. Aceite se qualquer v1 conferir: na troca do segredo vêm dois (o do segredo novo primeiro), por 24 horas.
  5. Assinatura inválida: responda 401 e descarte o corpo.
Node.js (Express)
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
});
Python (Flask)
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 "", 204

Vetor 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.

Vetor
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        b092d0b88bb0bc7eb7a2cf92028417596eb9d7ac97a8ff4296de884a55bbb99f

Entregas 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 = suspended em GET /v1/me).
  • O mesmo evento pode chegar mais de uma vez e fora de ordem. Guarde o id com restrição de unicidade por no mínimo 33 horas, ignore repetidos e ordene eventos da mesma cobrança por data.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.

Estado das funções em produção
FunçãoRotasEstado
Gerar e consultar cobrançaPOST /v1/charges, GET /v1/charges/{id}Disponível
Cancelar cobrançaPOST /v1/charges/{id}/cancelDisponível
Listar cobrançasGET /v1/chargesDisponível
SaldoGET /v1/balanceDisponível
Dados da chave e da contaGET /v1/meDisponível
Endpoint de notificaçãoGET, PUT e DELETE /v1/webhook-endpointDisponí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 pagadorSem rotaAinda não disponível (o titular devolve pelo portal)

Cancelar cobrança

POST/v1/charges/{charge_id}/cancelescopo: charges:write

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.

Respostas de cancelar cobrança
SituaçãoResposta
active, sem pedido aberto202, cobrança em cancelling
cancelling (pedido já aberto)202, a mesma cobrança, sem segundo pedido
cancelled200, a cobrança
processing, paid, partially_refunded, refunded, expired, ou pagamento em confirmação409 charge_not_cancellable, com charge_status
Cobrança de outra Conta, ou de outro usuário para a chave de representante404 not_found
  • O pagamento vale. Enquanto a remoção não for confirmada, a cobrança ainda pode ser paga: chega charge.paid e ela vai a paid. Se um Pix for confirmado depois do cancelled, ela vai a paid com paid_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.status vira failed e sai charge.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-Key devolve o estado atual, com o status HTTP da primeira resposta e Idempotent-Replayed: true.
curl
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"
202 · cancelling
{
  "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

GET/v1/chargesescopo: charges:read

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âmetros de listar cobranças
ParâmetroRegra
created_fromInício da janela, RFC 3339 com fuso (2026-10-01T00:00:00Z). Padrão: 30 dias antes de created_to.
created_toFim da janela, no mesmo formato. Padrão: agora. A janela tem no máximo 31 dias, e created_from vem antes de created_to.
statusRepetível (status=paid&status=refunded): processing, active, paid, expired, cancelled, partially_refunded, refunded. Outro valor dá 422.
limitDe 1 a 100. Padrão: 50.
cursorO 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
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"
200
{
  "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

GET/v1/balanceescopo: balance:read

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.

200 · titular
{
  "kind": "account_balance",
  "status": "ready",
  "available": "820.90",
  "blocked": "0.00",
  "read_at": "2026-10-22T14:30:05Z",
  "source": "institution"
}
202 · leitura em andamento
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.

200 · representante
{
  "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) e withdrawals: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: 403 ip_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. Todo POST, PUT e DELETE dessa 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, com retry_after (segundos) e until.
  • A chave precisa ter sido criada por um usuário ativo da Conta, com o aplicativo autenticador ativo; sem isso, 403 forbidden ou 422 account_not_ready.
  • Revogar a chave no portal recusa na hora os envios dela que ainda não foram transmitidos.

Limites

Limites do Pix enviado
LimiteValorAcima dele
Por envioDe R$ 5,00 a R$ 1.000,00422 below_minimum ou above_maximum
Por dia, por ContaR$ 3.000,00, ou R$ 10.000,00 a partir de 2.000 vendas pagas e confirmadas422 daily_cap_exceeded, com renews_at (e, ao titular, daily_cap e daily_used)
Por dia, para destino que ainda não recebeu da ContaR$ 1.000,00 somados422 new_destination_cap_exceeded
Envios por chave30 por hora429 rate_limited, com Retry-After
Prévias por Conta20 por hora, somadas portal e API429 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 (fee na 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, com max_sendable (o máximo que você pode enviar agora). Sem leitura de saldo disponível: 503 temporarily_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.

Algoritmo
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ÉTODO em 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çalho Idempotency-Key. Corpo vazio: o SHA-256 de zero bytes. Hexadecimal sempre em minúsculas.
  • PUT e DELETE não usam Idempotency-Key: neles IDEMPOTENCY_KEY é vazio, mesmo que você mande o cabeçalho (a mensagem fica com dois pontos seguidos entre o caminho e o hash do corpo). O DELETE vai sem corpo.
  • Assine os bytes que você vai mandar, não um JSON montado de novo depois.
Recusas da assinatura
HTTPcodeQuando
401signature_requiredCabeçalho ausente.
401signature_invalidMais 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.
401signature_expiredt 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).

Node.js
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,
});
Python
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.

Vetor
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=970389988e66a2e9d5056bdca1060f80137cb3cac7730dcc3729f210af9bba7f

Prévia do Pix

POST/v1/payouts/previewescopo: withdrawals:write

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.

Corpo da prévia
CampoTipoRegra
pix_keytextoA chave Pix do destino. Chave inválida: 422 validation_error.
amounttextoValor com duas casas, maior que zero ("150.00").
Corpo
{
  "pix_key": "+5511999990000",
  "amount": "150.00"
}
201
{
  "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"
}
Campos da prévia
CampoDescrição
idpvw_ + 32 hexadecimais. Use no envio.
recipientname, document_masked, institution_name e ispb do recebedor. Nunca o documento completo.
ownershipown (o próprio titular) ou third_party.
new_destinationtrue se o destino ainda não recebeu da Conta (conta no limite de destino novo).
feeTarifa do envio; total = amount + fee.
checkswithin_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_atA prévia vale por 10 minutos.
  • Chave Pix inexistente ou destino que não pode receber: 422 destination_unavailable, com reason: KEY_NOT_FOUND, DESTINATION_NOT_VERIFIED, THIRD_PARTY_NOT_ENABLED, DESTINATION_NOT_ALLOWED, LOOKUP_FAILED ou ACCOUNT_UNAVAILABLE. Sem dado do recebedor.
  • Consulta ainda em andamento: 503 preview_pending com Retry-After; repita com a mesma Idempotency-Key e o mesmo corpo.
  • Mesma Idempotency-Key com outra chave Pix ou outro valor: 409 idempotency_key_reused.

Enviar o Pix

POST/v1/payoutsescopo: withdrawals:write

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).

Corpo do envio
CampoTipoObrigatórioRegra
preview_idtextosimO id da prévia, da mesma chave.
amounttextosimIgual ao da prévia; diferente dá 422 amount_mismatch.
descriptiontextonãoAté 140 caracteres, texto neutro.
Corpo
{
  "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.*.

202
{
  "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"
}
Erros da prévia usada no envio
HTTPcodeSignificado
404preview_not_foundPrévia inexistente, de outra chave ou de outra Conta (sem diferença entre os casos).
409preview_expiredA prévia venceu; faça outra.
409preview_consumedA prévia já foi usada num envio.
409preview_not_readyA consulta da prévia ainda não terminou.
422amount_mismatchValor 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.

422 · limite do dia
{
  "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

GET/v1/payouts/{payout_id}escopo: withdrawals:read
GET/v1/payoutsescopo: withdrawals:read

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
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}"
Estados do Pix enviado
statusSignificado
processingRecebido e em conferência ou em transmissão.
pending_approvalPedido de representante esperando aprovação.
settledLiquidado; end_to_end_id preenchido.
rejectedRecusado antes da transmissão; veja reason.
failedNão concluído pela instituição; veja reason.
cancelledNão transmitido; veja reason.
refunded_by_recipientO recebedor devolveu o valor inteiro.
partially_refunded_by_recipientO 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.

payout.settled · corpo
{
  "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

POST/v1/transfers/previewescopo: transfers:write
POST/v1/transfersescopo: transfers:write
GET/v1/transfers/{transfer_id}escopo: transfers:read
GET/v1/transfersescopo: transfers:read

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.

Corpo · pelo ID de cliente
{
  "destination": { "client_id": "cli_7k2m6q4x" },
  "amount": "80.00"
}
Corpo · pelo documento
{
  "destination": { "document": "<CPF ou CNPJ do titular, só dígitos>" },
  "amount": "80.00"
}
Corpo · pelo e-mail
{
  "destination": { "email": "<e-mail cadastrado do titular>" },
  "amount": "80.00"
}
201
{
  "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.

202
{
  "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

GET/v1/med/infractionsescopo: med:read
GET/v1/med/infractions/{id}escopo: med:read
POST/v1/med/infractions/{id}/evidenceescopo: med:write
POST/v1/med/infractions/{id}/statementescopo: med:write
DELETE/v1/med/infractions/{id}/evidence/{evidence_id}escopo: med:write
POST/v1/med/infractions/{id}/submitescopo: med:write

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) e cursor (o next_cursor anterior; inválido dá 422 cursor_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 .../evidence com kind (DELIVERY_PROOF, CONVERSATION, INVOICE ou OTHER), content_type (application/pdf, image/png ou image/jpeg, conferido pelo conteúdo), content_base64 e filename (opcional). Por padrão, até 5 arquivos de até 2 MiB cada (os limites vigentes vêm em limits, no detalhe); o corpo desta rota aceita até 3 MiB. Resposta 201 com a evidência, sem o conteúdo.
  • Relato. POST .../statement com text (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.
  • POST pede Idempotency-Key e, se a chave tiver segredo de assinatura, Validra-Request-Signature; o DELETE da evidência também vai assinado, com IDEMPOTENCY_KEY vazio. Não há recusa da contestação pela API.
Erros do MED
HTTPcodeQuando
404not_foundContestação inexistente, de outra Conta, de outra cobrança ou ainda não liberada (sempre a mesma resposta).
409med_not_openPrazo vencido ou envio já concluído.
422evidence_refusedArquivo ou texto fora dos limites, com reason: kind, content_type, too_large, too_many, empty, invalid_base64, integrity ou statement_too_long.
Evidência · corpo
{
  "kind": "DELIVERY_PROOF",
  "filename": "comprovante-entrega.pdf",
  "content_type": "application/pdf",
  "content_base64": "JVBERi0xLjQK...(encurtado)"
}
201
{
  "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"
  }
}
Relato · corpo
{
  "text": "O produto foi entregue em 20/10 pelo bot; a conversa vai anexada."
}

Endpoint de notificação pela API

GET/v1/webhook-endpointescopo: webhooks:write
PUT/v1/webhook-endpointescopo: webhooks:write
DELETE/v1/webhook-endpointescopo: webhooks:write

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 com secret.
  • Mesma URL, sem rotação: 200 sem secret (perdeu o segredo: rotacione).
  • Endereço suspenso: o PUT com a mesma URL o reativa.
  • URL fora dos requisitos: 422 validation_error (campo url).

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, PUT e DELETE exigem chave com lista preenchida; sem ela, 403 ip_allowlist_required.
  • No máximo 10 PUT/DELETE por hora por chave e 3 trocas de URL por dia por usuário; acima, 429 rate_limited com Retry-After.
  • Chave com segredo de assinatura (vps_...): PUT e DELETE vão assinados (veja Assinatura do pedido), com IDEMPOTENCY_KEY vazio; sem a assinatura, 401 signature_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
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 }'
201
{
  "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ções não disponíveis na API
FunçãoSituação
ExtratoNão há rota de extrato. O titular consulta o extrato no portal.
Devolução ao pagadorNã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 boletosNã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 APINão há. Pela API só se consulta e se mandam evidências (veja MED).
CriptoativosA API não converte nem envia valores para criptoativos.
Transferência entre Contas e MED pela APIAs 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.