# Webhooks

Payiz écrit à votre serveur dès qu’un événement se produit, en JSON signé. Votre serveur répond 2xx en moins de dix secondes.

## Déclarer une destination

Dans votre espace : **Développeurs › Webhooks › Nouvelle adresse**. Donnez l’adresse de votre serveur (en HTTPS) et cochez les événements qui vous intéressent — rien de coché veut dire « tous ». Payiz vous rend un secret de signature, montré une fois puis relisible dans les réglages de la destination.

## Ce que vous recevez

```json
{
  "id": "evt_01j9x4w8n2q5tz7m3kd6hw2p",
  "type": "payment.succeeded",
  "created": 1790002912,
  "livemode": false,
  "data": {
    "object": {
      "object": "payment",
      "id": "pay_01j9x3kq7m4tzv8qhw2n",
      "amount": 10000,
      "currency": "XOF",
      "status": "succeeded",
      "description": "Commande 42",
      "reference": "cmd-42",
      "metadata": {},
      "fees_paid_by": "merchant",
      "amount_paid": 10000,
      "fee": 250,
      "net": 9750,
      "amount_refunded": 0,
      "payment_url": "https://pay.payiz.app/pay_01j9x3kq7m4tzv8qhw2n",
      "return_url": "https://boutique.bj/merci",
      "expires_at": 1790089092,
      "paid_at": 1790002912,
      "available_at": 1790089312,
      "latest_attempt": {
        "object": "payment_attempt",
        "id": "tnt_01j9x4a2b7c5d8e3f6g1h0jk",
        "payment": "pay_01j9x3kq7m4tzv8qhw2n",
        "status": "succeeded",
        "source": "api",
        "phone": "+229 01 •• •• 34 56",
        "country": "BEN",
        "operator": "mtn",
        "amount": 10000,
        "currency": "XOF",
        "failure_code": null,
        "payer_message": null,
        "operator_reference": "MP240927.1512.A83921",
        "created": 1790002700,
        "completed_at": 1790002912,
        "livemode": false
      },
      "created": 1790002692,
      "livemode": false
    }
  }
}
```

`data.object` est l’objet tel qu’une lecture de l’API le rendrait au moment de l’événement — le même que `GET /v1/payments/{id}`, figé à cet instant. Relisez-le si vous avez besoin de son état d’aujourd’hui. Un paiement y porte sa dernière demande, `latest_attempt` : sur `payment.attempt_failed`, sa raison est dans `failure_code`.

## Vérifier la signature

Chaque requête porte l’en-tête `Payiz-Signature` : un horodatage et une empreinte HMAC-SHA256 du corps brut.

```text
Payiz-Signature: t=1790002692,v1=5f2c…
```

```javascript
import { createHmac, timingSafeEqual } from 'node:crypto'

export function signatureValide(corpsBrut, entete, secret) {
  const parties = Object.fromEntries(entete.split(',').map((p) => p.split('=')))
  const attendue = createHmac('sha256', secret)
    .update(parties.t + '.' + corpsBrut)
    .digest('hex')
  // Refusez une signature de plus de cinq minutes.
  if (Math.abs(Date.now() / 1000 - Number(parties.t)) > 300) return false
  return timingSafeEqual(Buffer.from(attendue), Buffer.from(parties.v1))
}
```

```php
<?php
function signature_valide(string $corpsBrut, string $entete, string $secret): bool {
    parse_str(str_replace(',', '&', $entete), $parties);
    $attendue = hash_hmac('sha256', $parties['t'] . '.' . $corpsBrut, $secret);
    if (abs(time() - (int) $parties['t']) > 300) return false;
    return hash_equals($attendue, $parties['v1']);
}
```

Vérifiez toujours sur le **corps brut**, avant de le lire en JSON : un corps reformaté ne donne plus la même empreinte.

## Les événements

- `payment.succeeded` — L’argent est arrivé. C’est lui qui dit de livrer.
- `payment.attempt_failed` — Une demande n’a pas abouti chez le payeur. Le paiement reste payable jusqu’à son échéance.
- `payment.expired` — L’échéance est passée sans paiement.
- `payment.canceled` — Le paiement a été annulé avant d’être payé.
- `refund.succeeded` — Le remboursement est arrivé chez le payeur.
- `refund.failed` — Le remboursement n’a pas pu partir ; la somme revient sur votre solde.
- `payout.paid` — Le retrait est arrivé chez le bénéficiaire.
- `payout.failed` — Le retrait n’a pas pu partir ; la somme revient sur votre solde.
- `dispute.created` — Un payeur conteste un paiement : la somme est retenue, votre dossier est attendu.
- `dispute.closed` — L’issue du litige est tombée : gagné, la somme revient ; perdu, elle part.
- `conversion.completed` — Une conversion entre deux de vos portefeuilles est faite.

## Les relances

- Sans réponse 2xx, Payiz retente : 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, puis toutes les 24 h — puis abandonne.
- Une destination qui échoue trois jours de suite **se tait** : réveillez-la depuis l’écran quand votre serveur est réparé.
- La livraison se fait **au moins une fois**, sans ordre garanti : dédoublonnez par `id`, et relisez l’objet au besoin.
- Chaque livraison se relit dans l’écran — corps envoyé, signature, tentatives, réponse de votre serveur — et se **renvoie** d’un bouton.

## Essayer avant le premier paiement

« Envoyer un test », dans les réglages d’une destination, envoie un événement pour de faux, signé comme un vrai, de type `webhook.test`. Il apparaît dans les livraisons avec la réponse de votre serveur.

