FFC Business & Finance · API

Documentação da API de pagamentos. Pix, cartão, boleto, internacional, cripto e cobrança recorrente, 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 '{ "amount": 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.
bill_pay:createPagar boletos. Paga boletos de terceiros. ENVIA dinheiro para fora da conta; só marque se a integração realmente precisa pagar boletos.
bill_pay:readConsultar pagamentos de boleto. Lê os pagamentos de boleto 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). No cartão, define o NOME NA FATURA (statement/soft descriptor) desta transação; deve IDENTIFICAR o seu negócio. É sanitizado (MAIÚSCULAS, sem acento, só A-Z 0-9 e espaço) e cortado no limite da adquirente (cerca de 13 caracteres; até 22 conforme o tipo de conta). IMPORTANTE: a linha do extrato é COMPOSTA pela adquirente com o nome do recebedor cadastrado, então este campo ajusta o SUFIXO, não substitui o nome inteiro. Se ausente, usa o nome do seu estabelecimento. (Recurso atrás de liberação; enquanto desligado, o campo não altera a fatura.)
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.processorstring?Só method=card. DEVE bater com o processor devolvido por GET /card-tokenization (o token pertence àquela processadora). Divergente → a cobrança é recusada (re-tokenize com a atual). Compatibilidade: o campo card.acquirer legado ainda é aceito na requisição.
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.
card.installmentsint?Só method=card. Parcelas SEM juros ao comprador (1 = à vista, default). O número MÁXIMO de parcelas vem no GET /card-tokenization (campo maxInstallments): é a capacidade DESTA conta. Pedir MAIS que o máximo NÃO é re-clampado em silêncio: a cobrança é RECUSADA com erro explícito informando o máximo daquela transação (sem cobrar). O comprador paga o total dividido por N sem juros; o custo da parcela é do lojista (taxa por parcela).

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.processorcp_06Código OPACO da processadora; tem que bater com o processor devolvido por GET /card-tokenization. (card.acquirer legado ainda é aceito.)
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,
  "processor": "cp_06",            // código OPACO da processadora DESTA conta (eco em card.processor)
  "scheme": "s1",                  // formato de tokenização (ver "Passo 2"): "s1" | "s2" | "sandbox"
  "publicKey": "pk_...",           // chave publicável (vai ao navegador)
  "tokenizeUrl": "https://...",    // endpoint de tokenização (do provedor); poste o cartão AQUI
  "maxInstallments": 18            // nº máximo de parcelas DESTA conta (capacidade)
}

O processor é um código estável e OPACO (não revela a processadora; roteamento é interno). Chave nvr_dev_ (teste) aponta para o sandbox. 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 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-cartao-006",
    "customer": {
      "name": "Maria Silva",
      "document": "12345678909",
      "email": "maria@exemplo.com",
      "phone": "+5521999998888"
    },
    "card": {
      "token": "token_lgxVY49NcNtZLPGq",
      "processor": "cp_06",
      "holderPostalCode": "20000000",
      "holderAddressNumber": "100",
      "installments": 3
    }
  }'

card.installments é opcional (default 1 = à vista). Parcelas SEM juros ao comprador: ele paga o total dividido por N; a taxa por parcela é do lojista. O máximo é a capacidade DESTA conta, informada em maxInstallments no GET /card-tokenization. Pedir mais que o máximo não é re-clampado em silêncio: a cobrança é recusada com erro explícito informando o máximo daquela transação (sem cobrar).

Resposta de sucesso (aprovado):

{
  "id": "ord_3Kk9x2",
  "externalReference": "teste-cartao-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 formatos de tokenização

Siga sempre o que o /card-tokenization devolveu para a conta. O campo scheme diz o FORMATO da requisição; poste SEMPRE na tokenizeUrl devolvida (é o endpoint do provedor de tokenização). Se o processor/scheme da conta mudar, o token antigo não vale mais: re-tokenize com o atual. Os formatos diferem:

scheme: s1

POST <tokenizeUrl devolvida pelo /card-tokenization>
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 EXPIRA EM ~60 SEGUNDOS. Tokenize e cobre em seguida.

scheme: s2

POST <tokenizeUrl devolvida 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_...", ... }

scheme: sandbox

A chave de teste (nvr_dev_) devolve scheme: "sandbox" e não tem endpoint de tokenização (vem sem tokenizeUrl). Nenhuma processadora real é tocada. Você monta o token localmente, sem chamar nada: sbxtok_ seguido dos dígitos do cartão de teste. A decisão é determinística pelo último dígito: PAR aprova, ÍMPAR recusa.

// GET /api/v1/card-tokenization com chave de teste (nvr_dev_) → SEMPRE sandbox:
{
  "available": true,
  "processor": "cp_sandbox",
  "scheme": "sandbox",
  "publicKey": "pk_sandbox_dev",
  "maxInstallments": 1
  // NÃO vem "tokenizeUrl": o sandbox não tem endpoint de tokenização (a publicKey não é usada)
}

// Monte o token LOCALMENTE (não chame nenhum endpoint):
//   token = "sbxtok_" + <dígitos do cartão de teste>
// Último dígito decide (determinístico):
"sbxtok_4111111111111112"   // termina em 2 (PAR)   → APROVA
"sbxtok_4111111111111111"   // termina em 1 (ÍMPAR) → RECUSA

Cobrando com o token de sandbox (mesmos campos obrigatórios de cartão):

curl -X POST https://paynuvra.com/api/v1/charges \
  -H "Authorization: Bearer nvr_dev_SUA_CHAVE_AQUI" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 3000,
    "method": "card",
    "externalReference": "teste-sandbox-001",
    "customer": {
      "name": "Maria Silva",
      "document": "12345678909",
      "email": "maria@exemplo.com",
      "phone": "+5521999998888"      // telefone do portador: OBRIGATÓRIO no cartão
    },
    "card": {
      "token": "sbxtok_4111111111111112",  // termina em PAR → aprova
      "processor": "cp_sandbox",           // tem que bater com o processor do GET
      "holderPostalCode": "20000000",      // CEP do portador: OBRIGATÓRIO
      "holderAddressNumber": "100"         // número do endereço: OBRIGATÓRIO
    }
  }'

Cartão via API (sandbox ou produção) exige a flag global API_CARD_CHARGES_ENABLED: enquanto ela estiver desligada, tanto o GET /card-tokenization quanto o POST /charges de cartão respondem 503 (card_charges_disabled). O sandbox usa a chave nvr_dev_ (homologação, sem dinheiro real).

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 dura ~60s; demorou entre tokenizar e cobrar.Tokenize e chame POST /charges em seguida. Gere um token novo se expirar.
Token de outra processadoraO token pertence ao processor do /card-tokenization; a configuração da conta mudou.Re-tokenize (confira processor 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). Para autenticação 3DS (quando ligada para o seu estabelecimento), veja a seção 3DS abaixo.

3DS (autenticação do cartão)

O 3D Secure adiciona a autenticação do banco do portador ao cartão (liability shift). É opcional e ligado por estabelecimento: com 3DS desligado, o cartão via API segue igual. Com 3DS ligado, a cobrança de cartão exige a autenticação - feita no navegador pelo SDK 3DS da processadora. O PAN não passa pelo seu servidor nem pelo nosso. (O scriptUrl é o SDK do provedor de 3DS; a URL é dele.)

1. Peça o token 3DS (server-side, sua chave):

POST/api/v1/3ds/token
curl -X POST https://paynuvra.com/api/v1/3ds/token \
  -H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI"
# → { "token": "<token 3DS>", "scriptUrl": "<URL do SDK 3DS do provedor>", "env": "prod" }

2. No navegador: carregue o scriptUrl e rode TDS.init com os dados do cartão:

const r = await window.TDS.init(
  { token, tds_method_container_element: elA, challenge_container_element: elB, use_default_challenge_iframe_style: true, challenge_window_size: "03" },
  { payments: [{ payment_method: "credit_card", credit_card: { card: { number, holder_name, exp_month, exp_year, billing_address } }, amount }], customer, items }
);
// r[0] = { tds_server_trans_id, trans_status, authenticated_card, challenge_canceled }
// O SDK renderiza o desafio (iframe) sozinho quando o banco exige. Frictionless: retorna direto.

3. Envie a cobrança com o resultado da autenticação:

curl -X POST https://paynuvra.com/api/v1/charges \
  -H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "card",
    "amount": 20000,
    "customer": { "name": "João Silva", "document": "12345678901", "email": "joao@ex.com", "phone": "+5511999998888" },
    "card": {
      "token": "<card_token da tokenização>",
      "holderPostalCode": "01234567", "holderAddressNumber": "123",
      "authentication": { "transactionId": "<tds_server_trans_id>", "transStatus": "Y" }
    },
    "externalReference": "pedido-3ds-1"
  }'

trans_status: Y autenticado · A tentado (ambos seguem, com liability shift) · N não autenticado · R negado · U indisponível · C desafio não concluído · I informativo. Com 3DS ligado, só Y/A prosseguem; os demais (ou ausência da authentication) resultam em recusa honesta - nada é cobrado. A aprovação final é sempre confirmada por reconsulta/webhook(charge.paid), nunca pelo retorno da tela.

Pré-requisito: 3DS habilitado na processadora (Visa/Mastercard, 3DS 2.x) e ligado pela plataforma para o seu estabelecimento. O antifraude/AVS de hoje (documento, e-mail, telefone, CEP e número) continua valendo junto com o 3DS.

Cobrança via Boleto

POST/api/v1/charges

Mesmo endpoint da cobrança Pix, com "method": "boleto". A resposta traz a linha digitável (boleto.barcode) e o vencimento (boleto.dueDate). Requer acesso de produção com o método Boleto liberado e uma adquirente de boleto ativa na conta.

curl -X POST https://paynuvra.com/api/v1/charges \
  -H "Authorization: Bearer nvr_live_SUA_CHAVE_AQUI" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "boleto",
    "amount": 15000,
    "customer": { "name": "Maria Silva", "document": "12345678909", "email": "maria@ex.com" },
    "externalReference": "pedido-4471"
  }'
{
  "id": "ord_...",
  "method": "boleto",
  "status": "pending",
  "amount": 15000,
  "boleto": {
    "barcode": "34191.79001 01043.510047 91020.150008 9 96660000019790",
    "dueDate": "2026-09-03T00:00:00.000Z"
  }
}

O crédito nunca vem só do webhook: o pagamento é confirmado pelo evento charge.paid seguido de reconsulta na adquirente (fonte da verdade). Liquida em D+2 (dias úteis). Consulte o status por GET /api/v1/charges/{id} como fallback.

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 e o antifraude (validação de titular/AVS) são nossos: você não toca em dado de cartão (PCI mínimo). Quando o 3DS está ligado para o seu estabelecimento, a autenticação do banco também entra no fluxo (ver a seção 3DS).

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. Obrigatório.
descriptionstring?Aparece no checkout. Opcional (máx. 200).
externalReferencestring?Sua referência (idempotência/conciliação). Opcional (máx. 128).

Erros deste endpoint:

CampoTipoDescrição
400live_key_requiredSessão embedded exige chave de produção (nvr_live_); sandbox não cria sessão.
403method_not_grantedO método cartão não está liberado no seu acesso de produção.
403account_not_clearedConta sem KYC aprovado / e-mail não confirmado.
422embed_not_configuredNenhum domínio de embed cadastrado (Desenvolvedor → Domínios de embed).
422validation_errorCampos inválidos (veja details).
503function_unavailableCobrança de cartão bloqueada para a conta no momento.
502session_failedFalha ao criar a sessão. Tente novamente.

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 (análise da adquirente) 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 }
  ]
}

Internacional - câmbio e recebimento

Requer o método internacional liberado no acesso de produção (Desenvolvedor → Acesso de produção) e a habilitação da conta pela plataforma. Sem isso: 403 method_not_granted. Valores em unidades menores (centavos p/ BRL/MXN).

Câmbio (2 passos: cotar → converter)

POST/api/v1/fx/quote

Cota BRL→MXN ou BRL→USDT (a cotação é assinada e expira).

curl -X POST https://paynuvra.com/api/v1/fx/quote \
  -H "Authorization: Bearer nvr_live_..." -H "Content-Type: application/json" \
  -d '{ "from": "BRL", "to": "MXN", "amountCents": 100000 }'
// → { "quoteId": "...", "rate": ..., "estimatedAmountCents": ..., "feeCents": ..., "expiresAt": "..." }
POST/api/v1/fx/convert

Executa a cotação DENTRO da validade. Idempotente pela cotação (converte 1×).

-d '{ "quoteId": "...", "idempotencyKey": "op-123" }'
// 409 quote_expired (fora da validade) · 503 liquidity_unavailable (sem float; nada debitado)

Mínimo de conversão: R$ 50,00 (padrão; abaixo disso a taxa fixa do câmbio fica alta demais). Abaixo → recusa com o valor mínimo na mensagem.

Recebimento internacional

POST/api/v1/foreign/cash-in

Gera a instrução de recebimento (CLABE/SPEI, MXN). O crédito é confirmado por webhook + reconsulta - nunca pelo corpo da resposta.

-d '{ "currency": "MXN", "amount": 50000 }'   // amount em unidades menores (centavos MXN)
// → { "id": "...", "clabe": "...", "status": "awaiting", ... }
GET/api/v1/foreign/cash-in/{id}

Status do recebimento: awaitingcredited (fonte da verdade: reconsulta no webhook).

Mínimo de recebimento: 20,00 MXN (piso da adquirente para SPEI/CLABE). Abaixo → 422 below_minimum.

Pagamento internacional (saída)

POST/api/v1/foreign/payout

Envia moeda para fora: MXN via CLABE/SPEI (clabe+name+document) ou USDT on-chain TRON (address). O destinatário é validado ANTES de qualquer débito (rede/CLABE erradas = perda irreversível). Idempotente por externalReference. Requer o método internacional + permissão payout:create.

-d '{ "currency": "MXN", "amount": 50000, "externalReference": "pay-1",
      "clabe": "0123...(18)", "name": "Fulano", "document": "..." }'
// USDT: { "currency": "USDT", "amount": 100000000, "externalReference": "pay-2", "address": "T..." }
// → { "id": "...", "status": "processing", "currency": "MXN", "amount": 50000 }
// 422 insufficient_funds (saldo do cofre) · 422 invalid_recipient · 502 foreign_payout_failed (nada debitado)
GET/api/v1/foreign/payout/{id}

Estado público: processingcompleted | failed. A liquidação (e o estorno da reserva em falha) fecha SÓ por webhook + reconsulta da mesa - nunca pelo corpo. Enquanto as flags de saída não ligarem, responde 503 foreign_payout_disabled.

Moedas: câmbio BRL↔MXN/USDT; recebimento MXN; saída MXN (CLABE/SPEI) e USDT (TRON). Outras não são suportadas pela adquirente internacional hoje. Valores em unidades menores (MXN scale 2; USDT scale 6).

Cripto - compra e saque

Requer o método cripto liberado no acesso de produção. Fluxo em 2 passos (cotar → executar). Valores em centavos. O envio on-chain é IRREVERSÍVEL: a rede e o endereço são validados e travados na cotação.

POST/api/v1/crypto/quote

Cota compra (operation:"buy") ou saque (operation:"withdraw"). O endereço/rede entram AQUI (travados na cotação).

curl -X POST https://paynuvra.com/api/v1/crypto/quote \
  -H "Authorization: Bearer nvr_live_..." -H "Content-Type: application/json" \
  -d '{ "operation": "buy", "asset": "USDT", "amount": 50000, "address": "0x...", "network": "polygon" }'
// → { "quoteId": "...", "price": "...", "cryptoAmount": "...", "spread": <centavos>, "expiresAt": "...", "irreversible": true }
POST/api/v1/crypto/buy

Executa a compra da cotação (dentro da validade). Idempotente pela cotação.

-d '{ "quoteId": "...", "idempotencyKey": "op-1" }'   // → { "operationId": "...", "state": "settling", "cryptoAmount": "..." }
POST/api/v1/crypto/withdraw

Saca o saldo em cripto. Se enviar address/network, DEVEM bater com os da cotação (anti-swap) - senão 422 address_mismatch.

-d '{ "quoteId": "...", "address": "0x...", "network": "polygon", "idempotencyKey": "op-2" }'
// 409 quote_expired (fora da validade) · 502 crypto_settlement_failed (falha na mesa; NADA debitado, estorno net-zero)

Limites (compra e saque):

CampoTipoDescrição
Mínimo por ordemR$ 300,00 (para todos)Regra de margem da plataforma (a mesa cobra a partir de R$ 150; os R$ 300 são a margem Nuvra). NÃO é por estabelecimento. Abaixo → 422 below_min ("O valor mínimo para compra/saque de cripto é R$ …").
Máximo por operaçãoR$ 5.000,00 (padrão)Acima → 422 over_tx ("Valor acima do máximo por operação (R$ …)."). Definido pela plataforma (env).
Teto diárioR$ 20.000,00 (padrão)Soma de compras/saques do dia. Acima → 422 over_day ("Limite diário … atingido."). Verificado atomicamente no confirm.

Venda de cripto (off-ramp) - ASSÍNCRONA:

POST/api/v1/crypto/sell

Cria a ordem de venda. Devolve o depositAddress da mesa: o cliente envia a cripto para lá. O depósito on-chain é IRREVERSÍVEL.

-d '{ "asset": "USDT", "amount": "100", "walletAddress": "0x...", "network": "polygon" }'
// → {
//   "id": "...", "side": "sell", "depositAddress": "0x...", "status": "awaiting_deposit", "network": "polygon",
//   "estimatedBrl": {                     // PREVISÃO do que entra no seu saldo
//     "estimated": true, "locked": false, // referência do momento da criação - NÃO travado
//     "quotedAt": "2026-09-01T...Z",
//     "grossBrlCents": 60000,             // bruto estimado (cotação da mesa)
//     "feeCents": 900, "netCents": 59100, // taxa/spread + líquido estimado
//     "note": "Estimativa (...). O valor final é o do momento da liquidação."
//   }
// }

estimatedBrl é ESTIMATIVA, não valor travado. A venda é assíncrona: o depósito on-chain chega depois, então o preço final é o do momento da liquidação. Quando a ordem liquida, o GET traz o valor CONFIRMADO (estimated: false, locked: true) e os campos grossBrlCents/feeCents/netCents no topo.

POST/api/v1/crypto/sell/{id}/confirm

Informa a transactionHash do depósito on-chain. NÃO credita - o crédito em BRL fecha só pela fonte da verdade (webhook da mesa + reconsulta).

-d '{ "transactionHash": "0x..." }'   // → { "id": "...", "status": "processing" }
GET/api/v1/crypto/sell/{id}

Estado público da ordem: awaiting_depositprocessingcompleted (com grossBrlCents/feeCents/netCents) · failed · cancelled (expira em 24h sem depósito). Acompanhe por polling neste GET; o crédito aparece no seu saldo quando a mesa confirma o recebimento (webhook interno).

Depósito a MENOR ou a MAIOR (o valor informado é só referência da cotação):

O crédito é sempre proporcional ao que REALMENTE chegou - a mesa converte a cripto efetivamente depositada e nós creditamos esse BRL menos a taxa. Oamount informado na criação NÃO trava nada; serve só para a cotação de referência.

  • Depositou MENOS: credita o líquido do valor menor que chegou. Se o BRL recebido ficar abaixo de R$ 1,00, a ordem vai a failed e NADA é creditado (não fica pendente esperando o resto).
  • Depositou MAIS: credita o líquido do valor maior - sem teto. Você recebe pelo que efetivamente enviou.

Nunca há crédito parcial "aguardando o resto": cada depósito é conciliado pelo que a mesa confirmou. A previsão em BRL (estimatedBrl) é do valor informado; o valor final é o do depósito real na liquidação.

Redes suportadas (compra, saque e venda) - validadas ANTES de qualquer débito:

CampoTipoDescrição
polygonEVM - 0x + 40 hexEndereço fora do formato → recusado no quote (nunca debita).
ethereumEVM - 0x + 40 hexMesmo formato do Polygon: escolha a rede CERTA - endereço EVM não distingue a chain (ver aviso).
tronT + 33 (base58)USDT-TRC20 típico.
solanabase58, 32–44

Rede errada = perda irreversível. Validamos o FORMATO por rede antes do débito (rede fora da lista ou formato inválido → recusa). Mas endereços EVM (polygon/ethereum) têm o MESMO formato - se você escolher a chain errada com um endereço EVM válido, o formato passa e a mesa faz a 2ª checagem; confira sempre a rede. O par ativo × rede (ex.: USDT) é definido e validado pela MESA na cotação - não é lista fixa nossa; par não suportado → 422 unsupported. A VENDA nasce indisponível até as flags ligarem (503 crypto_sell_disabled). (A mesa é uma instituição parceira; o roteamento é interno.)

Pix Automático (cobrança recorrente)

Requer o método Pix Automático (recorrente) liberado no acesso de produção. O pagador AUTORIZA a recorrência uma vez (paga o QR da 1ª cobrança) e as próximas são debitadas automaticamente. Valores em centavos. Idempotente por externalReference.

POST/api/v1/recurring/authorizations

Cria a autorização + o plano. Valor fixo (amount) OU variável (minAmount = piso), nunca os dois. A resposta traz o consent (QR copia-e-cola + imagem) que o pagador paga para autorizar - a assinatura nasce awaiting_authorization e vira active quando a autorização é confirmada.

curl -X POST https://paynuvra.com/api/v1/recurring/authorizations \
  -H "Authorization: Bearer nvr_live_..." -H "Content-Type: application/json" \
  -d '{
    "externalReference": "assinatura-123",
    "description": "Plano mensal",
    "amount": 5000,
    "frequency": "MONTHLY",
    "startDate": "2026-09-10",
    "retryPolicy": "three_in_seven",
    "customer": { "name": "Fulano", "document": "12345678909", "email": "f@ex.com" }
  }'
// → { "id": "...", "status": "awaiting_authorization", "consent": { "pixCopyPaste": "...", "qrCodeBase64": "..." },
//     "frequency": "MONTHLY", "nextChargeAt": "...", "amount": 5000 }
CampoTipoDescrição
externalReferencestringSua referência única (idempotência). Repetir devolve a mesma assinatura.
amount / minAmountintegerValor fixo por ciclo (amount) OU piso p/ valor variável (minAmount). Centavos. Exatamente um.
frequencystringWEEKLY | MONTHLY | QUARTERLY | SEMIANNUALLY | ANNUALLY.
startDate / finishDatedateInício e teto temporal opcional (YYYY-MM-DD).
retryPolicystringnone (sem retentativa) | three_in_seven (até 3 em 7 dias corridos por saldo insuficiente).
customerobjectname, document (CPF/CNPJ), email?, phone? do pagador.
GET/api/v1/recurring/authorizations/{id}

Status da assinatura: awaiting_authorization · active · failing (um ciclo falhou por saldo; a autorização segue ativa) · cancelled_by_seller · cancelled_by_payer · refused · expired.

GET/api/v1/recurring/authorizations/{id}/charges

Histórico dos ciclos: cada um com status (scheduled | paid | refused | cancelled), amount, dueDate, paidAt.

DELETE/api/v1/recurring/authorizations/{id}

Cancela a autorização (encerra os agendamentos pendentes). Idempotente. O pagador também pode cancelar pelo banco dele - refletimos como cancelled_by_payer.

Cada ciclo pago é uma VENDA (entra no seu saldo com a taxa Pix, liquidação normal) e dispara os mesmos webhooks de cobrança (order.approved). Falha por saldo insuficiente NÃO cancela a assinatura - o próximo ciclo segue.

Enquanto o motor de recorrência não está ligado, a rota responde 503 recurring_disabled (honesto, nunca cria pela metade). Contrato via instituição parceira (Jornada 3).

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.

Limite diário. Existe um teto de saque por dia (fuso America/Sao_Paulo). O painel e a API somam no MESMO balde: um saque feito pelo painel consome o mesmo limite de um saque por API. Excedido → resposta 422 com code: "daily_limit_exceeded" e mensagem informando o teto do dia e o quanto ainda resta. Nada é debitado.

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"

Pagar boleto (bill_pay)

POST/api/v1/bill-payments

Paga um boleto de terceiro a partir do seu saldo. Dinheiro saindo: exige a permissão sensível bill_pay:create (opt-in) e o mesmo IP allowlist do saque. O valor é sempre o do código de barras(o amountCents só é usado quando o boleto não embute valor). Boleto vencido é recusado. Idempotente por externalReference.

curl -X POST https://paynuvra.com/api/v1/bill-payments \
  -H "Authorization: Bearer nvr_live_SUA_CHAVE_COM_BILL_PAY" \
  -H "Content-Type: application/json" \
  -d '{
    "identificationField": "34191.79001 01043.510047 91020.150008 9 96660000019790",
    "externalReference": "conta-luz-08-2026"
  }'

Resposta: { id, status, amount, beneficiary, dueDate, externalReference, createdAt }. status: processing · paid · failed.

GET/api/v1/bill-payments/{id}

A confirmação é sempre pela fonte da verdade (reconsulta na fonte da verdade via webhook bill_payment.paid / bill_payment.failed) - nunca só pela resposta imediata. Falha na adquirente estorna o débito automaticamente.

Webhooks de saída

Cadastre a URL em Desenvolvedor → Webhooks e escolha os eventos. Eventos disponíveis:

order.createdorder.approvedorder.refusedorder.refundedcharge.createdcharge.paidcharge.failedcharge.refundedcharge.disputedcart.abandonedtransfer.completedpayout.requestedpayout.completedpayout.failedsubscription.activatedsubscription.cancelled_by_payersubscription.refusedrecurring_charge.refused

Pagamento aprovado - quais eventos chegam? Todo pagamento aprovado (checkout, link de cobrança e API) dispara os dois: order.approved e seu apelido público charge.paid - com o mesmo objeto data. Assine apenas um deles (ou trate os dois como o mesmo evento, deduplicando por data.id) para não processar a venda em dobro. O mesmo vale para os pares order.refused/charge.failed e order.refunded/charge.refunded. charge.created acompanha order.created (cobrança gerada). Além desses, charge.disputed chega quando uma venda vira disputa/chargeback e transfer.completed quando uma transferência interna é concluída.

Pix Automático (recorrente). Cada ciclo cobrado é uma venda: quando liquida, chega order.approved/charge.paid como qualquer cobrança (mesmo formato). Consulte o estado da assinatura e o histórico de ciclos pelas rotas /api/v1/recurring/authorizations/{id} e /charges. Falha de um ciclo por saldo insuficiente vira order.refused e a assinatura fica failing, mas a autorização permanece ativa para o próximo ciclo (não é cancelada).

Eventos do ciclo de vida da assinatura. Além dos de cobrança, você pode assinar: subscription.activated (o pagador autorizou a recorrência), subscription.cancelled_by_payer (cancelou pelo banco dele), subscription.refused (não autorizada) e recurring_charge.refused (um ciclo falhou por saldo insuficiente/sem limite). O data traz id/externalReference da assinatura (e cycleReference/amount no ciclo recusado).

O corpo é { event, data, sentAt }. Para eventos order.* (e seus apelidos charge.*), o data traz o pedido (o valor vem em total, com amountCents como apelido do mesmo valor):

{
  "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).
422daily_limit_exceededTeto diário de saque atingido (painel + API somam no mesmo balde, fuso America/Sao_Paulo). A message traz o teto do dia e o disponível restante. Nada foi debitado.
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 não está liberado no seu acesso de produção. Métodos: Pix, cartão, boleto, internacional (câmbio + recebimento) e cripto (compra/saque). Solicite a liberação do método em Desenvolvedor → Acesso de produção.
403method_unavailableO método ainda não tem rota pública executando. Aparece na seleção como indisponível; nunca é concedido nem cobrado. (Hoje todos os métodos publicados executam.)
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.

UTMify (rastreamento de conversão)

Ao conectar sua conta UTMify, toda venda aprovada é enviada automaticamente para a UTMify (server-to-server), com as UTMs capturadas no checkout. Você não precisa programar nada; só colar o token.

Como conectar

  1. Entre no painel da UTMify com a sua conta.
  2. Abra Integrações → Credenciais de API.
  3. Clique em Adicionar Credencial e dê um nome.
  4. Copie o token (ele só aparece uma vez).
  5. Cole em Integrações → UTMify no painel e salve.

Use Credenciais de API, não Webhooks; são coisas diferentes e o token de Webhooks não funciona aqui.

Quando enviamos

Na aprovação da venda (o pagamento confirmado). Também refletimos reembolso quando ocorre. Nada é enviado enquanto a venda está pendente.

O que enviamos

CampoTipoDescrição
orderIdstringIdentificador do pedido na Nuvra.
platformstringOrigem do evento (OneAOne).
paymentMethodstringpix, credit_card ou boleto.
statusstringwaiting_payment, paid, refused ou refunded.
createdAt / approvedDate / refundedAtdatetimeDatas do pedido, aprovação e reembolso (UTC).
customer.name / email / phone / documentstringDados do comprador (o que ele informou no checkout).
customer.country / ipstringPaís (BR) e IP do comprador na compra.
products[].id / name / planId / planName / quantity / priceInCentsarrayItens da venda e valores em centavos.
trackingParameters.utm_source / utm_medium / utm_campaign / utm_content / utm_termstringUTMs capturadas no checkout.
trackingParameters.src / sckstringParâmetros extras de rastreio (quando presentes).
commission.totalPriceInCents / gatewayFeeInCents / userCommissionInCents / currencyobjectValores da venda em centavos e a moeda (BRL).

Não apareceu lá?

  • Confirme o estado em Integrações → UTMify: precisa estar Conectado (verde). Se estiver "Falta conectar", cole o token.
  • Veja as últimas tentativas na mesma tela: o motivo aparece em português (token inválido, dado faltando).
  • Token inválido: gere um novo em Credenciais de API (não em Webhooks) e cole de novo.
  • Só contam vendas aprovadas depois de você conectar; vendas anteriores não são reenviadas.

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.