Nuvra · API
Documentação da API de pagamentos. Pix e cartão, com webhooks e saques.
Comece aqui em 5 minutosComece por aqui
Integre em 5 minutos. Você vai precisar de uma chave de API (aba Chaves, que exige KYC aprovado e 2FA ativado).
- Crie uma chave de teste (
nvr_dev_). Ela opera no sandbox, sem dinheiro real. - Crie uma cobrança Pix e mostre o QR/copia-e-cola ao seu cliente:
curl -X POST https://paynuvra.com/api/v1/charges \
-H "Authorization: Bearer nvr_dev_SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{ "amountCents": 5000, "externalReference": "pedido-123", "customer": { "name": "Maria", "document": "12345678909" } }'- Assine um webhook (aba Webhooks) para receber
charge.paidquando o cliente pagar. - Trocou pra produção? Gere uma chave
nvr_live_e use a mesma basehttps://paynuvra.com/api/v1.
É isso. As seções abaixo detalham autenticação, cada recurso, erros e webhooks.
Autenticação
Gere a chave em Desenvolvedor → Chaves de API e envie no header Authorization como Bearer token. Chaves nvr_dev_ operam em homologação (sandbox); nvr_live_ em produção. Base: https://paynuvra.com/api/v1.
curl https://paynuvra.com/api/v1/balance \ -H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI"
Sem chave válida → 401 unauthorized. Excesso de requisições → 429 rate_limited.
Cada chave carrega permissões granulares. Ao criar, o escopo pré-marca a lista e você ajusta. payout:create é sensível (envia dinheiro) e só entra por marcação explícita.
| Permissão | O que permite |
|---|---|
charge:create | Criar cobranças. Gera novas cobranças (Pix, cartão, boleto). |
charge:read | Consultar cobranças. Lê cobranças e o status de pagamento. |
charge:refund | Estornar cobranças. Estorna (reembolsa) uma cobrança paga. DEVOLVE dinheiro ao comprador e debita seu saldo; só marque se a integração realmente precisa estornar. |
payout:create | Criar saques. Emite saques Pix. ENVIA dinheiro para fora da conta; só marque se a integração realmente precisa sacar. |
payout:read | Consultar saques. Lê os saques e o status de cada um. |
order:create | Criar pedidos. Cria pedidos pela API. |
order:read | Consultar pedidos. Lê os pedidos da conta. |
balance:read | Consultar saldo. Lê o saldo disponível e a receber. |
webhook:create | Criar webhooks. Cadastra endpoints para receber eventos. |
webhook:read | Consultar webhooks. Lê os webhooks configurados e as entregas. |
webhook:update | Editar webhooks. Altera URL, eventos ou status de um webhook. |
webhook:delete | Remover webhooks. Exclui webhooks (ação destrutiva). |
Criar cobrança (Pix)
/api/v1/chargesIdempotente por externalReference: reenviar a mesma referência com o mesmo payload devolve a cobrança existente (200) em vez de criar outra (201). A adquirente não é chamada de novo.
Reenviar a mesma externalReference com um payload diferente (outro valor, método, documento ou e-mail) responde 409 idempotency_error. Nunca devolvemos a cobrança antiga em silêncio (isso mascararia, por exemplo, reusar a referência de um R$10 numa cobrança de R$1.000). Para uma cobrança nova, use uma referência nova; para repetir, mande o payload idêntico.
| Campo | Tipo | Descrição |
|---|---|---|
| amount | int (centavos) | Obrigatório. Mín. 100 (R$1,00). |
| customer.name | string | Obrigatório. |
| customer.document | string | Obrigatório. CPF ou CNPJ. |
| customer.email | string? | Opcional no Pix; OBRIGATÓRIO no cartão. |
| customer.phone | string? | Opcional no Pix; OBRIGATÓRIO no cartão. Aceito em QUALQUER formato BR, com ou sem +55, com ou sem pontuação (ex.: (11) 99999-8888, 11999998888, +5511999998888). Normalizamos para E.164; precisa ser um número BR válido (DDD + 8 dígitos fixo ou 9 celular), senão é tratado como ausente. Devolvido normalizado em order.*/cart.abandoned; habilita recuperação por WhatsApp. |
| externalReference | string | Obrigatório (máx. 128). Sua chave de idempotência. |
| description | string? | Opcional (máx. 200). |
| expiresIn | int? | Opcional. Segundos até expirar (60–86400). |
| method | "pix" | "card" | "boleto" | Opcional (default pix). Cartão exige o bloco card (veja “Cobrar no cartão”); boleto devolve boleto.barcode + boleto.dueDate e confirma pelo webhook. |
| card.token | string | Só method=card. Token gerado no navegador (o PAN nunca toca nosso servidor). |
| card.acquirer | string? | Só method=card. DEVE bater com o acquirer devolvido por GET /card-tokenization (o token pertence àquela adquirente). Divergente → a cobrança é recusada (re-tokenize com a atual). |
| card.holderPostalCode | string | OBRIGATÓRIO no cartão. CEP do titular (antifraude/AVS das adquirentes). |
| card.holderAddressNumber | string | OBRIGATÓRIO no cartão. Número do endereço do titular. |
No cartão, além do token, são obrigatórios customer.email, customer.phone (com DDD), card.holderPostalCode (CEP) e card.holderAddressNumber. Faltando qualquer um, a API responde 422 validation_error apontando o campo, antes de tocar a adquirente.
curl -X POST https://paynuvra.com/api/v1/charges \
-H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"customer": { "name": "Maria Silva", "document": "12345678909" },
"externalReference": "pedido-4821",
"description": "Plano Pro",
"expiresIn": 3600
}'Resposta (201):
{
"id": "ord_...",
"externalReference": "pedido-4821",
"status": "pending",
"amount": 15000,
"pix": {
"copyPaste": "00020126...5204",
"qrCodeBase64": "data:image/png;base64,...",
"expiresAt": "2026-07-08T12:00:00.000Z"
}
}status: pending · approved · expired · refunded · refused. paidAt vem quando aprovado. Acompanhe a aprovação pelo webhook order.approved (não faça polling agressivo).
Cobrar no cartão (via API)
token de uso único, e só esse token vai para a sua API. Nunca tokenize no backend, nunca guarde o token (ele é de uso único e expira). Isso mantém o seu PCI no nível mínimo (SAQ-A).Cobre no cartão pela sua própria tela (sem iframe), em 3 passos: (1) descubra o tokenizador da conta, (2) tokenize o cartão no navegador, (3) crie a cobrança com o token.
O que o cartão exige (todos obrigatórios)
As adquirentes de cartão validam o titular (antifraude/AVS), então estes campos são obrigatórios. Faltando qualquer um, a resposta é 422 validation_error apontando o campo, e nada é cobrado.
| Campo | Exemplo | Por que |
|---|---|---|
amount | 3000 | Valor em centavos (R$ 30,00). |
externalReference | pedido-77 | Sua chave de idempotência (reenviar devolve a mesma cobrança). |
customer.name | Maria Silva | Nome do titular do cartão. |
customer.document | 12345678909 | CPF ou CNPJ, só dígitos. Exigido no antifraude. |
customer.email | maria@exemplo.com | Recibo e antifraude da adquirente. |
customer.phone | +5521999998888 | Qualquer formato BR (com/sem +55, com/sem pontuação); normalizamos. Precisa ser válido com DDD, as adquirentes recusam cartão sem telefone. |
card.token | token_lgxVY49... | Token gerado no NAVEGADOR (passo 2). Nunca no backend. |
card.acquirer | pagarme | Tem que bater com o acquirer devolvido por GET /card-tokenization. |
card.holderPostalCode | 20000000 | CEP do titular, só dígitos. AVS/antifraude. |
card.holderAddressNumber | 100 | Número do endereço do titular. AVS. |
Exemplo completo (aprovado em produção)
Passo 1. Descubra o tokenizador da conta:
/api/v1/card-tokenizationcurl https://paynuvra.com/api/v1/card-tokenization \
-H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI"
// resposta
{
"available": true,
"acquirer": "pagarme", // a adquirente de cartão DESTA conta
"tokenizer": "pagarme", // como tokenizar (ver "Passo 2")
"publicKey": "pk_...", // chave publicável (vai ao navegador)
"tokenizeUrl": "https://..." // presente só quando o tokenizador exige (ex.: Xlent)
}Chave nvr_dev_ (teste) aponta para o sandbox, nenhuma adquirente real é tocada. Sem tokenizador disponível, available vem false.
Passo 2. Tokenize o cartão no navegador (ver os dois formatos logo abaixo) e obtenha o token.
POST /api/v1/charges em seguida. Não guarde o token nem tokenize com antecedência. Se estourar os 60s, gere um novo antes de cobrar.Passo 3. Crie a cobrança com o token (o número/validade/CVV do cartão nunca vão no corpo, a API os recusa):
/api/v1/chargescurl -X POST https://paynuvra.com/api/v1/charges \
-H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"amount": 3000,
"method": "card",
"externalReference": "teste-pagarme-006",
"customer": {
"name": "Maria Silva",
"document": "12345678909",
"email": "maria@exemplo.com",
"phone": "+5521999998888"
},
"card": {
"token": "token_lgxVY49NcNtZLPGq",
"acquirer": "pagarme",
"holderPostalCode": "20000000",
"holderAddressNumber": "100"
}
}'Resposta de sucesso (aprovado):
{
"id": "ord_3Kk9x2",
"externalReference": "teste-pagarme-006",
"status": "approved",
"amount": 3000, // bruto (centavos)
"fee": 169, // taxa
"net": 2831, // líquido que entra no seu saldo
"currency": "BRL",
"method": "card",
"customer": { "name": "Maria Silva", "document": "12345678909", "email": "maria@exemplo.com" },
"paidAt": "2026-08-20T21:03:00.000Z",
"createdAt": "2026-08-20T21:02:58.000Z"
}O status já pode vir approved ou refused; confirme sempre pelo webhook order.approved.
Passo 2 em detalhe: os dois formatos de tokenização
Siga sempre o que o /card-tokenization devolveu para a conta. Se a adquirente da conta mudar (ex.: de Xlent para Pagar.me), o token antigo não vale mais: re-tokenize com a atual. Os formatos diferem:
Pagar.me (tokenizer: pagarme)
POST https://api.pagar.me/core/v5/tokens?appId=<publicKey>
Content-Type: application/json
{
"type": "card",
"card": {
"number": "4111111111111111",
"holder_name": "Maria Silva",
"exp_month": 12, // INTEIRO (1 a 12)
"exp_year": 2028, // AAAA (4 dígitos)
"cvv": "123"
}
}
// resposta: { "id": "token_...", ... }
// ATENÇÃO: o token da Pagar.me EXPIRA EM 60 SEGUNDOS. Tokenize e cobre em seguida.Xlent (tokenizer: xlent)
POST <tokenizeUrl devolvido pelo /card-tokenization>
Content-Type: application/json
{
"publishableKey": "<publicKey>", // vai no CORPO
"card": {
"number": "4111111111111111",
"expMonth": "12", // MM (2 dígitos, string)
"expYear": "28", // AA (2 dígitos, string)
"cvv": "123",
"holderName": "Maria Silva"
}
}
// resposta: { "token": "token_...", ... }Erros comuns (causa e correção)
| Erro | Causa | Correção |
|---|---|---|
422 validation_error | Faltou um campo obrigatório do titular (o corpo aponta qual em details.fieldErrors). | Envie o campo indicado. Nada foi cobrado. |
409 idempotency_error | A externalReference já foi usada com um payload diferente (outro valor/método/documento/e-mail). | Use uma referência nova para uma cobrança nova; para repetir, mande o payload idêntico. |
402 card_charge_failed | A adquirente recusou o cartão (saldo/limite/antifraude). Nada é debitado. | Tente outro cartão. O motivo cru fica no painel (transação, ícone de detalhe). |
Token expirado | O token da Pagar.me dura 60s; demorou entre tokenizar e cobrar. | Tokenize e chame POST /charges em seguida. Gere um token novo se expirar. |
Token de outra adquirente | O token pertence à adquirente do /card-tokenization; a designação da conta mudou. | Re-tokenize com a adquirente atual (confira acquirer no /card-tokenization). |
Corpo de um 422 (campo faltando):
{
"error": "validation_error",
"details": {
"formErrors": [],
"fieldErrors": {
"card": ["Para cartão, informe o CEP do titular (card.holderPostalCode)."]
}
}
}Requer acesso de produção com o método Cartão liberado (Desenvolvedor, Acesso de produção). Autenticação 3DS será adicionada numa próxima fase.
Consultar cobrança
/api/v1/charges/{id}Devolve o mesmo objeto da criação, com o status atual (útil como fallback ao webhook).
curl https://paynuvra.com/api/v1/charges/ord_... \ -H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI"
Checkout embedded (cartão)
O comprador paga o cartão sem sair da sua tela: você cria a sessão pela API, recebe uma embed_url e embute nosso checkout num iframe. A tokenização, o 3DS e o antifraude são nossos: você não toca em dado de cartão (PCI mínimo).
1. Crie a sessão (server-side, com sua chave nvr_live_):
/api/v1/checkout-sessionscurl -X POST https://paynuvra.com/api/v1/checkout-sessions \
-H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{ "amount": 3490, "description": "Recuperacao carrinho #8842" }'{
"id": "COB-8F3K2A",
"object": "checkout_session",
"method": "card",
"amount": 3490,
"embed_url": "https://paynuvra.com/embed/eyJ...<token>",
"expires_at": "2026-08-11T23:59:00.000Z",
"allowed_origins": ["https://sualoja.com"]
}| Campo | Tipo | Descrição |
|---|---|---|
| amount | int (centavos) | Valor da cobrança. Definido no servidor, o comprador NÃO altera. |
| description | string? | Aparece no checkout. Opcional. |
2. Embute no seu site (uma linha de SDK):
<script src="https://paynuvra.com/embed.js"></script>
<div id="pay"></div>
<script>
PayEmbed.checkout({
url: EMBED_URL, // a embed_url da sessão
mount: "#pay",
onPaid: function(e){ window.location = "/obrigado"; },
onRefused: function(e){ /* mostrar erro, permitir tentar de novo */ },
onClose: function(e){ /* comprador fechou */ }
});
</script>Eventos recebidos na sua página: onReady, onResize (altura automática), onProcessing, onPaid, onRefused, onPending (3DS/análise) e onClose. O webhook charge.paid continua sendo a confirmação oficial (o evento na tela é pra UX).
Segurança: só os domínios cadastrados na sua conta (Desenvolvedor → Domínios de embed) podem embutir o checkout; a sessão expira; e o valor vem sempre da cobrança criada por API.
Saldo
/api/v1/balanceSaldos por método em centavos: disponível, pendente (a liquidar) e reservado (retido por disputa/MED).
{
"environment": "live",
"balances": [
{ "method": "pix", "available": 124050, "pending": 0, "reserved": 0 }
]
}Pedidos
/api/v1/ordersOs últimos 50 pedidos da conta (id, cliente, valor em centavos, status, método, data).
Payout (saque via API)
/api/v1/payoutsRequer uma chave com permissão de payout (sem ela → 403 forbidden_scope). Pode exigir IP na allowlist. Recebedoras precisam de KYC aprovado: pendente retorna 403 kyc_required. Idempotente por externalReference.
| Campo | Tipo | Descrição |
|---|---|---|
| amount | int (centavos) | Valor que o DESTINATÁRIO recebe. A taxa é debitada por cima. |
| pixKey | string | Chave Pix do destinatário. |
| pixKeyType | "cpf"|"cnpj"|"email"|"phone"|"random" | Tipo da chave (validado). |
| externalReference | string? | Sua chave de idempotência. |
| description | string? | Opcional. |
curl -X POST https://paynuvra.com/api/v1/payouts \
-H "Authorization: Bearer nvr_live_SUA_CHAVE_DE_PAYOUT" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"pixKey": "12345678909",
"pixKeyType": "cpf",
"externalReference": "saque-1099"
}'Resposta: { id, status, amount (líquido), fee, totalDebited, ... }. status: processing · completed · failed.
Consultar saque
/api/v1/payouts/{id}Requer a mesma chave com permissão de payout. Devolve o saque com o status atual: use como fallback ao webhook payout.completed / payout.failed.
curl https://paynuvra.com/api/v1/payouts/pay_... \ -H "Authorization: Bearer nvr_live_SUA_CHAVE_DE_PAYOUT"
Webhooks de saída
Cadastre a URL em Desenvolvedor → Webhooks e escolha os eventos. Eventos disponíveis:
order.createdorder.approvedorder.refusedorder.refundedcharge.paidcart.abandonedpayout.requestedpayout.completedpayout.failedO corpo é { event, data, sentAt }. Para eventos order.*, o data traz o pedido:
{
"event": "order.approved",
"data": {
"id": "ord_...",
"status": "approved",
"method": "pix",
"total": 15000,
"customer": {
"name": "Maria Silva",
"email": "maria@example.com",
"phone": "+5511999998888" // E.164 quando o comprador informou; null se não
},
"createdAt": "2026-07-08T11:00:00.000Z"
},
"sentAt": "2026-07-08T11:00:01.000Z"
}O customer.phone é o telefone do comprador em E.164 (+55…) quando informado, ou null. Ele aparece em todos os order.*/charge.* e no cart.abandoned (carrinho pendente não pago dentro da janela): use-o para recuperar a venda por WhatsApp. Sem telefone informado, o evento chega sem telefone.
Cada entrega traz os headers X-Nuvra-Event (tipo), X-Nuvra-Event-Id (id ÚNICO da entrega) e X-Nuvra-Signature: um HMAC-SHA256 do corpo cru usando o segredo do webhook. Deduplique pelo X-Nuvra-Event-Id, uma reentrega carrega o MESMO id. Contra replay, confira também o sentAt do corpo (está assinado): rejeite entregas fora de uma janela (ex.: ±5 min). O histórico fica em Desenvolvedor → Webhooks. Recalcule e compare a assinatura para validar a origem:
// Node.js: validação da assinatura
import { createHmac } from "node:crypto";
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody) // corpo CRU (string), antes de JSON.parse
.digest("hex");
if (expected !== req.headers["x-nuvra-signature"]) {
return res.status(401).end(); // origem não confiável
}
const { event, data, sentAt } = JSON.parse(rawBody);Erros
Erros vêm como { error, message?, details? } com o código HTTP:
| Campo | Tipo | Descrição |
|---|---|---|
| 401 | unauthorized | Chave ausente, inválida ou revogada. |
| 403 | insufficient_scope | A chave não tem a permissão exigida (ex.: charge:create). O campo required indica qual falta. |
| 403 | forbidden_scope / ip_not_allowed / kyc_required | Sem permissão de payout, IP não autorizado, ou KYC pendente. |
| 403 | api_disabled | API da conta desabilitada pela plataforma. |
| 422 | validation_error | Campos inválidos (veja details). |
| 422 | refund_not_allowed / simulate_not_allowed | Estorno só de cobrança paga; simulação só no sandbox em teste. |
| 402 | card_charge_failed | Cartão recusado/indisponível (nada foi debitado). Veja message/fieldErrors. |
| 403 | method_not_granted | O método (ex.: cartão) não está liberado no seu acesso de produção. |
| 503 | card_charges_disabled / method_unavailable | Cobrança no cartão via API ainda não habilitada, ou sem adquirente ativa para o método. |
| 502 | refund_failed | A adquirente recusou o estorno (nada foi debitado do seu saldo). |
| 429 | rate_limited | Muitas requisições. Respeite o header Retry-After (segundos) antes de repetir. Limites: charges 10/s (rajada 20), payouts 2/s (rajada 5), leituras 10/s. |
| 400 | invalid_json | Corpo não é JSON válido. |
| 502 | charge_failed / payout_failed | Falha ao processar no adquirente. |
Limitações e disponibilidade
Para evitar surpresa de integração, o que ainda não está disponível hoje:
- Estorno via API:
POST /api/v1/charges/{id}/refundestorna uma cobrança paga (permissãocharge:refund, opt-in; idempotente). Cancelar cobrança pendente segue no painel. - Sandbox, simular pagamento: em teste (chave
nvr_dev_),POST /api/v1/charges/{id}/simulate-approvalaprova a cobrança e dispara o webhookorder.approved, pra você exercitar o fluxo charge→webhook sem pagar de verdade. - Listagem de payouts não é exposta; consulte um saque por ID (
/api/v1/payouts/{id}). GET /api/v1/ordersé paginado por cursor:?limit=(1–100, default 50) e?cursor=(onextCursorda página anterior;null= acabou).
Entrega de webhook: até 3 tentativas com backoff na hora + reentrega por cron (outbox durável), timeout de 8s. O corpo traz event, data e sentAt; a resposta de venda/o webhook trazem fee, net e endToEndId (e2e) para reconciliação. Trate os eventos de forma idempotente, deduplique pelo header X-Nuvra-Event-Id. Se um evento falhar, reconsulte o recurso pelo GET correspondente.