Documentation/Recevoir les webhooks
openapi.jsonOuvrir mon espace

Recevoir les 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.

© 2026 PayizUne question ? L’aide est dans le rond, en bas à droite.

Recevoir les 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.