# Vérification de la signature du webhook

La notification de paiement arrive à une adresse que seuls votre boutique et notre service connaissent. Mais l'adresse peut fuiter — des journaux, de la configuration, de l'historique de développement. Pour distinguer une notification authentique d'une contrefaçon, chaque requête est signée avec le secret du webhook.

Vérifiez la signature avant de livrer la commande ou de changer son statut.

## En-têtes de signature

| En-tête | Valeur |
| ------------- | ------------------------------------------------ |
| `X-Timestamp` | Heure d'envoi, unix time en secondes |
| `X-Signature` | Le préfixe `sha256=` et le HMAC-SHA256 en hexadécimal |

La chaîne signée est l'heure d'envoi, un point et le corps de la requête :

```
1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}
```

La clé de signature est le champ Secret du webhook (Webhook secret) dans les réglages du projet. Vous pouvez le réémettre au même endroit si la valeur est compromise.

## Étapes de vérification

1. Prenez le corps brut de la requête, avant d'analyser le JSON.
2. Concaténez la valeur de `X-Timestamp`, un point et le corps.
3. Calculez le HMAC-SHA256 avec le secret du webhook.
4. Comparez le résultat avec `X-Signature` par une comparaison en temps constant.
5. Rejetez la requête si `X-Timestamp` s'écarte trop de l'heure actuelle.

## Exemples d'implémentation

**PHP**

```php showLineNumbers
<?php
$secret = 'webhook secret from your project settings';

$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
    http_response_code(400);
    exit;
}

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

$data = json_decode($body, true);

// Handle the order

http_response_code(200);
```

**TypeScript**

```typescript showLineNumbers
import { createHmac, timingSafeEqual } from 'node:crypto';
import express, { type Request, type Response } from 'express';

const app = express();
const secret = 'webhook secret from your project settings';

// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
    const timestamp = req.get('X-Timestamp') ?? '';
    const signature = req.get('X-Signature') ?? '';
    const body = (req.body as Buffer).toString('utf8');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(400);
    }

    const expected = 'sha256=' + createHmac('sha256', secret)
        .update(`${timestamp}.${body}`, 'utf8')
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);

    // Check the length first: timingSafeEqual needs buffers of equal size
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.sendStatus(403);
    }

    const data = JSON.parse(body);

    // Handle the order

    res.sendStatus(200);
});
```

**JavaScript**

Le même gestionnaire sans les types — pour un projet JavaScript pur.

```js showLineNumbers
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const secret = 'webhook secret from your project settings';

// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const timestamp = req.get('X-Timestamp') ?? '';
    const signature = req.get('X-Signature') ?? '';
    const body = req.body.toString('utf8');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(400);
    }

    const expected = 'sha256=' + createHmac('sha256', secret)
        .update(`${timestamp}.${body}`, 'utf8')
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);

    // Check the length first: timingSafeEqual needs buffers of equal size
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.sendStatus(403);
    }

    const data = JSON.parse(body);

    // Handle the order

    res.sendStatus(200);
});
```

**Python**

```python showLineNumbers
import hmac
import hashlib
import time

from flask import Flask, request

app = Flask(__name__)
secret = 'webhook secret from your project settings'

@app.post('/webhook')
def webhook():
    timestamp = request.headers.get('X-Timestamp', '')
    signature = request.headers.get('X-Signature', '')
    body = request.get_data(as_text=True)

    if abs(time.time() - int(timestamp or 0)) > 300:
        return '', 400

    expected = 'sha256=' + hmac.new(
        secret.encode(),
        f'{timestamp}.{body}'.encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return '', 403

    data = request.get_json()

    # Handle the order

    return '', 200
```

## À garder à l'esprit

**C'est le corps brut qui est signé.** Si vous analysez le JSON et le reconstruisez, l'ordre des clés et les espaces changent — la signature ne correspondra pas. Calculez le HMAC avant l'analyse.

**Chaque nouvelle tentative a sa propre signature.** L'heure d'envoi est nouvelle à chaque tentative, donc la signature diffère aussi. Ne stockez pas une signature pour la comparer aux requêtes futures.

**Le secret peut être réémis.** L'ancien secret cesse de fonctionner immédiatement après la réémission, mettez donc à jour la valeur dans votre boutique au même moment.

**La vérification de la signature ne remplace pas l'idempotence.** La signature prouve que la requête est authentique, pas que vous la voyez pour la première fois. Une nouvelle tentative arrive avec une signature valide — vérifiez par `invoice.id`.
