Nuvra · API

Documentação da API de pagamentos. Pix e cartão, com webhooks e saques.

Comece aqui em 5 minutos

Comece por aqui

Integre em 5 minutos. Você vai precisar de uma chave de API (aba Chaves, que exige KYC aprovado e 2FA ativado).

  1. Crie uma chave de teste (nvr_dev_). Ela opera no sandbox, sem dinheiro real.
  2. 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" } }'
  1. Assine um webhook (aba Webhooks) para receber charge.paid quando o cliente pagar.
  2. Trocou pra produção? Gere uma chave nvr_live_ e use a mesma base https://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ãoO que permite
charge:createCriar cobranças. Gera novas cobranças (Pix, cartão, boleto).
charge:readConsultar cobranças. Lê cobranças e o status de pagamento.
charge:refundEstornar 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:createCriar saques. Emite saques Pix. ENVIA dinheiro para fora da conta; só marque se a integração realmente precisa sacar.
payout:readConsultar saques. Lê os saques e o status de cada um.
order:createCriar pedidos. Cria pedidos pela API.
order:readConsultar pedidos. Lê os pedidos da conta.
balance:readConsultar saldo. Lê o saldo disponível e a receber.
webhook:createCriar webhooks. Cadastra endpoints para receber eventos.
webhook:readConsultar webhooks. Lê os webhooks configurados e as entregas.
webhook:updateEditar webhooks. Altera URL, eventos ou status de um webhook.
webhook:deleteRemover webhooks. Exclui webhooks (ação destrutiva).

Criar cobrança (Pix)

POST/api/v1/charges

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

CampoTipoDescrição
amountint (centavos)Obrigatório. Mín. 100 (R$1,00).
customer.namestringObrigatório.
customer.documentstringObrigatório. CPF ou CNPJ.
customer.emailstring?Opcional no Pix; OBRIGATÓRIO no cartão.
customer.phonestring?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.
externalReferencestringObrigatório (máx. 128). Sua chave de idempotência.
descriptionstring?Opcional (máx. 200).
expiresInint?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.tokenstringSó method=card. Token gerado no navegador (o PAN nunca toca nosso servidor).
card.acquirerstring?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.holderPostalCodestringOBRIGATÓRIO no cartão. CEP do titular (antifraude/AVS das adquirentes).
card.holderAddressNumberstringOBRIGATÓ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)

O número do cartão (PAN) nunca passa pelo seu servidor. A tokenização acontece no navegador do comprador, no ato da compra: o navegador manda o cartão direto para a adquirente e recebe um 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.

CampoExemploPor que
amount3000Valor em centavos (R$ 30,00).
externalReferencepedido-77Sua chave de idempotência (reenviar devolve a mesma cobrança).
customer.nameMaria SilvaNome do titular do cartão.
customer.document12345678909CPF ou CNPJ, só dígitos. Exigido no antifraude.
customer.emailmaria@exemplo.comRecibo e antifraude da adquirente.
customer.phone+5521999998888Qualquer formato BR (com/sem +55, com/sem pontuação); normalizamos. Precisa ser válido com DDD, as adquirentes recusam cartão sem telefone.
card.tokentoken_lgxVY49...Token gerado no NAVEGADOR (passo 2). Nunca no backend.
card.acquirerpagarmeTem que bater com o acquirer devolvido por GET /card-tokenization.
card.holderPostalCode20000000CEP do titular, só dígitos. AVS/antifraude.
card.holderAddressNumber100Número do endereço do titular. AVS.

Exemplo completo (aprovado em produção)

Passo 1. Descubra o tokenizador da conta:

GET/api/v1/card-tokenization
curl 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.

O token da Pagar.me expira em 60 segundos. Tokenize no navegador e chame 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):

POST/api/v1/charges
curl -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)

ErroCausaCorreção
422 validation_errorFaltou um campo obrigatório do titular (o corpo aponta qual em details.fieldErrors).Envie o campo indicado. Nada foi cobrado.
409 idempotency_errorA 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_failedA 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 expiradoO 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 adquirenteO 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

GET/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_):

POST/api/v1/checkout-sessions
curl -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"]
}
CampoTipoDescrição
amountint (centavos)Valor da cobrança. Definido no servidor, o comprador NÃO altera.
descriptionstring?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

GET/api/v1/balance

Saldos 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

GET/api/v1/orders

Os últimos 50 pedidos da conta (id, cliente, valor em centavos, status, método, data).

Payout (saque via API)

POST/api/v1/payouts

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

CampoTipoDescrição
amountint (centavos)Valor que o DESTINATÁRIO recebe. A taxa é debitada por cima.
pixKeystringChave Pix do destinatário.
pixKeyType"cpf"|"cnpj"|"email"|"phone"|"random"Tipo da chave (validado).
externalReferencestring?Sua chave de idempotência.
descriptionstring?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

GET/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.failed

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

CampoTipoDescrição
401unauthorizedChave ausente, inválida ou revogada.
403insufficient_scopeA chave não tem a permissão exigida (ex.: charge:create). O campo required indica qual falta.
403forbidden_scope / ip_not_allowed / kyc_requiredSem permissão de payout, IP não autorizado, ou KYC pendente.
403api_disabledAPI da conta desabilitada pela plataforma.
422validation_errorCampos inválidos (veja details).
422refund_not_allowed / simulate_not_allowedEstorno só de cobrança paga; simulação só no sandbox em teste.
402card_charge_failedCartão recusado/indisponível (nada foi debitado). Veja message/fieldErrors.
403method_not_grantedO método (ex.: cartão) não está liberado no seu acesso de produção.
503card_charges_disabled / method_unavailableCobrança no cartão via API ainda não habilitada, ou sem adquirente ativa para o método.
502refund_failedA adquirente recusou o estorno (nada foi debitado do seu saldo).
429rate_limitedMuitas 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.
400invalid_jsonCorpo não é JSON válido.
502charge_failed / payout_failedFalha 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}/refund estorna uma cobrança paga (permissão charge: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-approval aprova a cobrança e dispara o webhook order.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= (o nextCursor da 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.