Sign in
Introduction/Notifications Webhook

Notifications webhook

Recevez en temps réel les mises à jour de statut des paiements et des retraits via des webhooks signés HMAC.

Le système 2328.io envoie un webhook à votre url_callback chaque fois qu'un statut de paiement change. C'est la méthode recommandée pour être notifié des paiements réussis.

Format de la requête

  • Méthode : POST
  • Content-Type : application/json
  • Signature : champ sign dans le corps de la requête

Payload

Le corps du webhook suit le format de la réponse de /v1/payment/info et ajoute tx_explorer_url, ainsi que le champ sign utilisé pour vérifier la signature.

Paiement réussi

JSON
{
  "uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
  "order_id": "ORDER-12345",
  "amount": "180.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
  "expires_at": "2026-05-09T16:56:58+03:00",
  "created_at": "2026-05-09T15:56:58+03:00",
  "payer_currency": "TON",
  "payer_amount": "0.95256917",
  "network": "TON",
  "address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
  "payment_status": "paid",
  "txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "tx_explorer_url": "https://tonviewer.com/transaction/41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "payment_amount": "0.95256917",
  "merchant_amount": "0.949711462490000000",
  "amount_usd": "2.41324380",
  "exchange_rate": "0.01340691",
  "sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}

Paiement annulé / échoué

Lorsque le paiement n'est pas dans un état terminal paid, txid, payment_amount et merchant_amount valent null :

JSON
{
  "uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
  "order_id": "ORDER-12345",
  "amount": "2800.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
  "expires_at": "2026-05-09T06:19:04+03:00",
  "created_at": "2026-05-09T05:19:04+03:00",
  "payer_currency": "ETH",
  "payer_amount": "0.01620968",
  "network": "ETH-ERC20",
  "address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
  "payment_status": "cancel",
  "txid": null,
  "tx_explorer_url": null,
  "payment_amount": null,
  "merchant_amount": null,
  "amount_usd": "37.53934800",
  "exchange_rate": "0.01340691",
  "sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}

Référence des champs

ChampTypeDescription
uuidstringUUID du paiement
order_idstringVotre ID de commande
amountdecimal (8 dp)Montant fiat dans currency
currencystringDevise fiat demandée par le marchand
urlstringURL de la page de paiement hébergée
expires_atstring (ISO 8601)Date d'expiration de la session de paiement
created_atstring (ISO 8601)Date de création de la session de paiement
payer_currencystringCrypto utilisée par le payeur
payer_amountdecimal (8 dp)Montant en crypto attendu
networkstringRéseau blockchain
addressstringAdresse de dépôt
payment_statusstringL'une des valeurs : pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (voir References)
txidstring | nullHash de la transaction blockchain, présent uniquement après confirmation du paiement
tx_explorer_urlstring | nullURL de la transaction dans l’explorateur blockchain. Vaut null si txid est absent ou si le transfert est un P2P interne.
payment_amountdecimal | nullMontant réellement payé, présent uniquement après le paiement
merchant_amountdecimal (18 dp) | nullMontant crédité au marchand après les frais
amount_usddecimal (8 dp)Montant en USD au moment de la création
exchange_ratedecimalTaux de change crypto / fiat utilisé
signstring (hex)Signature HMAC-SHA256 du payload

Vérification de la signature

Pour vérifier la signature d'un webhook :

  1. Extrayez le champ sign du payload
  2. Supprimez le champ sign de l'objet
  3. Encodez les champs restants en JSON
  4. Encodez le JSON en Base64
  5. Calculez HMAC-SHA256 à partir de la chaîne Base64 en utilisant votre API_KEY
  6. Comparez la signature calculée à la valeur sign à l'aide d'une comparaison à temps constant
PHP
<?php
function verifyWebhookSign(array $data, string $apiKey): bool {
    $receivedSign = $data['sign'] ?? '';
    unset($data['sign']);

    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $base64 = base64_encode($json);
    $calculated = hash_hmac('sha256', $base64, $apiKey);

    return hash_equals($calculated, $receivedSign);
}

$apiKey = 'YOUR_API_KEY';
$payload = json_decode(file_get_contents('php://input'), true);

if (!verifyWebhookSign($payload, $apiKey)) {
    http_response_code(401);
    exit;
}

switch ($payload['payment_status']) {
    case 'paid':
    case 'overpaid':
        // Credit the order — check idempotency by order_id first
        break;
    case 'underpaid_check':
    case 'underpaid':
    case 'cancel':
        break;
}

http_response_code(200);

Vérifiez toujours la signature avant de créditer des fonds à un utilisateur. Un webhook non signé ou mal signé pourrait être une requête frauduleuse.

Webhooks de retrait

Lorsque le status d'un retrait change, le système envoie un webhook POST à l'URL url_callback fournie lors de la création du retrait. Si url_callback n'a pas été fourni, aucun webhook n'est envoyé pour ce retrait.

Les webhooks de retrait doivent être vérifiés avec votre Payout API key — pas l'API key classique. L'algorithme de signature est identique à celui des webhooks de paiement (retirer sign, encoder en JSON, base64, HMAC-SHA256), seule la clé diffère.

Payload

JSON
{
  "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
  "order_id": "4dfdcc84402b1185b71cbe399321533e",
  "status": "completed",
  "currency": "TRX",
  "network": "TRX-TRC20",
  "amount": "3.00",
  "merchant_amount": "3.00",
  "network_amount": "3.00",
  "amount_usd": "1.04",
  "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
  "memo": null,
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "tx_explorer_url": "https://tronscan.org/#/transaction/9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "block_number": null,
  "error_type": null,
  "created_at": "2026-05-07T00:08:38+03:00",
  "updated_at": "2026-05-07T00:08:54+03:00",
  "from_currency": "USDT",
  "debited_amount": "1.050735",
  "debited_currency": "USDT",
  "sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}

Référence des champs

ChampTypeDescription
uuidstringUUID du retrait
order_idstringVotre identifiant d'idempotence / référence, si vous en avez fourni un
statusstringpending, completed, failed, cancelled (voir References)
currencystringDevise du retrait
networkstringRéseau blockchain
amountdecimalMontant du retrait (dans currency)
merchant_amountdecimalMontant prélevé sur le solde marchand
network_amountdecimalMontant réellement envoyé on-chain
amount_usddecimalValeur en USD au moment du retrait
to_addressstringAdresse blockchain du destinataire
memostring | nullMémo / tag de destination, le cas échéant
txidstring | nullHash de la transaction blockchain, défini lors du completed
tx_explorer_urlstring | nullURL de la transaction dans l’explorateur blockchain. Vaut null si txid est absent ou si le transfert est un P2P interne.
block_numberinteger | nullHauteur du bloc de la transaction on-chain
error_typestring | nullRaison lorsque status = failed (par ex. aml_risk, voir References)
created_atstring (ISO 8601)Date de création du retrait
updated_atstring (ISO 8601)Date du dernier changement de statut
from_currencystringSolde source débité pour le retrait lorsqu'une conversion automatique a été utilisée (par ex. USDT pour un retrait en BTC)
debited_amountdecimalMontant débité du solde from_currency
debited_currencystringDevise du débit
signstring (hex)Signature HMAC-SHA256 du payload, signée avec la Payout API key

Bonnes pratiques

  • Idempotence — Vérifiez toujours si le paiement a déjà été traité (par order_id ou uuid). Les webhooks peuvent arriver plusieurs fois.
  • Réponse rapide — Renvoyez HTTP 200 le plus rapidement possible. Déléguez le traitement lourd à une file d'attente en arrière-plan.
  • Retentatives — Si le système ne reçoit pas de HTTP 200, le webhook est renvoyé après 2 minutes. Maximum 5 tentatives.
  • Traitement asynchrone — Traitez les événements webhook de manière asynchrone pour éviter de bloquer la réponse.
  • Sécurité — Vérifiez TOUJOURS la signature sign avant de faire confiance au payload.

Les webhooks peuvent arriver dans le désordre. Ne supposez pas que le premier webhook reçu est l'état final — récupérez toujours les données via /v1/payment/info (ou /v1/payout/status/{uuid}) si vous avez besoin de certitude.

Contrat de livraison et de traitement

Utilisez l'ordre suivant dans votre point de terminaison webhook :

  1. Lisez le corps de la requête sans enregistrer les secrets ni la signature complète.
  2. Identifiez s'il s'agit d'un événement de paiement/de portefeuille statique ou d'un événement de paiement afin de sélectionner la clé API correcte.
  3. Supprimez sign, reproduisez les octets JSON documentés, calculez le HMAC-SHA256 et comparez en temps constant.
  4. Validez les identifiants requis, les chaînes décimales et les valeurs de statut.
  5. Insérez de manière atomique un enregistrement de boîte de réception/idempotence. S'il existe déjà, renvoyez HTTP 200 sans répéter les effets secondaires.
  6. Valider la mutation de la commande/du grand livre et mettre en file d'attente les e-mails non critiques, l'analytique ou les notifications.
  7. Retourner rapidement HTTP 200.

Ne pas appeler de services tiers lents pendant la transaction d'idempotence. Un délai après la validation mais avant le retour peut entraîner une nouvelle tentative ; le doublon doit observer la clé de boîte de réception validée et devenir une opération nulle.

ÉvénementIdentité principaleNotes
Session de paiementuuid + preuve de statut/versionLa même facture peut émettre plusieurs changements de statut légitimes.
Recharge partielle de paiementfacture uuid + txidPlus d'un virement peut appartenir à la même facture sous-payée.
Dépôt de portefeuille statiqueréseau + txidorder_id est réutilisé pour chaque dépôt vers ce portefeuille.
Paiementpaiement uuid + statutNe créez jamais un deuxième paiement à partir de la logique de reprise du webhook.

Si votre schéma n'a pas d'identifiant d'événement, stockez le hash de la charge utile vérifiée comme preuve d'audit supplémentaire, mais ne remplacez pas les identités commerciales ci-dessus par un horodatage.

Commande et réconciliation

La livraison est au moins une fois et les messages d'état peuvent arriver dans le désordre. Implémentez des règles commerciales monotones plutôt que « le dernier traitement l'emporte » :

  • ne jamais déplacer une commande exécutée vers check parce qu'un événement plus ancien est arrivé en retard ;
  • autoriser underpaid_check à recevoir des txids supplémentaires sans répéter les crédits précédents ;
  • considérer paid et overpaid comme des états de règlement réussis, tout en conservant leurs montants différents ;
  • conserver underpaid comme un résultat final de paiement partiel sauf si l'API autoritaire rapporte ultérieurement un autre état ;
  • rediriger aml_lock vers la révision et ne pas permettre à un opérateur de réessai générique de l'exécuter ;
  • interroger les informations de paiement/de retrait chaque fois que la transition est impossible, que le contexte est manquant ou financièrement ambigu.

Exécuter la réconciliation programmée même lorsque la livraison du webhook semble saine. Comparez votre état terminal local et le montant crédité avec /v1/payment/info, /v1/static-wallet/transactions ou /v1/payout/status/{uuid} et alertez sur les différences au lieu d'écraser silencieusement l'historique du grand livre.

Sécurité du point de terminaison webhook

  • Exiger HTTPS et maintenir le rappel accessible publiquement ; les cibles de rappel privées/boucle locale sont rejetées lors de la création du paiement.
  • Imposer une petite limite de taille pour le corps de la requête et le type de contenu JSON.
  • Limiter le débit avant un travail coûteux, mais laisser suffisamment de marge pour les pointes légitimes et les réessais.
  • Ne jamais autoriser un webhook uniquement par l'IP source. Les listes blanches réseau sont une défense en profondeur ; la vérification HMAC est obligatoire.
  • Redigez sign, les clés API, les adresses lorsque requis par la politique, et les métadonnées personnelles dans les journaux d'application.
  • Gardez à disposition à la fois les clés actuelles et celles explicitement prévues pour le remplacement durant une fenêtre de rotation contrôlée ; ne devinez jamais quelle clé a signé un événement.
  • Renvoyez un corps d'erreur générique en cas de signatures invalides afin que le point de terminaison ne devienne pas un oracle de clé ou de compte.