Pular para o conteúdo
xFlow Payments

Blog

API Pix no Laravel

Para integrar a API Pix no Laravel, chame POST /api/v1/charges com Http::withBasicAuth (pk_live_ como usuário, sk_live_ como senha), mostre o pix.qrCodeBase64 e o pix.copyPaste no Blade e receba o webhook transaction.paid numa rota fora da verificação de CSRF, validando o HMAC-SHA256 de $request->getContent() com hash_equals. Antes da produção, teste com chaves pk_test_.

Atualizado em · Equipe xFlow Payments · 6 min de leitura

No Laravel, a API Pix se integra com a Http facade, sem pacote extra: uma chamada cria a cobrança, o Blade mostra o QR Code e o copia e cola, e uma rota recebe o webhook que confirma o pagamento. O código abaixo segue a documentação da API, usa Laravel 11 ou superior e a base https://app.xflowpayments.com/api/v1. Se você usa PHP puro, o guia é o de API Pix em PHP. Para a visão geral, veja o 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.
  • As credenciais no .env e expostas em config/services.php:
php
// config/services.php
'xflow' => [
    'public_key' => env('XFLOW_PUBLIC_KEY'),
    'secret_key' => env('XFLOW_SECRET_KEY'),
    'webhook_secret' => env('XFLOW_WEBHOOK_SECRET'),
],

No resto do código, leia com config('services.xflow...'), nunca com env(): depois de php artisan config:cache, o env() fora dos arquivos de configuração retorna null. O segredo não vai para o repositório nem para o front-end.

Um cliente com a Http facade

A autenticação é HTTP Basic, com a chave pública como usuário e o segredo como senha. Valores monetários vão em centavos, como inteiros. Uma classe pequena guarda a configuração:

php
<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

class Xflow
{
    public static function http(): PendingRequest
    {
        return Http::baseUrl('https://app.xflowpayments.com/api/v1')
            ->withBasicAuth(config('services.xflow.public_key'), config('services.xflow.secret_key'))
            ->acceptJson()
            ->timeout(30);
    }
}

Como Xflow::http() devolve um PendingRequest novo a cada chamada, um header que você acrescenta numa cobrança, como a Idempotency-Key, não vaza para a próxima requisição.

Criar a cobrança Pix

php
use App\Services\Xflow;

$pedido = Pedido::findOrFail($pedidoId); // seu model

$cobranca = Xflow::http()
    ->withHeaders(['Idempotency-Key' => "pedido-{$pedido->id}"])
    ->post('/charges', [
        'amountCents' => $pedido->total_centavos, // inteiro: R$ 19,90 = 1990
        'customer' => [
            'name' => $pedido->cliente_nome,
            'email' => $pedido->cliente_email,
            'document' => $pedido->cliente_documento, // CPF ou CNPJ, só dígitos
        ],
        'description' => "Pedido #{$pedido->id}",
        'external_reference' => "pedido-{$pedido->id}",
    ])
    ->throw()
    ->json();

$pedido->update(['xflow_charge_id' => $cobranca['id']]);

O que cada parte resolve:

  1. Idempotency-Key por pedido. Se a requisição cair e você repetir, a API devolve a mesma cobrança em vez de gerar outro Pix. A mesma chave com outro corpo dá 409 idempotency_conflict. Detalhes em idempotência em API de pagamento.
  2. throw() transforma qualquer 4xx ou 5xx em RequestException, tratada mais abaixo.
  3. external_reference (até 120 caracteres) liga a cobrança ao pedido. Ele volta em toda resposta e webhook e filtra a listagem. Valor fora do mínimo ou do máximo da conta retorna 400 validation.

Guarde o id da cobrança. Com ele você consulta o status e, se precisar, pede o estorno Pix por API.

Mostrar o QR Code no Blade

pix.qrCodeBase64 é um PNG em data-URL, pronto para o src:

blade
<img src="{{ $cobranca['pix']['qrCodeBase64'] }}" alt="QR Code Pix" width="240" height="240">
<textarea readonly>{{ $cobranca['pix']['copyPaste'] }}</textarea>

O {{ }} do Blade escapa o HTML e não altera o data-URL. pix.copyPaste é o código BR Code que o pagador cola no app do banco: repasse sem mudar nada. Os dois campos vêm na criação e em GET /api/v1/charges/{id}, mas não na listagem.

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
// routes/web.php
Route::post('/webhooks/xflow', XflowWebhookController::class);

// bootstrap/app.php: a xFlow não envia token de CSRF
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['webhooks/xflow']);
})

No Laravel 10, o mesmo caminho vai no $except de App\Http\Middleware\VerifyCsrfToken. Outra saída é registrar a rota em routes/api.php, que não passa pelo CSRF e ganha o prefixo /api.

php
<?php

namespace App\Http\Controllers;

use App\Jobs\ProcessarPagamentoXflow;
use Illuminate\Http\Request;

class XflowWebhookController
{
    public function __invoke(Request $request)
    {
        $raw = $request->getContent(); // corpo cru, antes de qualquer parse
        $esperado = 'sha256=' . hash_hmac('sha256', $raw, config('services.xflow.webhook_secret'));

        if (! hash_equals($esperado, (string) $request->header('x-webhook-signature'))) {
            abort(401);
        }

        $evento = json_decode($raw, true);

        if ($evento['event'] === 'transaction.paid' && $evento['data']['livemode'] === true) {
            ProcessarPagamentoXflow::dispatch($evento['data']);
        }

        return response()->noContent(); // 204 confirma a entrega
    }
}

Os cuidados:

  • Use $request->getContent(). $request->all() ou $request->json() devolvem o JSON já convertido, e recodificar muda os bytes do HMAC.
  • hash_equals compara em tempo constante. Não use === para assinatura.
  • Responda 2xx. Sem isso, a xFlow reenvia até 3 vezes.
  • Ignore livemode: false, que marca eventos gerados com chave de teste.

Fila opcional

O job deixa a resposta rápida e isola o trabalho pesado:

php
<?php

namespace App\Jobs;

use App\Models\Pedido;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class ProcessarPagamentoXflow implements ShouldQueue
{
    use Queueable;

    public int $tries = 5;

    public function __construct(public array $data) {}

    public function handle(): void
    {
        // Só muda pedido que ainda não está pago: o mesmo evento pode chegar duas vezes.
        Pedido::where('referencia', $this->data['external_reference'])
            ->where('status', '!=', 'pago')
            ->update(['status' => 'pago', 'end_to_end' => $this->data['pix']['endToEnd']]);
    }
}

Ajuste os nomes das colunas ao seu banco. Repare na troca de responsabilidade: depois do 204, a xFlow considera o evento entregue. Se o job falhar, quem tenta de novo é a sua fila, até o $tries. Sem fila, chame o mesmo código direto no controller. Os outros eventos estão em webhook Pix.

Testar no sandbox

Troque o .env para a chave de teste e rode no php artisan tinker:

php
$teste = Xflow::http()->post('/charges', [
    'amountCents' => 1990,
    'customer' => ['name' => 'Cliente', 'email' => 'c@exemplo.com', 'document' => '00000000000'],
])->throw()->json();

Xflow::http()->post("/test/charges/{$teste['id']}/pay")->throw();

$paga = Xflow::http()->get("/charges/{$teste['id']}")->throw()->json();
dump($paga['status'], $paga['livemode']); // "paid", false

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 rodam em teste (403 test_mode_unsupported). Mais em sandbox da API Pix.

Nos testes automatizados, Http::fake() substitui a API, e a rota do webhook se testa com um corpo assinado por você:

php
it('aceita webhook assinado', function () {
    config(['services.xflow.webhook_secret' => 'segredo-de-teste']);
    $corpo = json_encode(['event' => 'transaction.pending', 'data' => ['livemode' => false]]);
    $assinatura = 'sha256=' . hash_hmac('sha256', $corpo, 'segredo-de-teste');

    $this->call('POST', '/webhooks/xflow', [], [], [], [
        'CONTENT_TYPE' => 'application/json',
        'HTTP_X_WEBHOOK_SIGNATURE' => $assinatura,
    ], $corpo)->assertNoContent();
});

Erros e limite de requisições

Com throw(), os erros chegam como RequestException, com a resposta em $e->response:

php
try {
    $cobranca = Xflow::http()
        ->withHeaders(['Idempotency-Key' => "pedido-{$pedido->id}"])
        ->post('/charges', $dados)
        ->throw()
        ->json();
} catch (RequestException $e) {
    Log::warning('xFlow recusou a chamada', [
        'status' => $e->response->status(),
        'erro' => $e->response->json('error'),
        'request_id' => $e->response->json('request_id'),
    ]);
    throw $e;
}

O request_id localiza a chamada em Integrações > Logs da API, que guarda cada requisição autenticada por 30 dias. Os códigos que aparecem primeiro:

  • 401 unauthorized: chave inválida ou revogada, ou IP fora da allowlist.
  • 403 account_inactive: conta em análise ou bloqueada. Siga com a chave de teste.
  • 429 rate_limited: espere os segundos de $e->response->header('Retry-After') e repita com a mesma Idempotency-Key. O limite é por conta, e toda resposta autenticada traz x-ratelimit-remaining.

A lista 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ó no .env de produção e rode php artisan config:cache.
  2. Cadastre a URL de produção do webhook e guarde o segredo do endpoint.
  3. Confirme que a rota do webhook está fora do CSRF e que o worker da fila está rodando.
  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}. Chaves, logs e os outros guias estão na página Desenvolvedores.

Perguntas frequentes

Por que o webhook Pix retorna 419 no Laravel?

Porque a rota está em routes/web.php e passa pela verificação de CSRF, que recusa POST sem token. Exclua o caminho em bootstrap/app.php com validateCsrfTokens(except: ['webhooks/xflow']), no Laravel 11 ou superior, ou registre a rota em routes/api.php.

Preciso de pacote para usar a API Pix no Laravel?

Não. A Http facade, que já vem no Laravel, faz as chamadas com withBasicAuth. A API da xFlow é REST com JSON, valores em centavos e autenticação HTTP Basic com a chave pública e o segredo.

Devo processar o webhook Pix numa fila?

É opcional. Validar a assinatura na hora e mandar o trabalho para um job deixa a resposta rápida. Depois que você responde 2xx, a xFlow não reenvia o evento, então as novas tentativas passam a ser da sua fila, configuradas com $tries.

Como testar a API Pix no Laravel sem pagar?

Com chaves de teste (pk_test_/sk_test_), crie a cobrança e chame POST /api/v1/test/charges/{id}/pay. A cobrança vira paid e o webhook chega com livemode: false. Nos testes automatizados, use Http::fake para não chamar a API.

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