Pular para o conteúdo principal

Como receber webhooks em sua aplicação Laravel

Webhooks​

Webhooks permitem que uma aplicação ou serviço envie automaticamente informações ou dados para outra aplicação ou serviço sempre que um evento específico ocorre.

A API da OpenPix, por exemplo, pode enviar um webhook para a sua aplicação sempre que uma cobrança for paga ou cancelada, possibilitando que sua aplicação seja notificada em tempo real sobre essas mudanças de status.

Como receber webhooks​

Vamos seguir os seguintes passos:

Criar a rota​

Para receber os webhooks, é fundamental estabelecer um endpoint dedicado ao processamento de todas as notificações.

No contexto do Laravel, você pode criar uma nova rota, como por exemplo /openpix/webhook, no arquivo routes/web.php:

use App\Http\Controllers\OpenPix\WebhookController;

Route::post('/openpix/webhook', [WebhookController::class, 'receive']);

A seguir, você pode configurar o controller responsável pelo tratamento desses webhooks:

<?php

// ./app/Http/Controllers/OpenPix/WebhookController.php

namespace App\Http\Controllers\OpenPix;

use App\Models\Donation;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use OpenPix\PhpSdk\Client;

class WebhookController extends Controller
{
const SIGNATURE_HEADER = "x-webhook-signature";

const OPENPIX_CHARGE_COMPLETED_EVENT = "OPENPIX:CHARGE_COMPLETED";

public function __construct(private Client $openpix)
{
}

/**
* Recebe todos os webhooks enviados pela plataforma.
*/
public function receive(Request $request)
{
// ...
}
}

Como desabilitar a verificação CSRF​

O CSRF no Laravel é uma medida de segurança que protege contra ataques nos quais um invasor tenta forçar um usuário autenticado a executar ações não autorizadas.

Todas as requisições POST devem conter um token CSRF para garantir que a ação tenha sido autorizada, inclusive para o endpoint de webhook.

Para evitar erros de CSRF, é necessário desabilitar essa verificação, adicionando o caminho da rota dos webhooks, como /openpix/webhook, no array $except do arquivo ./app/Http/Middleware/VerifyCsrfToken.php. Por exemplo:

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
"/openpix/webhook", // Coloque aqui o caminho para a sua rota
];
}

Não se preocupe, pois isso será substituído pela verificação da assinatura dos webhooks.

Validando a assinatura​

Adicione esse método em seu controller:

/**
* Permite requisições somente a partir da OpenPix.
*
* Caso ocorra algum erro, irá retornar uma `Response`
* que indica um erro na validação. Ocorrendo nenhum erro,
* irá retornar apenas `null`.
*/
private function allowRequestOnlyFromOpenPix(Request $request)
{
$rawPayload = $request->getContent();

// self::SIGNATURE_HEADER = "x-webhook-signature"
$signature = $request->header(self::SIGNATURE_HEADER);

$isWebhookValid = ! empty($rawPayload)
&& ! empty($signature)
&& $this->openpix->webhooks()->isWebhookValid($rawPayload, $signature);

if ($isWebhookValid) return null;

return response()->json([
"errors" => [
[
"message" => "Invalid webhook signature."
],
],
], 400);
}

Chamando esse método irá garantir que somente a OpenPix tenha webhooks processados pela sua aplicação através da validação da assinatura de webhooks utilizando o SDK de PHP.

Veja um exemplo de chamada desse método no método receive:

/**
* Recebe todos os webhooks enviados pela plataforma.
*/
public function receive(Request $request)
{
// Se retornar uma resposta, então devolvemos para
// o cliente da requisição, que nesse caso irá ser um erro.
if ($response = $this->allowRequestOnlyFromOpenPix($request)) return $response;

// Continuamos com o processamento...
return $this->handleWebhook($request);
}

Recebendo os dados de um webhook​

É possível receber os parâmetros da requisição de webhook utilizando o método input de uma Request do Laravel. Veja um exemplo:

// Obtém o parâmetro `event` da requisição.
$event = $request->input("event");

Validando os eventos recebidos​

Durante o processamento dos webhooks, é possível validar seus tipos e encaminhá-los para métodos mais especializados, de acordo com o tipo de webhook, por exemplo.

Cada requisição irá trazer consigo um parâmetro event contendo o tipo do webhook. Veja algumas possibilidades de valores:

  • OPENPIX:CHARGE_CREATED - Nova cobrança criada.
  • OPENPIX:CHARGE_COMPLETED - Cobrança concluída é quando uma cobrança é totalmente paga.
  • OPENPIX:CHARGE_EXPIRED - Cobrança expirada é quando uma cobrança não foi totalmente paga e expirou.
  • OPENPIX:TRANSACTION_RECEIVED - Nova transação PIX recebida.
  • OPENPIX:TRANSACTION_REFUND_RECEIVED - Novo reembolso de transação PIX recebido ou reembolsado.
  • OPENPIX:MOVEMENT_CONFIRMED - Pagamento confirmado é quando a transação do pix referente ao pagamento é confirmada.
  • OPENPIX:MOVEMENT_FAILED - Falha no pagamento é quando o pagamento é aprovado e ocorre um erro.
  • OPENPIX:MOVEMENT_REMOVED - O pagamento foi removido por um usuário.

Assumindo que você tenha um método handleWebhook que recebe todos os webhooks com a assinatura validada, veja o exemplo:

/**
* Dispara o método apropriado de acordo com o tipo
* validado do webhook.
*/
private function handleWebhook(Request $request)
{
// Valida se o webhook é de quando uma cobrança foi paga
// utilizando o método `isChargePaidPayload`.
if ($this->isChargePaidPayload($request)) {
// Dispara o método `handleChargePaidWebhook`
// quando uma cobrança for paga.
return $this->handleChargePaidWebhook($request);
}

// Valida se o webhook é de teste utilizando
// o método `isTestPayload`.
if ($this->isTestPayload($request)) {
// Dispara o método `handleTestWebhook` se for um webhook de teste.
return $this->handleTestWebhook();
}

return response()->json([
"errors" => [
[
"message" => "Invalid webhook type.",
],
]
], 400);
}

Utilizamos o parâmetro event para diferenciar diante os outros tipos de webhooks:

/**
* Verifica se é o webhook de quando uma cobrança foi paga.
*/
private function isChargePaidPayload(Request $request)
{
$event = $request->input("event");

$allowedEvents = [
// Indicam quais eventos são considerados como
// uma cobrança sendo paga.

// "OPENPIX:CHARGE_COMPLETED"
self::OPENPIX_CHARGE_COMPLETED_EVENT,

// "OPENPIX:TRANSACTION_RECEIVED"
self::OPENPIX_TRANSACTION_RECEIVED_EVENT,
];

$isChargePaidEvent = ! empty($event) && in_array($event, $allowedEvents);

return $isChargePaidEvent
&& ! empty($request->input("charge.correlationID"));
}

Webhook de teste​

Ao configurar uma nova integração via webhooks na plataforma, ela enviará um webhook de teste para verificar se tudo está correto com sua aplicação.

Esse webhook de teste será enviado com o campo event contendo o valor especificado pelo evento selecionado na plataforma. Por exemplo, se você selecionar o evento Cobrança paga, o campo event será OPENPIX:CHARGE_COMPLETED.

Veja como é possível verificar se um webhook é de teste:

/**
* Verifica se o webhook é do tipo de teste.
*
* A plataforma da OpenPix envia um webhook de teste
* para validar se o receptor de webhooks está funcionando.
*/
private function isTestPayload(Request $request)
{
$event = $request->input("event");

return ! empty($event) && $event === self::OPENPIX_CHARGE_COMPLETED_EVENT;
}

Veja um exemplo de processamento desse webhook:

/**
* Processa o webhook de teste enviado pela plataforma.
*/
private function handleTestWebhook()
{
return response()->json(["message" => "Success."]);
}

Processando um webhook​

Neste exemplo abaixo, você pode ver como receber uma notificação de que uma cobrança foi paga na OpenPix e atualizar o status de uma doação para 'paga'.

/**
* Ativado quando uma cobrança foi paga.
*/
public function handleChargePaidWebhook(Request $request)
{
// Obtemos o correlationID do webhook utilizando o método `input`
// de uma `$request`.
// Um identificador único que relaciona uma cobrança Pix com uma doação.
$correlationID = $request->input("charge.correlationID");

// Encontra a doação a partir do correlationID da cobrança.
$donation = Donation::where("correlationID", $correlationID)->first();

// Verifica se essa doação existe.
if (empty($donation)) {
return response()->json([
"errors" => [
[
"message" => "Donation not found.",
],
],
], 404);
}

// Atualiza o status da doação para paga.
$donation->status = "PAID";
$donation->save();

// Retorna uma resposta 200 para a plataforma OpenPix.
return response()->json(["message" => "Success."]);
}

Exemplo de integração com webhooks​

Temos um exemplo completo do controller que recebe webhooks nesse arquivo.

Veja mais exemplos de integração com Laravel em nosso repositório.