Webhooks
Vérifier les signatures
Chaque livraison est signée avec HMAC-SHA256. Vérifiez la signature sur le corps brut avant de faire confiance au contenu.
Vérifier les signatures
Chaque livraison porte les entêtes X-HonkIO-Signature, X-HonkIO-Timestamp et X-HonkIO-Event. Vérifiez la signature avant de faire confiance au contenu, et rejetez tout ce qui ne correspond pas. Le secret de signature est retourné une seule fois, à la création du webhook. Renouvelez le secret depuis le tableau de bord ou avec POST /v1/webhooks/:id/rotate-secret; l’ancien secret cesse de signer immédiatement. Répondez par un code autre que 2xx à une signature invalide et la nouvelle tentative arrivera signée avec le nouveau secret.
signed_string = timestamp + "." + body
signature = HMAC_SHA256(key = signing_secret, message = signed_string)
# timestamp: the X-HonkIO-Timestamp value as sent, decimal Unix epoch seconds
# body: the raw request bytes, read before any JSON parsing
# key: the UTF-8 bytes of the secret as issued, not hex decoded- Séparateur : un point littéral se trouve entre l’horodatage et le premier octet du corps.
- Horodatage : la valeur
X-HonkIO-Timestampexactement telle qu’envoyée, en secondes Unix décimales. - Corps : les octets bruts de la requête, lus avant toute analyse JSON. Tout ce qui analyse puis resérialise le JSON modifie les octets, et l’empreinte ne correspondra jamais.
- Signature : hexadécimal minuscule, 64 caractères. Rejetez tout ce qui ne fait pas 64 caractères hexadécimaux avant de décoder, puis comparez en temps constant.
- Secret : les octets UTF-8 du secret de signature exactement tel qu’émis. Ne le décodez pas au préalable.
Avec le SDK Node.js, honkio.webhooks.verify fait toutes les vérifications ci-dessus et renvoie l’événement analysé :
const webhookSecret = process.env.HONKIO_WEBHOOK_SECRET ?? ''
if (!webhookSecret) throw new Error('Set HONKIO_WEBHOOK_SECRET')
// A fetch-style handler (Next.js route handlers, Hono, Bun and so on).
// With Express, pass the body from express.raw() and req.headers instead.
export async function POST(request: Request) {
// The raw text, read before any JSON parsing: it is what was signed.
const { data: event, error } = honkio.webhooks.verify(await request.text(), request.headers, webhookSecret)
if (error) return new Response(null, { status: 400 }) // invalid_signature or invalid_argument
console.log(event.id, event.type, event.livemode)
return new Response(null, { status: 200 })
}Sans le SDK, ou pour transposer la vérification dans un autre langage, voici un vérificateur complet, qui refuse par défaut, en Node pur :
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody must be the unparsed body. Most frameworks hand you a parsed object
// by default; reach for the raw buffer (express.raw, request.text(), and so on).
export function verifyHonkioWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
const timestamp = headers['x-honkio-timestamp']
const signature = headers['x-honkio-signature']
if (!timestamp || !signature) return false
const ts = Number.parseInt(timestamp, 10)
if (!Number.isFinite(ts)) return false
if (Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false
// Buffer.from(x, 'hex') drops invalid bytes without complaining, which can
// truncate two different values into a match. Check the shape first.
if (!/^[0-9a-f]{64}$/i.test(signature)) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'))
}Validez votre implémentation avec ce vecteur. Si vous reproduisez la signature, c’est terminé :
secret deadbeef00112233445566778899aabbccddeeffdeadbeef0123456789abcdef
timestamp 1788920816
body {"id":"00000000-0000-4000-8000-000000000000","type":"message.delivered","created":"2026-09-09T02:26:57Z","account_id":"acc_test","data":{"message_id":"msg_test","to":"+16135550123","status":"delivered"}}
signature 058aca981d55947bb71d2ba4ff98487d5f5c6caf01189af72266cb3f1e22ccbfLe secret ci-dessus est jetable et n’appartient à aucun compte, vous pouvez donc le placer dans un test. Votre propre secret n’a jamais besoin de quitter votre serveur, et nous ne l’enverrons jamais par courriel, pas plus qu’une signature.
Rejetez tout ce qui sort d’une fenêtre de 300 secondes par rapport à X-HonkIO-Timestamp, et dédupliquez sur l’id de l’évènement. La livraison se fait au moins une fois, donc le même id peut légitimement arriver plusieurs fois.
Les nouvelles tentatives et les rejeux sont signés au moment de leur envoi, un même id d’évènement peut donc arriver avec un horodatage et une signature différents à chaque fois. Les livraisons rejouées portent aussi X-HonkIO-Replay: true. Vérifiez toujours avec l’entête d’horodatage arrivé avec cette requête, jamais avec un horodatage conservé auparavant.
Il n’existe pas de point de terminaison pour la rotation des secrets. Pour changer un secret, enregistrez un second point de terminaison, confirmez qu’il vérifie correctement, puis supprimez le premier. Les deux reçoivent les évènements tant qu’ils sont actifs, ce que votre déduplication par id absorbe.
HonkIO