Pular para o conteúdo
xFlow Payments

Blog

API Pix no Next.js

Para integrar a API Pix no Next.js, crie a cobrança no servidor, numa Server Action ou num Route Handler, com POST /api/v1/charges e Basic auth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste no componente e confirme o pagamento em app/api/webhook/route.ts, validando o HMAC-SHA256 de await request.text(). O segredo nunca vai para o navegador.

Atualizado em · Equipe xFlow Payments · 7 min de leitura

No Next.js com App Router, a API Pix se integra em três peças: uma função só do servidor que fala com a API, uma Server Action que cria a cobrança e devolve o QR Code ao componente, e um Route Handler que recebe o webhook de pagamento. Tudo com o fetch nativo, sem pacote para a API. O código segue a documentação da API e usa a base https://app.xflowpayments.com/api/v1. A base de Node.js está em API Pix em Node.js, e a visão geral, no guia da API Pix.

O que você precisa

  • Uma chave 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_...), que pode ser criada antes da aprovação do cadastro.
  • XFLOW_PUBLIC_KEY, XFLOW_SECRET_KEY e XFLOW_WEBHOOK_SECRET no .env.local ou nas variáveis do servidor.
  • O pacote server-only (npm install server-only).

Importante: nunca use o prefixo NEXT_PUBLIC_ nessas variáveis. O Next.js embute variáveis com esse prefixo no JavaScript enviado ao navegador, e o segredo ficaria público.

Um cliente que só roda no servidor

A autenticação é HTTP Basic: usuário é a chave pública, senha é o segredo. Valores vão em centavos, como inteiros. O import "server-only" faz o build falhar se algum componente do navegador importar este arquivo:

ts
// lib/xflow.ts
import "server-only";

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 class XflowErro extends Error {
  status: number;
  code: string;
  retryAfter: number | null;
  constructor(status: number, corpo: { error?: string; message?: string; request_id?: string }, retryAfter: number | null) {
    super(`${status} ${corpo.error}: ${corpo.message} (${corpo.request_id})`);
    this.status = status;
    this.code = corpo.error ?? "";
    this.retryAfter = retryAfter;
  }
}

type Opcoes = { method?: "GET" | "POST"; body?: unknown; idempotencyKey?: string };

export async function xflow<T>(path: string, { method = "GET", body, idempotencyKey }: Opcoes = {}): Promise<T> {
  const headers: Record<string, string> = { Authorization: AUTH };
  if (body !== undefined) headers["Content-Type"] = "application/json";
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

  const res = await fetch(BASE + path, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const texto = await res.text();
  const json = texto ? JSON.parse(texto) : {};
  if (!res.ok) {
    throw new XflowErro(res.status, json, Number(res.headers.get("retry-after")) || null);
  }
  return json as T;
}

Toda resposta traz um request_id (req_...), e a mensagem do XflowErro já o inclui. Com ele você acha a chamada em Integrações > Logs da API, que guarda cada requisição autenticada por 30 dias.

Criar a cobrança com uma Server Action

ts
// app/pedidos/[id]/actions.ts
"use server";

import { xflow } from "@/lib/xflow";
import { buscarPedido } from "@/lib/pedidos"; // sua camada de dados

type Cobranca = { id: string; status: string; pix: { copyPaste: string; qrCodeBase64: string } };

export async function gerarPix(pedidoId: string) {
  // Toda Server Action é um endpoint público: valide e confira se o pedido é do usuário logado.
  const pedido = await buscarPedido(pedidoId);
  if (!pedido) throw new Error("Pedido não encontrado");

  const cobranca = await xflow<Cobranca>("/charges", {
    method: "POST",
    idempotencyKey: `pedido-${pedido.id}`,
    body: {
      amountCents: pedido.totalCents, // o valor vem do banco, nunca do navegador
      customer: { name: pedido.nome, email: pedido.email, document: pedido.documento },
      description: `Pedido #${pedido.id}`,
      external_reference: `pedido-${pedido.id}`,
    },
  });

  return { qrCode: cobranca.pix.qrCodeBase64, copiaECola: cobranca.pix.copyPaste };
}

Três decisões importam aqui:

  1. A action recebe só o id do pedido. Qualquer pessoa pode enviar o mesmo POST que o seu formulário envia, então valor, cliente e dono do pedido saem do banco.
  2. Idempotency-Key por pedido. Um clique duplo ou uma nova tentativa devolve a mesma cobrança em vez de gerar outro Pix. A mesma chave com outro corpo dá 409 idempotency_conflict. Mais em idempotência em API de pagamento.
  3. O retorno é enxuto. O que a action devolve vai para o navegador, então mande só o que a tela usa.

Se quem cria a cobrança é um app ou outro sistema, use o mesmo xflow() dentro de um Route Handler com export async function POST. As regras são as mesmas.

Mostrar o QR Code e o copia e cola

pix.qrCodeBase64 é um PNG em data-URL, então um <img> comum basta:

tsx
// app/pedidos/[id]/pagar-com-pix.tsx
"use client";

import { useState } from "react";
import { gerarPix } from "./actions";

type Pix = Awaited<ReturnType<typeof gerarPix>>;

export function PagarComPix({ pedidoId }: { pedidoId: string }) {
  const [pix, setPix] = useState<Pix | null>(null);

  if (!pix) {
    return <button onClick={async () => setPix(await gerarPix(pedidoId))}>Pagar com Pix</button>;
  }
  return (
    <div>
      <img src={pix.qrCode} alt="QR Code Pix" width={240} height={240} />
      <textarea readOnly value={pix.copiaECola} />
      <button onClick={() => navigator.clipboard.writeText(pix.copiaECola)}>Copiar código Pix</button>
    </div>
  );
}

pix.copyPaste é o código BR Code que o pagador cola no app do banco: repasse sem alterar. O botão de copiar ajuda quem está pagando pelo celular.

Receber o webhook num Route Handler

Cadastre a URL em Integrações > Webhooks. Ela precisa ser https:// e pública. Cada entrega é um POST JSON com os headers x-webhook-event e x-webhook-signature: sha256=..., o HMAC-SHA256 do corpo cru com o segredo do endpoint.

ts
// app/api/webhook/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";
import { darBaixaNoPedido } from "@/lib/pedidos"; // sua camada de dados

function assinaturaValida(corpo: string, header: string | null) {
  const segredo = process.env.XFLOW_WEBHOOK_SECRET ?? "";
  const esperado = Buffer.from("sha256=" + createHmac("sha256", segredo).update(corpo).digest("hex"));
  const recebido = Buffer.from(header ?? "");
  return esperado.length === recebido.length && timingSafeEqual(esperado, recebido);
}

export async function POST(request: Request) {
  const corpo = await request.text(); // corpo cru: não chame request.json() antes de validar
  if (!assinaturaValida(corpo, request.headers.get("x-webhook-signature"))) {
    return new Response(null, { status: 401 });
  }

  const { event, data } = JSON.parse(corpo);
  try {
    if (event === "transaction.paid" && data.livemode) {
      await darBaixaNoPedido(data.external_reference, data.pix.endToEnd);
    }
    return new Response(null, { status: 200 });
  } catch {
    return new Response(null, { status: 500 }); // sem 2xx, a entrega é reenviada
  }
}

O que cada detalhe evita:

  • request.text() em vez de request.json(). O HMAC precisa do texto exato. Converter e serializar de novo muda os bytes.
  • timingSafeEqual compara em tempo constante. Como ele exige buffers do mesmo tamanho, o tamanho é conferido antes.
  • Runtime Node.js. O exemplo usa node:crypto. Esse já é o runtime padrão do Route Handler, então não troque para edge.
  • proxy.ts (o antigo middleware.ts). Se ele exige login, libere /api/webhook. Quem protege a rota é a assinatura.
  • Responda 2xx. Sem isso, a xFlow reenvia até 3 vezes. A baixa precisa ignorar pedido que já está pago, e livemode: false marca evento de teste.

Os outros eventos, como estorno e saque, estão em webhook Pix.

Testar no sandbox

Ponha as chaves de teste no .env.local. Os endpoints e as respostas são os mesmos, com livemode: false. Nenhum provedor Pix é chamado, e o pix.copyPaste de teste é um marcador que não pode ser pago. Gere a cobrança pela sua página e simule o pagamento com o id dela:

bash
curl -X POST https://app.xflowpayments.com/api/v1/test/charges/{id}/pay \
  -u "pk_test_...:sk_test_..."

A cobrança vira paid, a taxa é calculada com as taxas reais da sua conta e o webhook transaction.paid chega com livemode: false. Para recebê-lo no localhost, exponha o servidor com um túnel HTTPS e cadastre essa URL. Com chave de produção, a simulação responde 403 test_mode_only. O roteiro completo está em sandbox da API Pix.

Erros e limite de requisições

Dentro da Server Action, capture o XflowErro, registre a mensagem com o request_id no servidor e devolva ao usuário um texto simples. Os códigos mais comuns:

  • 401 unauthorized: chave inválida ou revogada, ou IP fora da allowlist.
  • 400 validation: corpo inválido ou valor fora do mínimo ou do máximo da conta.
  • 403 account_inactive: conta em análise ou bloqueada. Siga com a chave de teste.
  • 429 rate_limited: espere os segundos de erro.retryAfter e repita com a mesma Idempotency-Key. O limite é por conta, e toda resposta autenticada traz x-ratelimit-remaining.

A tabela completa está em erros da API Pix.

Antes de ir para produção

  1. Troque as chaves de teste por pk_live_/sk_live_ só nas variáveis do ambiente de produção, sem NEXT_PUBLIC_.
  2. Cadastre a URL de produção do webhook e guarde o segredo do endpoint.
  3. Confirme que o proxy.ts não bloqueia /api/webhook.
  4. Mantenha o filtro de livemode e a baixa idempotente.
  5. Se quiser, ative a allowlist de IP em Chaves de API. Fora da lista, a resposta é 401.

Prefira o webhook ao polling de GET /api/v1/charges/{id}. Arquivos prontos em Node.js, PHP e Python estão em exemplos de código da API Pix, e a página Desenvolvedores reúne chaves, logs e guias.

Perguntas frequentes

Posso chamar a API Pix direto de um componente no navegador?

Não. A chamada leva o segredo sk_live_, que não pode chegar ao navegador. Crie a cobrança numa Server Action ou num Route Handler e devolva ao componente só o QR Code e o copia e cola. E não use o prefixo NEXT_PUBLIC_ nas variáveis da xFlow.

Por que a assinatura do webhook não confere no Next.js?

Porque o corpo foi lido com request.json() e serializado de novo. O HMAC-SHA256 do header x-webhook-signature é calculado sobre o corpo cru. Leia com await request.text(), valide e só depois faça JSON.parse.

Server Action ou Route Handler para criar a cobrança Pix?

Server Action é o caminho mais curto quando a própria página do Next.js gera o Pix. Route Handler serve quando outro cliente, como um app de celular, chama o seu backend. Nos dois casos o valor vem do banco de dados, nunca do navegador.

Como testar o webhook Pix do Next.js rodando em localhost?

A URL do webhook precisa ser HTTPS e pública. Exponha o servidor local com um túnel HTTPS, cadastre essa URL usando a chave de teste e simule o pagamento com POST /api/v1/test/charges/{id}/pay. O evento chega com livemode: false.

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.BlogAPI Pix em Node.jsPara 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.FerramentaExemplos de código da API PixArquivos prontos para integrar a API Pix da xFlow em Node.js, PHP e Python: criar a cobrança, mostrar o QR Code e o copia e cola, receber o webhook assinado, testar no sandbox e conciliar as cobranças pagas. Licença MIT.