Pular para o conteúdo
xFlow Payments

Blog

API Pix em Node.js

Para integrar a API Pix em Node.js, chame POST /api/v1/charges com fetch e Basic auth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste da resposta e confirme o pagamento pelo webhook transaction.paid, validando a assinatura HMAC-SHA256 do corpo cru. Antes da produção, teste com chaves pk_test_ e simule o pagamento.

Atualizado em · Equipe xFlow Payments · 7 min de leitura

Você integra a API Pix em Node.js em três passos: criar a cobrança, mostrar o QR Code ao pagador e confirmar o pagamento pelo webhook. Tudo com o fetch nativo do Node.js 18 ou superior, sem pacote extra para falar com a API. O código abaixo segue a documentação da API, usa a base https://app.xflowpayments.com/api/v1 e funciona igual com chave de produção e de teste. Se você ainda está avaliando a integração como um todo, comece pelo guia da API Pix.

O que você precisa

  • Uma chave de API, gerada no painel em Desenvolvedores > Chaves de API. A chave pública (pk_live_...) fica sempre visível. O segredo (sk_live_...) aparece uma única vez.
  • Para testar, uma chave do ambiente Teste (pk_test_... e sk_test_...). Ela pode ser criada antes da aprovação do cadastro.
  • Node.js 18 ou superior, que já traz fetch.
  • Três variáveis de ambiente: XFLOW_PUBLIC_KEY, XFLOW_SECRET_KEY e, para o webhook, XFLOW_WEBHOOK_SECRET.

O segredo fica só no servidor. Nunca o envie ao navegador nem o grave no repositório.

Um cliente HTTP pequeno

A autenticação é HTTP Basic: usuário é a chave pública, senha é o segredo. Valores monetários vão em centavos, como inteiros. Toda resposta traz um request_id (req_...) no corpo e no header x-request-id. Esta função concentra autenticação, JSON e erros:

js
// xflow.mjs
const BASE = "https://app.xflowpayments.com/api/v1";
const AUTH =
  "Basic " +
  Buffer.from(`${process.env.XFLOW_PUBLIC_KEY}:${process.env.XFLOW_SECRET_KEY}`).toString("base64");

export async function xflow(path, { method = "GET", body, idempotencyKey } = {}) {
  const headers = { Authorization: AUTH };
  if (body) headers["Content-Type"] = "application/json";
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

  const res = await fetch(BASE + path, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });
  const texto = await res.text();
  const json = texto ? JSON.parse(texto) : {};

  if (!res.ok) {
    const erro = new Error(`${res.status} ${json.error}: ${json.message} (${json.request_id})`);
    erro.status = res.status;
    erro.code = json.error;
    erro.retryAfter = Number(res.headers.get("retry-after")) || null;
    throw erro;
  }
  return json;
}

Grave o request_id dos erros no seu log. Com ele você encontra a chamada em Integrações > Logs da API, onde cada requisição autenticada aparece com status, duração e código de erro por 30 dias. A lista completa de códigos está em erros da API Pix.

Criar a cobrança Pix

js
import { xflow } from "./xflow.mjs";

const pedido = { id: "1234", totalCents: 1990 };

const cobranca = await xflow("/charges", {
  method: "POST",
  idempotencyKey: `pedido-${pedido.id}`,
  body: {
    amountCents: pedido.totalCents,
    customer: {
      name: "Cliente Exemplo",
      email: "cliente@exemplo.com",
      document: "00000000000", // CPF ou CNPJ, só dígitos
    },
    description: `Pedido #${pedido.id}`,
    external_reference: `pedido-${pedido.id}`,
  },
});

console.log(cobranca.id, cobranca.status); // "clx...", "pending"

Três detalhes evitam retrabalho:

  1. Idempotency-Key única por pedido. Se a rede cair e você repetir a chamada, a API devolve a mesma cobrança em vez de gerar outro Pix. Reusar a chave com um corpo diferente retorna 409 idempotency_conflict. Mais sobre isso em idempotência em API de pagamento.
  2. external_reference (até 120 caracteres) é o id do pedido no seu sistema. Ele volta em toda resposta e webhook da cobrança e serve de filtro em GET /api/v1/charges?external_reference=pedido-1234.
  3. Limites de valor. Cada conta tem valor mínimo e máximo por cobrança. Fora da faixa, a resposta é 400 validation e nada é criado.

Mostrar o QR Code e o copia e cola

A resposta 201 já traz o QR pronto. Você não precisa gerar imagem no seu servidor:

js
const html = `
  <img src="${cobranca.pix.qrCodeBase64}" alt="QR Code Pix" width="240" height="240">
  <textarea readonly>${cobranca.pix.copyPaste}</textarea>
`;

pix.qrCodeBase64 é um PNG em data-URL, pronto para <img src>. pix.copyPaste é o código no padrão BR Code que o pagador cola no app do banco. Trate esse texto como opaco e repasse sem alterar. Os dois campos vêm no 201 e no GET /api/v1/charges/{id}, mas não na listagem. Para detalhes de exibição, veja Pix copia e cola por API e QR Code Pix dinâmico por API.

Receber o webhook com Express

Cadastre a URL em Integrações > Webhooks ou por POST /api/v1/webhooks. Ela precisa começar com https:// e ser pública. A cada evento, a xFlow envia um POST JSON com o header x-webhook-event e o header x-webhook-signature: sha256=..., que é o HMAC-SHA256 do corpo cru calculado com o segredo do endpoint.

Como a validação usa o corpo cru, a rota recebe o corpo com express.raw, e não com express.json:

js
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SEGREDO = process.env.XFLOW_WEBHOOK_SECRET;

function assinaturaValida(raw, header) {
  const esperado = "sha256=" + createHmac("sha256", SEGREDO).update(raw).digest("hex");
  const a = Buffer.from(esperado);
  const b = Buffer.from(header ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

const app = express();

app.post("/webhooks/xflow", express.raw({ type: "application/json" }), async (req, res) => {
  if (!Buffer.isBuffer(req.body) || !assinaturaValida(req.body, req.get("x-webhook-signature"))) {
    return res.sendStatus(401);
  }
  const { event, data } = JSON.parse(req.body.toString("utf8"));

  try {
    if (event === "transaction.paid" && data.livemode) {
      await darBaixaNoPedido(data.external_reference, data.pix.endToEnd);
    }
    res.sendStatus(200);
  } catch {
    res.sendStatus(500); // sem 2xx, a entrega é reenviada
  }
});

app.listen(3000);

darBaixaNoPedido é a sua função de negócio. Quatro cuidados:

  • Responda 2xx para confirmar. Sem isso, a xFlow reenvia até 3 vezes. Como o mesmo evento pode chegar mais de uma vez, a baixa precisa ser idempotente: se o pedido já está pago, não faça nada.
  • Filtre livemode. Eventos gerados com chave de teste trazem livemode: false no data. Não dê baixa em pedido real com eles.
  • No webhook, o valor vem em amount, com net e fee em transaction.paid. Na cobrança, o campo é amountCents.
  • payer e pix.endToEnd chegam do PSP na confirmação e servem para conciliar com o extrato. Veja o que é o End-to-end ID.

O guia completo de eventos está em webhook Pix.

A mesma rota com http nativo

Sem Express, o módulo node:http resolve. O cuidado é o mesmo: juntar o corpo em Buffer antes de validar.

js
import { createServer } from "node:http";
// assinaturaValida: a mesma função do exemplo com Express

createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/webhooks/xflow") {
    return res.writeHead(404).end();
  }
  const partes = [];
  req.on("data", (parte) => partes.push(parte));
  req.on("end", async () => {
    const raw = Buffer.concat(partes);
    if (!assinaturaValida(raw, req.headers["x-webhook-signature"])) {
      return res.writeHead(401).end();
    }
    const { event, data } = JSON.parse(raw.toString("utf8"));
    try {
      await tratarEvento(event, data); // sua função
      res.writeHead(200).end();
    } catch {
      res.writeHead(500).end();
    }
  });
}).listen(3000);

Testar no sandbox

Troque as variáveis para a chave de teste. Os endpoints e os formatos de resposta são os mesmos, e toda resposta traz livemode: false. Nenhum provedor Pix é chamado, então o pix.copyPaste de teste é um marcador que não pode ser pago. Para simular o pagamento:

js
const teste = await xflow("/charges", {
  method: "POST",
  body: {
    amountCents: 1990,
    customer: { name: "Cliente", email: "c@exemplo.com", document: "00000000000" },
  },
});

await xflow(`/test/charges/${teste.id}/pay`, { method: "POST" });

const paga = await xflow(`/charges/${teste.id}`);
console.log(paga.status, paga.livemode); // "paid", false

A cobrança vira paid, a taxa é calculada com as taxas reais da sua conta e o webhook transaction.paid é disparado com livemode: false. A simulação só aceita chave de teste: com chave de produção, a resposta é 403 test_mode_only. Saques não rodam em teste (403 test_mode_unsupported). O passo a passo do ambiente está em sandbox da API Pix.

Erros e limite de requisições

  • 401 unauthorized: chave ausente, inválida ou revogada, ou IP fora da allowlist configurada no painel.
  • 429 rate_limited: espere os segundos do header retry-after (no exemplo, erro.retryAfter) e tente de novo com backoff. O limite é por conta, e toda resposta autenticada traz x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset.
  • 502 charge_failed: falha ao gerar a cobrança no provedor. Guarde o request_id e tente de novo com backoff.

Antes de ir para produção

  1. Troque pk_test_/sk_test_ por pk_live_/sk_live_ só nas variáveis do servidor de produção.
  2. Cadastre a URL de produção do webhook e guarde o segredo. Criado pela API, o secret aparece uma única vez, na resposta 201.
  3. Mantenha o filtro de livemode ativo no handler.
  4. Registre o request_id de toda resposta com erro.
  5. Se quiser restringir a origem das chamadas, configure a allowlist de IP em Chaves de API. Fora da lista, a resposta é 401, mesmo com a credencial certa.

Prefira o webhook para saber que a cobrança foi paga. Se precisar de um plano B, faça polling de GET /api/v1/charges/{id} até o status virar paid, respeitando os headers de limite. O mesmo fluxo em outras linguagens está em API Pix em PHP e API Pix em Python.

Perguntas frequentes

Preciso de SDK para usar a API Pix em Node.js?

Não. A API da xFlow é REST com JSON, e o fetch nativo do Node.js 18 ou superior resolve. Você monta o header Authorization com Basic base64(pk_live_...:sk_live_...) e envia o corpo em JSON, com valores em centavos.

Por que a assinatura do webhook não bate quando uso Express?

Porque o express.json() transforma o corpo em objeto antes da validação, e a assinatura x-webhook-signature é calculada sobre o corpo cru. Use express.raw({ type: 'application/json' }) na rota do webhook e só faça JSON.parse depois de validar.

Como testar a integração Pix em Node.js sem pagar?

Crie uma chave de teste no painel (pk_test_/sk_test_), gere a cobrança com ela e chame POST /api/v1/test/charges/{id}/pay. A cobrança vira paid e o webhook transaction.paid chega com livemode: false.

O valor da cobrança vai em reais ou em centavos?

Em centavos, como número inteiro. R$ 19,90 vira amountCents: 1990.

Continue lendo

SoluçãoAPI PixA API Pix da xFlow Payments é uma API REST para criar cobranças Pix com QR Code e copia e cola, estornar, sacar e receber webhooks assinados. Uma única integração conecta a sua operação a 11 adquirentes, com roteamento por regras e retentativa automática.BlogWebhook Pix: eventos, assinatura HMAC e boas práticasWebhook Pix é o aviso que a API envia para a sua URL quando algo acontece com uma cobrança ou um saque, como o pagamento confirmado. Na xFlow, cada aviso é um POST JSON assinado com HMAC-SHA256, reenviado até 3 vezes até você responder 2xx.BlogAPI Pix sandbox: como testar sem mover dinheiroO sandbox da API Pix da xFlow funciona com chaves pk_test_ e sk_test_: mesmos endpoints, mesmas respostas com livemode: false, nenhum dinheiro real e um endpoint para simular o pagamento que dispara o webhook transaction.paid.BlogErros da API PixNa API Pix da xFlow, todo erro volta em JSON com error (o código), message e request_id. O status HTTP diz a família do problema: 400 corpo inválido, 401 credencial, 403 conta ou modo, 404 recurso inexistente, 409 idempotência, 422 recusa de negócio, 429 limite e 502 falha no provedor. O request_id localiza a chamada em Integrações > Logs da API.BlogAPI Pix em PythonPara integrar a API Pix em Python, envie POST /api/v1/charges com requests e auth=(pk_live_, sk_live_), mostre o pix.qrCodeBase64 e o pix.copyPaste da resposta e confirme o pagamento pelo webhook transaction.paid, validando o HMAC-SHA256 do corpo cru com hmac.compare_digest. No sandbox, use chaves pk_test_ e simule o pagamento.