Vérification des webhooks
Chaque webhook Kadryza est signé avec le secret de l’endpoint.
Header confirmé :
X-Kadryza-Signature: sha256=<hmac_hex>Algorithme :
- récupérer le body JSON brut ;
- calculer
HMAC-SHA256(rawBody, webhookSecret); - encoder en hex ;
- 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: trueLes 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-Signatureest vérifié ;- la comparaison est constant-time ;
- l’endpoint répond en HTTP
2xxaprès traitement accepté ; - le traitement est idempotent ;
- le secret webhook n’est jamais loggé.