Cómo verificar webhooks con las bibliotecas Svix
Verificación usando nuestras bibliotecas oficiales
Verificación utilizando bibliotecas
npm install svix
# Or
yarn add svix
Luego verifica los webhooks usando el código a continuación. La carga útil es el cuerpo (cadena) sin procesar de la solicitud, y los encabezados son los encabezados pasados en la solicitud.
Debes usar el cuerpo de la solicitud sin procesar al verificar los webhooks, ya que la firma criptográfica es sensible incluso a los cambios más mínimos. Debes tener cuidado con los frameworks que analizan la solicitud como JSON y luego la convierten en una cadena porque esto también romperá la verificación de la firma.
Mira los ejemplos a continuación sobre cómo obtener el cuerpo de la solicitud sin procesar con diferentes frameworks.
La firma que debes obtener de donde agregaste el punto final, p. ej., el portal de la aplicación.
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// Estos fueron enviados desde el servidor
const headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"test": 2432232314}';
const wh = new Webhook(secret);
// Lanza un error, devuelve el contenido verificado si tiene éxito
wh.verify(payload, headers);
Ejemplos específicos del framework
¡Aquí hay ejemplos de cómo ajustar los ejemplos anteriores a tu framework favorito!
Python (Django)
from django.http import HttpResponse
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@csrf_exempt
def webhook_handler(request):
headers = request.headers
payload = request.body
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
return HttpResponse(status=400)
# Haz algo con el mensaje...
return HttpResponse(status=204)
Python (Flask)
from flask import request
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@app.route('/webhook/')
def webhook_handler():
headers = request.headers
payload = request.get_data()
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
return ('', 400)
# Haz algo con el mensaje...
return ('', 204)
Python (FastAPI)
from fastapi import Request, Response, status
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@router.post("/webhook/", status_code=status.HTTP_204_NO_CONTENT)
async def webhook_handler(request: Request, response: Response):
headers = request.headers
payload = await request.body()
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
response.status_code = status.HTTP_400_BAD_REQUEST
return
# Haz algo con el mensaje...
Node.js (Next.js)
import { Webhook } from "svix";
import { buffer } from "micro";
export const config = {
api: {
bodyParser: false,
},
}
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
export default async function handler(req, res) {
const payload = (await buffer(req)).toString();
const headers = req.headers;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Haz algo con el mensaje...
res.json({});
}
Node.js (Next.js 13 App Router)
import { Webhook } from "svix";
const webhookSecret: string = process.env.WEBHOOK_SECRET || "your-secret";
export async function POST(req: Request) {
const svix_id = req.headers.get("svix-id") ?? "";
const svix_timestamp = req.headers.get("svix-timestamp") ?? "";
const svix_signature = req.headers.get("svix-signature") ?? "";
const body = await req.text();
const sivx = new Webhook(webhookSecret);
let msg;
try {
msg = sivx.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
} catch (err) {
return new Response("Bad Request", { status: 400 });
}
console.log(msg);
// Resto
return new Response("OK", { status: 200 });
}
Node.js (Netlify Functions)
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
export const handler = async ({body, headers}) => {
const payload = body;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Haz algo con el mensaje...
res.json({});
}
Node.js (Express)
Nota: Al integrar este ejemplo en una base de código más grande, deberás asegurarte de no aplicar el middleware express.json()
a la ruta del webhook, porque la carga útil debe pasarse a wh.verify
sin ningún análisis previo.
import { Webhook } from "svix";
import bodyParser from "body-parser";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
app.post('/webhook', bodyParser.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body;
const headers = req.headers;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Haz algo con el mensaje...
res.json({});
});
Node.js (NestJS)
Inicializa la aplicación con la bandera rawBody
establecida en true. Consulta la documentación de NestJS para obtener más detalles.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
{ rawBody: true } // agregar bandera rawBody
);
await app.listen(3000);
}
bootstrap();
// webhook.controller.ts
import { Controller, Post, RawBodyRequest, Req } from '@nestjs/common';
import { Request } from 'express';
import { Webhook } from 'svix';
@Controller('webhook')
class WebhookController {
@Post()
webhook(@Req() request: RawBodyRequest<Request>) {
const secret = 'whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw';
const wh = new Webhook(secret);
const payload = request.rawBody.toString('utf8');
const headers = request.headers;
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
// manejar error
}
// Haz algo con el mensaje...
}
}
Go (biblioteca estándar)
package main
import (
"io"
"log"
"net/http"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
func main() {
wh, err := svix.NewWebhook(secret)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
headers := r.Header
payload, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = wh.Verify(payload, headers)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Haz algo con el mensaje...
w.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", nil)
}
Go (Gin)
package main
import (
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
func main() {
wh, err := svix.NewWebhook(secret)
if err != nil {
log.Fatal(err)
}
r := gin.Default()
r.POST("/webhook", func(c *gin.Context) {
headers := c.Request.Header
payload, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = wh.Verify(payload, headers)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Haz algo con el mensaje...
c.JSON(200, gin.H{})
})
r.Run()
}
Rust (axum)
Agrega la ruta webhook_in
a continuación a un enrutador axum.
use axum::{body::Bytes, http::StatusCode};
use hyper::HeaderMap;
pub const SECRET: &'static str = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
pub async fn webhook_in(headers: HeaderMap, body: Bytes) -> StatusCode {
let wh = svix::webhooks::Webhook::new(SECRET);
if let Err(_) = wh {
return StatusCode::INTERNAL_SERVER_ERROR;
}
if let Err(_) = wh.verify(&body, &headers) {
return StatusCode::BAD_REQUEST;
}
// Haz algo con el mensaje...
StatusCode::NO_CONTENT
}
Ruby (Ruby on Rails)
Una vez que hayas configurado tu proyecto, agrega una ruta a tu archivo config/routes.rb
en la parte superior del bloque Rails.application.routes.draw
:
Rails.application.routes.draw do
post "/webhook", to: "webhook#index"
# Para detalles sobre el DSL disponible dentro de este archivo, consulta https://guides.rubyonrails.org/routing.html
end
La ruta anterior declara que las solicitudes POST /webhook
se asignan a la acción de índice de WebhookController
.
Para crear WebhookController
y su acción de índice, ejecutaremos el generador de controladores (con la opción --skip-routes
porque ya tenemos una ruta apropiada):
bin/rails generate controller Webhook index --skip-routes
Rails creará varios archivos por ti:
create app/controllers/webhook_controller.rb
invoke erb
create app/views/webhook
create app/views/webhook/index.html.erb
invoke test_unit
create test/controllers/webhook_controller_test.rb
invoke helper
create app/helpers/webhook_helper.rb
invoke test_unit
invoke assets
invoke scss
create app/assets/stylesheets/webhook.scss
Ahora podemos agregar nuestra lógica de verificación al archivo app/controllers/webhook_controller.rb
recién creado:
require 'svix'
class WebhookController < ApplicationController
protect_from_forgery with: :null_session # desactiva el middleware CSRF; requerido para los puntos finales de la API
def index begin payload = request.body.read headers = request.headers wh = Svix::Webhook.new("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")
json = wh.verify(payload, headers)
# Haz algo con el mensaje...
head :no_content rescue head :bad_request end endend
PHP (Laravel)
En tu archivo routes/api.php
agrega lo siguiente después de la última directiva de uso:
use Svix\Webhook;
use Svix\Exception\WebhookVerificationException;
Route::post('webhook', function(Request $request) {
$payload = $request->getContent();
$headers = collect($request->headers->all())->transform(function ($item) {
return $item[0];
});
try {
$wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw");
$json = $wh->verify($payload, $headers);
# Haz algo con el mensaje...
return response()->noContent();
} catch (WebhookVerificationException $e) {
return response(null, 400);
}
});