Pular para o conteúdo
xFlow Payments

Blog

API Pix em Java

Para integrar a API Pix em Java, envie POST /api/v1/charges com o java.net.http.HttpClient 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 o HMAC-SHA256 do corpo cru com javax.crypto.Mac e MessageDigest.isEqual. Antes da produção, teste com chaves pk_test_.

Atualizado em · Equipe xFlow Payments · 8 min de leitura

Você integra a API Pix em Java com o java.net.http.HttpClient do próprio JDK: um POST cria a cobrança, a resposta traz o QR Code e o copia e cola, e um endpoint do seu servidor recebe o webhook que confirma o pagamento. O código abaixo segue a documentação da API, roda em Java 17 ou superior e usa a base https://app.xflowpayments.com/api/v1. Para entender a integração antes do código, leia o 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.
  • Java 17 ou superior e uma biblioteca de JSON. O exemplo usa Jackson, que já vem no Spring Boot.
  • As variáveis de ambiente XFLOW_PUBLIC_KEY, XFLOW_SECRET_KEY e XFLOW_WEBHOOK_SECRET.

O segredo fica só no servidor: nunca no repositório, no navegador ou num app instalado no celular do cliente.

Um cliente com HttpClient

A autenticação é HTTP Basic: usuário é a chave pública, senha é o segredo. Valores monetários vão em centavos, como inteiros. Esta classe concentra autenticação, JSON e erros:

java
// XflowClient.java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;

public class XflowClient {
    private static final String BASE = "https://app.xflowpayments.com/api/v1";
    private static final ObjectMapper JSON = new ObjectMapper();

    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    private final String auth;

    public XflowClient(String chavePublica, String segredo) {
        String cred = chavePublica + ":" + segredo;
        auth = "Basic " + Base64.getEncoder().encodeToString(cred.getBytes(StandardCharsets.UTF_8));
    }

    public JsonNode chamar(String metodo, String caminho, Object corpo, String idempotencyKey)
            throws IOException, InterruptedException {
        var req = HttpRequest.newBuilder(URI.create(BASE + caminho))
                .timeout(Duration.ofSeconds(30))
                .header("Authorization", auth);
        if (idempotencyKey != null) req.header("Idempotency-Key", idempotencyKey);
        if (corpo != null) {
            req.header("Content-Type", "application/json")
               .method(metodo, HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(corpo)));
        } else {
            req.method(metodo, HttpRequest.BodyPublishers.noBody());
        }

        HttpResponse<String> resp = http.send(req.build(), HttpResponse.BodyHandlers.ofString());
        JsonNode json = resp.body().isBlank() ? JSON.createObjectNode() : JSON.readTree(resp.body());
        if (resp.statusCode() >= 400) {
            long retryAfter = resp.headers().firstValueAsLong("retry-after").orElse(0);
            throw new XflowException(resp.statusCode(), json, retryAfter);
        }
        return json;
    }

    public static class XflowException extends RuntimeException {
        public final int status;
        public final String codigo;
        public final long retryAfter;

        XflowException(int status, JsonNode corpo, long retryAfter) {
            super(status + " " + corpo.path("error").asText() + ": " + corpo.path("message").asText()
                    + " (" + corpo.path("request_id").asText() + ")");
            this.status = status;
            this.codigo = corpo.path("error").asText();
            this.retryAfter = retryAfter;
        }
    }
}

Crie um XflowClient só e reutilize: o HttpClient é imutável e pode ser compartilhado entre threads. Toda resposta traz um request_id (req_...), no corpo e no header x-request-id, e a mensagem da exceção já o inclui. Grave no log: 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 Pix

java
var xflow = new XflowClient(System.getenv("XFLOW_PUBLIC_KEY"), System.getenv("XFLOW_SECRET_KEY"));
String pedidoId = "1234";

JsonNode cobranca = xflow.chamar("POST", "/charges", Map.of(
        "amountCents", 1990, // R$ 19,90
        "customer", Map.of(
                "name", "Cliente Exemplo",
                "email", "cliente@exemplo.com",
                "document", "00000000000"), // CPF ou CNPJ, só dígitos
        "description", "Pedido #" + pedidoId,
        "external_reference", "pedido-" + pedidoId
), "pedido-" + pedidoId);

String id = cobranca.path("id").asText(); // "clx...", status "pending"
String qrCode = cobranca.path("pix").path("qrCodeBase64").asText();
String copiaECola = cobranca.path("pix").path("copyPaste").asText();

Três cuidados:

  1. Idempotency-Key única por pedido. Se a rede cair e você repetir a chamada com a mesma chave, a API devolve a mesma cobrança em vez de gerar outro Pix. A mesma chave com outro corpo retorna 409 idempotency_conflict. O tema completo está em idempotência em API de pagamento.
  2. Centavos como inteiro. Se o seu sistema guarda o total em BigDecimal de reais, converta com total.movePointRight(2).intValueExact(). O método lança exceção se sobrar fração de centavo, em vez de arredondar escondido.
  3. external_reference (até 120 caracteres) é o id do pedido no seu sistema. Ele volta em toda resposta e webhook e filtra a listagem em GET /api/v1/charges?external_reference=pedido-1234. Valor fora do mínimo ou do máximo da conta retorna 400 validation.

Mostrar o QR Code e o copia e cola

pix.qrCodeBase64 é um PNG em data-URL, pronto para o src de uma imagem. Num template Thymeleaf:

html
<img th:src="${qrCode}" alt="QR Code Pix" width="240" height="240">
<textarea readonly th:text="${copiaECola}"></textarea>

Se precisar do arquivo, para gravar em disco ou mandar a um app, decodifique a parte depois da vírgula com Base64.getDecoder().decode(qrCode.substring(qrCode.indexOf(',') + 1)). pix.copyPaste é o código BR Code que o pagador cola no app do banco: repasse sem alterar. Os dois campos vêm na criação e em GET /api/v1/charges/{id}, mas não na listagem. Mais em Pix copia e cola por API.

Receber o webhook com Spring Boot

Cadastre a URL em Integrações > Webhooks ou por POST /api/v1/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 calculado com o segredo do endpoint.

java
// XflowWebhookController.java (imports de java.security, javax.crypto e Spring omitidos)
@RestController
public class XflowWebhookController {
    private final ObjectMapper json = new ObjectMapper();
    private final PedidoService pedidos; // sua camada de negócio
    private final byte[] segredo;

    public XflowWebhookController(PedidoService pedidos, @Value("${XFLOW_WEBHOOK_SECRET}") String segredo) {
        this.pedidos = pedidos;
        this.segredo = segredo.getBytes(StandardCharsets.UTF_8);
    }

    @PostMapping("/webhooks/xflow")
    public ResponseEntity<Void> receber(
            @RequestBody byte[] corpo,
            @RequestHeader(value = "x-webhook-signature", required = false) String assinatura)
            throws Exception {
        if (!assinaturaValida(corpo, assinatura)) {
            return ResponseEntity.status(401).build();
        }
        JsonNode evento = json.readTree(corpo);
        JsonNode data = evento.path("data");
        if ("transaction.paid".equals(evento.path("event").asText()) && data.path("livemode").asBoolean()) {
            pedidos.darBaixa(data.path("external_reference").asText(), data.path("pix").path("endToEnd").asText());
        }
        return ResponseEntity.ok().build(); // exceção acima vira 500 e a entrega é reenviada
    }

    boolean assinaturaValida(byte[] corpo, String assinatura) throws GeneralSecurityException {
        if (assinatura == null) return false;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(segredo, "HmacSHA256"));
        String esperado = "sha256=" + HexFormat.of().formatHex(mac.doFinal(corpo));
        return MessageDigest.isEqual(
                esperado.getBytes(StandardCharsets.UTF_8),
                assinatura.getBytes(StandardCharsets.UTF_8));
    }
}

Por que cada parte está assim:

  • @RequestBody byte[], e não um DTO. Se o Spring converter o JSON em objeto, você perde os bytes originais e o HMAC deixa de bater.
  • MessageDigest.isEqual compara em tempo constante: o tempo depende só do tamanho, não do conteúdo. Não use String.equals para assinatura.
  • Responda 2xx para confirmar. Sem isso, a xFlow reenvia até 3 vezes. Por isso darBaixa precisa ignorar pedido que já está pago.
  • Filtre livemode. Eventos gerados com chave de teste trazem livemode: false e não podem dar baixa em pedido real.

Se o projeto usa Spring Security, libere a rota e desligue o CSRF só para ela, com csrf.ignoringRequestMatchers("/webhooks/xflow"). Sem Spring, o HttpServer do JDK (com.sun.net.httpserver) resolve: leia exchange.getRequestBody().readAllBytes() e passe os bytes para o mesmo assinaturaValida. Os outros eventos, como estorno e saque, estão no guia de webhook Pix.

Testar no sandbox

Troque as variáveis para a chave de teste. 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. Para simular o pagamento:

java
JsonNode teste = xflow.chamar("POST", "/charges", Map.of(
        "amountCents", 1990,
        "customer", Map.of("name", "Cliente", "email", "c@exemplo.com", "document", "00000000000")
), null);
String testeId = teste.path("id").asText();

xflow.chamar("POST", "/test/charges/" + testeId + "/pay", null, null);

JsonNode paga = xflow.chamar("GET", "/charges/" + testeId, null, null);
System.out.println(paga.path("status").asText() + " " + paga.path("livemode").asBoolean()); // paid false

A cobrança vira paid, a taxa é calculada com as taxas reais da sua conta e o webhook transaction.paid chega com livemode: false e payer: null. Com chave de produção, a simulação responde 403 test_mode_only. Saques não rodam em teste (403 test_mode_unsupported). O roteiro completo está em sandbox da API Pix.

Erros e limite de requisições

HTTP Código O que fazer
401 unauthorized Confira as chaves e, se usa allowlist de IP, o IP do servidor.
400 validation Corrija o corpo. O campo details aponta o problema.
403 account_inactive Conta em análise ou bloqueada. Siga com a chave de teste.
429 rate_limited Espere os segundos do retry-after e repita com backoff.
502 charge_failed Falha ao gerar a cobrança no provedor. Repita com backoff.

O limite é por conta, e toda resposta autenticada traz x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset. Uma retentativa simples para o 429:

java
static JsonNode comRetentativa(Callable<JsonNode> chamada) throws Exception {
    for (int tentativa = 1; ; tentativa++) {
        try {
            return chamada.call();
        } catch (XflowClient.XflowException e) {
            if (e.status != 429 || tentativa == 3) throw e;
            long segundos = e.retryAfter > 0 ? e.retryAfter : 1L << tentativa;
            Thread.sleep(segundos * 1000);
        }
    }
}

Numa IOException ao criar a cobrança, repita com a mesma Idempotency-Key: se a primeira chamada chegou a criar o Pix, você recebe a mesma cobrança. A tabela completa está em erros da API Pix.

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 e a baixa idempotente.
  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.

Prefira o webhook ao polling de GET /api/v1/charges/{id}. O mesmo fluxo em outras linguagens está em API Pix em C# e API Pix em Go, e a página Desenvolvedores reúne chaves, logs e todos os guias.

Perguntas frequentes

Preciso de SDK para usar a API Pix em Java?

Não. A API da xFlow é REST com JSON e autenticação HTTP Basic, e o java.net.http.HttpClient, que vem no JDK desde o Java 11, faz as chamadas. Para o JSON, qualquer biblioteca serve. O exemplo usa Jackson, que já vem no Spring Boot.

Por que a assinatura do webhook não confere no Spring Boot?

Quase sempre porque o corpo foi convertido em objeto antes da validação. O HMAC-SHA256 é calculado sobre os bytes exatos recebidos. Receba o corpo como @RequestBody byte[], valide e só depois converta com o ObjectMapper. Gere o hexadecimal em minúsculas, como faz o HexFormat.of().

Qual versão do Java o exemplo exige?

Java 17 ou superior. O HttpClient existe desde o Java 11, e o HexFormat, que converte o HMAC em hexadecimal, chegou no Java 17. Em versões anteriores, faça essa conversão byte a byte com String.format("%02x", b).

Como testar a integração Pix em Java sem pagar?

Crie uma chave de teste (pk_test_/sk_test_) no painel, 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.

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.BlogIdempotência em API de pagamento: retry sem duplicarIdempotência é a garantia de que repetir a mesma requisição não repete o efeito. Na API da xFlow, você envia o header Idempotency-Key ao criar cobranças e saques: num retry com a mesma chave, a API devolve a mesma cobrança ou o mesmo saque, sem gerar outro Pix nem debitar o saldo de novo.BlogAPI Pix em C#Para integrar a API Pix em C#, registre um HttpClient tipado com Basic auth (pk_live_ como usuário, sk_live_ como senha), envie POST /api/v1/charges, mostre o pix.qrCodeBase64 e o pix.copyPaste da resposta e confirme o pagamento pelo webhook transaction.paid, validando o HMACSHA256 do corpo cru com CryptographicOperations.FixedTimeEquals. Antes da produção, teste com chaves pk_test_.