> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kemypay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recevez le résultat de chaque paiement, signé

Quand un paiement atteint son statut final, nous envoyons (`POST`) un événement à la `webhook_url` du paiement, ou à défaut à celle de son service.

| Événement | Quand |
| - | - |
| `mobile_money.collection.completed` | Le client a payé. `net_amount` est sur le solde du service |
| `mobile_money.collection.failed` | Le paiement a échoué. `failure_code` dit pourquoi |

## Contenu

```json theme={null}
{
  "id": "evt_5b1f0c2a-9d3e-4f7a-8b6c-1e2d3f4a5b6c",
  "type": "mobile_money.collection.completed",
  "created_at": "2026-10-29T10:01:12+00:00",
  "data": {
    "code": "019a9c41-7e2d-4b8f-a1c3-5d6e7f8a9b0c",
    "reference": "order-2026-10-29-0001",
    "service_code": "019a8f2e-4c1b-7d3a-9e55-2b6f0c1d8a47",
    "status": "completed",
    "currency": "XAF",
    "value": "1000.00",
    "amount": "1000.00",
    "fee": "33.00",
    "net_amount": "967.00",
    "metadata": { "user_id": "42" },
    "operator_transaction_id": "MP261029.1001.A12345"
  },
  "mode": "live"
}
```

`data` est le même objet que [`GET /collections/{code}`](https://docs.kemypay.com/fr/api-reference/collections/get).

## En-têtes

| En-tête | Contenu |
| - | - |
| `X-KemyCard-Signature` | `t=<timestamp>,v1=<signature>` |
| `X-KemyCard-Timestamp` | L'horodatage utilisé dans la signature |
| `X-KemyCard-Event` | Le type d'événement |
| `X-KemyCard-Delivery-Id` | Unique pour chaque tentative d'envoi |
| `X-KemyCard-Idempotency-Key` | L'`id` de l'événement : le même à chaque relance |

## Vérifier la signature

La signature est un **HMAC-SHA256** de `"<timestamp>.<raw body>"` avec le `webhook_secret` du service. Vérifiez-la toujours, sur le corps **brut**, avant de faire confiance à l'événement.

```javascript Node.js theme={null}
import crypto from 'node:crypto';

export function verifyKemyPay(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
}
```

```php PHP theme={null}
function verifyKemyPay(string $rawBody, string $signatureHeader, string $secret, int $tolerance = 300): bool
{
    parse_str(str_replace(',', '&', $signatureHeader), $parts);
    $timestamp = (int) ($parts['t'] ?? 0);
    if ($timestamp === 0 || abs(time() - $timestamp) > $tolerance) {
        return false;
    }

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

    return hash_equals($expected, (string) ($parts['v1'] ?? ''));
}

// $rawBody = file_get_contents('php://input'); // corps brut de la requête
```

```python Python theme={null}
import hashlib, hmac, time

def verify_kemypay(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp = int(parts.get("t", "0"))
    if not timestamp or abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## Répondre vite, traiter une seule fois

* Répondez **`2xx` en moins de 15 secondes**, puis faites le travail long en arrière-plan.
* **Traitez chaque événement une seule fois** : enregistrez l'`id` de l'événement (aussi dans `X-KemyCard-Idempotency-Key`) et ignorez-le si vous le revoyez.
* Toute autre réponse, ou un délai dépassé, déclenche une relance : tout de suite, puis après 1 minute, 5 minutes, 30 minutes, 2 heures, 8 heures et 24 heures. Ensuite, l'événement est abandonné : vous pouvez toujours lire le paiement avec [`GET /collections/{code}`](https://docs.kemypay.com/fr/api-reference/collections/get).

> [!WARNING]
>
> Vous avez perdu votre secret ? [Renouvelez-le](https://docs.kemypay.com/fr/api-reference/services/rotate-secret.md) : l'ancien cesse aussitôt de fonctionner, donc mettez d'abord votre serveur à jour.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.