Pular para o conteúdo
xFlow Payments

Blog

API Pix em PHP

Para integrar a API Pix em PHP, envie POST /api/v1/charges com cURL usando Basic auth (CURLOPT_USERPWD com pk_live_:sk_live_), exiba 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 hash_hmac e hash_equals. No sandbox, use chaves pk_test_ e simule o pagamento.

Atualizado em · Equipe xFlow Payments · 7 min de leitura

Para usar a API Pix em PHP, você precisa só da extensão cURL: uma chamada cria a cobrança, a resposta traz o QR Code e o copia e cola, e um script recebe o webhook que confirma o pagamento. O código abaixo segue a documentação da API, roda em PHP 8 e usa a base https://app.xflowpayments.com/api/v1. Para a visão geral da integração, veja o guia da API Pix.

Antes de começar

  1. Gere uma chave 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.
  2. Para testar, crie também uma chave do ambiente Teste (pk_test_... e sk_test_...). Dá para criar antes da aprovação do cadastro.
  3. Guarde as credenciais em variáveis de ambiente: XFLOW_PUBLIC_KEY, XFLOW_SECRET_KEY e XFLOW_WEBHOOK_SECRET.

O segredo nunca vai para o navegador nem para o repositório. Se vazar, revogue no painel: a revogação é imediata.

Uma função para chamar a API

A autenticação é HTTP Basic, com a chave pública como usuário e o segredo como senha. O cURL monta o header sozinho com CURLOPT_USERPWD. Os valores vão em centavos, como inteiros.

php
<?php
// xflow.php
const XFLOW_BASE = 'https://app.xflowpayments.com/api/v1';

function xflow(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array
{
    $headers = ['Accept: application/json'];
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
    }
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }

    $ch = curl_init(XFLOW_BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPAUTH       => CURLAUTH_BASIC,
        CURLOPT_USERPWD        => getenv('XFLOW_PUBLIC_KEY') . ':' . getenv('XFLOW_SECRET_KEY'),
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }

    $raw = curl_exec($ch);
    if ($raw === false) {
        throw new RuntimeException('Falha de rede: ' . curl_error($ch));
    }
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $json = json_decode($raw, true) ?? [];

    if ($status >= 400) {
        throw new RuntimeException(sprintf(
            '%d %s: %s (%s)',
            $status,
            $json['error'] ?? 'erro',
            $json['message'] ?? '',
            $json['request_id'] ?? 'sem request_id'
        ), $status);
    }
    return $json;
}

Toda resposta traz um request_id (req_...), no corpo e no header x-request-id. Grave esse valor no log quando der erro. Ele localiza a chamada em Integrações > Logs da API, que guarda status, duração e código de erro de cada requisição autenticada por 30 dias. Os códigos possíveis estão em erros da API Pix.

Criar a cobrança Pix

php
<?php
require __DIR__ . '/xflow.php';

$pedidoId = '1234';

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

echo $cobranca['id'] . ' ' . $cobranca['status']; // clx... pending

O último argumento é a Idempotency-Key. Use uma por pedido: se a requisição cair e você repetir, a API devolve a mesma cobrança em vez de criar outro Pix. Reusar a chave com outro corpo dá 409 idempotency_conflict. O tema completo está em idempotência em API de pagamento.

Grave o id da cobrança junto do pedido. É com ele que você consulta o status em GET /api/v1/charges/{id} e, se precisar devolver o dinheiro, pede o estorno em POST /api/v1/charges/{id}/refund, como mostra o guia de estorno Pix por API.

O external_reference (até 120 caracteres) é o id do pedido no seu sistema. Ele volta em toda resposta e webhook da cobrança e pode ser usado como filtro na listagem. Se o valor estiver fora do mínimo ou do máximo da sua conta, a resposta é 400 validation.

Exibir o QR Code e o copia e cola

php
<img src="<?= htmlspecialchars($cobranca['pix']['qrCodeBase64']) ?>" alt="QR Code Pix" width="240" height="240">
<textarea readonly><?= htmlspecialchars($cobranca['pix']['copyPaste']) ?></textarea>

pix.qrCodeBase64 é um PNG em data-URL, pronto para o src da imagem. pix.copyPaste é o payload BR Code que o pagador cola no app do banco. Não altere esse texto. Os dois campos vêm na criação e em GET /api/v1/charges/{id}, mas não na listagem. Veja também Pix copia e cola por API.

Receber o webhook

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.

php
<?php
// webhook.php
$raw = file_get_contents('php://input');
$assinatura = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$esperado = 'sha256=' . hash_hmac('sha256', $raw, getenv('XFLOW_WEBHOOK_SECRET'));

if (!hash_equals($esperado, $assinatura)) {
    http_response_code(401);
    exit;
}

$evento = json_decode($raw, true);
$data = $evento['data'];

try {
    if ($evento['event'] === 'transaction.paid' && $data['livemode'] === true) {
        darBaixaNoPedido($data['external_reference'], $data['pix']['endToEnd']); // sua função
    }
    http_response_code(200);
} catch (Throwable $e) {
    http_response_code(500); // sem 2xx, a entrega é reenviada
}

O que esse script faz e por quê:

  • Valida antes de decodificar. A assinatura é calculada sobre o texto exato recebido. Se você decodificar e codificar o JSON de novo, o HMAC muda. No Laravel, leia o corpo com $request->getContent().
  • Compara com hash_equals, que leva o mesmo tempo para qualquer entrada.
  • Responde 2xx para confirmar. Sem isso, a xFlow reenvia até 3 vezes. Por isso darBaixaNoPedido precisa ignorar pedido que já está pago.
  • Ignora livemode: false, que marca eventos gerados com chave de teste.

No data do webhook, o valor vem em amount, e transaction.paid traz também net e fee. payer e pix.endToEnd vêm do PSP na confirmação e servem para conciliar com o extrato. Os outros eventos estão em webhook Pix.

Testar no sandbox

Com a chave de teste nas variáveis, o código é o mesmo. As respostas trazem livemode: false, nenhum provedor Pix é chamado e o pix.copyPaste é um marcador que não pode ser pago. Para simular o pagamento:

php
<?php
require __DIR__ . '/xflow.php';

$teste = xflow('POST', '/charges', [
    'amountCents' => 1990,
    'customer' => ['name' => 'Cliente', 'email' => 'c@exemplo.com', 'document' => '00000000000'],
]);

xflow('POST', "/test/charges/{$teste['id']}/pay");

$paga = xflow('GET', "/charges/{$teste['id']}");
echo $paga['status']; // paid

A cobrança vira paid, a taxa sai das taxas reais da sua conta e o webhook transaction.paid chega com livemode: false. Com chave de produção, a simulação responde 403 test_mode_only. Saques não funcionam em teste e retornam 403 test_mode_unsupported. Mais detalhes em sandbox da API Pix.

Conciliar as cobranças pagas

Para fechar o dia, liste as cobranças pagas de um período. A listagem usa paginação por cursor: limit vai de 1 a 100 e, enquanto has_more for true, você repete a chamada com starting_after igual ao id do último item.

php
<?php
require __DIR__ . '/xflow.php';

function cobrancasPagas(string $de, string $ate): Generator
{
    $cursor = null;
    do {
        $query = http_build_query(array_filter([
            'status' => 'paid', 'from' => $de, 'to' => $ate, 'limit' => 100, 'starting_after' => $cursor,
        ]));
        $pagina = xflow('GET', '/charges?' . $query);
        yield from $pagina['data'];
        $cursor = end($pagina['data'])['id'] ?? null;
    } while ($pagina['has_more'] && $cursor !== null);
}

foreach (cobrancasPagas('2026-10-01', '2026-10-31') as $c) {
    echo $c['external_reference'] . ' ' . $c['amountCents'] . PHP_EOL;
}

Cada item traz payer e pix.endToEnd, os campos que a documentação indica para bater o recebimento com o extrato bancário.

Antes de ir para produção

  1. Troque as chaves de teste pelas de produção (pk_live_/sk_live_) só no servidor de produção.
  2. Cadastre a URL de produção do webhook e guarde o segredo do endpoint.
  3. Mantenha o filtro que ignora livemode: false.
  4. Grave o request_id de toda resposta com erro.
  5. Se quiser, ative a allowlist de IP em Chaves de API. Requisições de fora da lista recebem 401.

Erros que aparecem primeiro

HTTP Código O que fazer
401 unauthorized Confira as chaves. Se usa allowlist de IP, confira se o IP do servidor está na lista.
400 validation Corrija o corpo. O campo details aponta o problema.
403 account_inactive A conta está em análise ou bloqueada. Use a chave de teste enquanto isso.
409 idempotency_conflict Use uma Idempotency-Key nova para um pedido novo.
429 rate_limited Espere os segundos do header retry-after e tente de novo com backoff.

Prefira o webhook para saber que a cobrança foi paga. O polling de GET /api/v1/charges/{id} até o status virar paid fica como alternativa. O mesmo fluxo em outras linguagens está em API Pix em Node.js e API Pix em Python.

Perguntas frequentes

Preciso de biblioteca para usar a API Pix em PHP?

Não. A extensão cURL do PHP basta. A API da xFlow é REST com JSON e autenticação HTTP Basic: usuário é a chave pública pk_live_ e senha é o segredo sk_live_.

Como ler o corpo cru do webhook em PHP?

Com file_get_contents('php://input'). Calcule o HMAC-SHA256 desse texto com o segredo do endpoint, prefixe com sha256= e compare com o header x-webhook-signature usando hash_equals. No Laravel, o corpo cru vem de $request->getContent().

Como testar a API Pix em PHP sem pagar de verdade?

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

Por que recebo 409 idempotency_conflict?

Porque a Idempotency-Key enviada já foi usada com um corpo diferente. A mesma chave só serve para repetir o mesmo pedido. Para uma cobrança nova, gere uma chave nova.

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