Skip to Content
GuidesVérifier les webhooks

Vérification des webhooks

Chaque webhook Kadryza est signé avec le secret de l’endpoint.

Header confirmé :

X-Kadryza-Signature: sha256=<hmac_hex>

Algorithme :

  1. récupérer le body JSON brut ;
  2. calculer HMAC-SHA256(rawBody, webhookSecret) ;
  3. encoder en hex ;
  4. comparer au header sha256=<hex> en constant-time.
⚠️

Ne vérifiez jamais la signature sur un objet JSON déjà parsé puis re-sérialisé. Le HMAC doit être calculé sur le body brut reçu.

Avec le SDK

import { verifyWebhookSignature, parseKadryzaWebhookPayload, getWebhookIdempotencyKey, isPaymentSessionWebhook, isTransactionWebhook } from '@kadryza/sdk' const valid = verifyWebhookSignature({ payload: rawBody, signature: req.headers['x-kadryza-signature'], secret: process.env.KADRYZA_WEBHOOK_SECRET! }) if (!valid) { throw new Error('Invalid Kadryza webhook signature') } const event = parseKadryzaWebhookPayload(rawBody) const idempotencyKey = getWebhookIdempotencyKey(event)

Vous pouvez aussi utiliser l’instance SDK :

const event = kadryza.webhooks.verifyAndParse({ payload: rawBody, signature: req.headers['x-kadryza-signature'], secret: process.env.KADRYZA_WEBHOOK_SECRET! }) if (!event) { throw new Error('Invalid Kadryza webhook signature') }

Le SDK accepte sha256=<hex> et garde la compatibilité avec un hex brut.

Avec Node.js crypto

import crypto from 'node:crypto' function verifyKadryzaSignature(rawBody: string, signature: string | undefined, secret: string): boolean { if (!signature) return false const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex') const actualBuffer = Buffer.from(signature) const expectedBuffer = Buffer.from(expected) if (actualBuffer.length !== expectedBuffer.length) { return false } return crypto.timingSafeEqual(actualBuffer, expectedBuffer) }

Express

import express from 'express' import { verifyWebhookSignature, parseKadryzaWebhookPayload, getWebhookIdempotencyKey, isPaymentSessionWebhook, isTransactionWebhook } from '@kadryza/sdk' const app = express() app.post( '/webhooks/kadryza', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8') const signature = req.header('X-Kadryza-Signature') const valid = verifyWebhookSignature({ payload: rawBody, signature, secret: process.env.KADRYZA_WEBHOOK_SECRET! }) if (!valid) { return res.status(401).send('invalid signature') } const event = parseKadryzaWebhookPayload(rawBody) const idempotencyKey = getWebhookIdempotencyKey(event) // Stockez idempotencyKey avant toute action irréversible. if (isTransactionWebhook(event) && event.event === 'transaction.success') { // Transaction confirmée. } if (isPaymentSessionWebhook(event) && event.event === 'payment_session.succeeded') { // Payment session confirmée. } return res.sendStatus(200) } )

Next.js Route Handler

import { NextRequest, NextResponse } from 'next/server' import { verifyWebhookSignature, parseKadryzaWebhookPayload } from '@kadryza/sdk' export async function POST(request: NextRequest) { const rawBody = await request.text() const signature = request.headers.get('x-kadryza-signature') ?? undefined const valid = verifyWebhookSignature({ payload: rawBody, signature, secret: process.env.KADRYZA_WEBHOOK_SECRET! }) if (!valid) { return NextResponse.json({ error: 'invalid signature' }, { status: 401 }) } const event = parseKadryzaWebhookPayload(rawBody) return NextResponse.json({ received: true }) }

Idempotence

Une même livraison peut arriver plusieurs fois. Les événements payment_session.* portent un identifiant stable entre les retries : event_id dans le corps, X-Kadryza-Event-ID en en-tête. Les événements transaction.* n’en ont pas : la clé est alors event:data.id:data.status. getWebhookIdempotencyKey choisit la bonne :

const idempotencyKey = getWebhookIdempotencyKey(event)

Stockez cette clé dans votre base avant d’exécuter une action irréversible.

Headers de test

Les webhooks déclenchés par /v1/webhooks/:id/test incluent :

X-Kadryza-Test: true

Les webhooks payment_session.* liés à une session test incluent aussi ce header, avec data.environment = "test".

Les événements transaction.* de sandbox ne portent pas ce header.

Tester localement

Kadryza refuse les URL de webhook qui ne pointent pas vers une adresse publique : http://localhost:3000/… est rejeté (400 WEBHOOK_URL_UNSAFE), même avec une clé de test. Pour recevoir des webhooks sur votre machine, exposez votre serveur local derrière une URL publique temporaire (un tunnel HTTPS, par exemple ngrok), enregistrez cette URL avec POST /v1/webhooks, puis déclenchez un envoi avec POST /v1/webhooks/:id/test.

Retirez l’endpoint (DELETE /v1/webhooks/:id) quand le tunnel est fermé : sinon les livraisons échoueront et seront réessayées.

Checklist

  • le body brut est disponible ;
  • X-Kadryza-Signature est vérifié ;
  • la comparaison est constant-time ;
  • l’endpoint répond en HTTP 2xx après traitement accepté ;
  • le traitement est idempotent ;
  • le secret webhook n’est jamais loggé.