npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

paytech-node-ts-sdk

v2.0.0

Published

SDK TypeScript pour PayTech : paiement, statut, remboursement et vérification des notifications IPN. Zéro dépendance.

Readme

paytech-node-ts-sdk

SDK TypeScript pour PayTech — l'agrégateur sénégalais qui couvre Wave, Orange Money, Free Money, Wizall et carte bancaire en une seule intégration.

Paiement, statut, remboursement, et surtout vérification cryptographique des notifications IPN — l'étape que la plupart des intégrations oublient, et sans laquelle n'importe qui peut déclarer une commande payée.

Aucune dépendance de production. Node ≥ 18.

npm install paytech-node-ts-sdk

Créer un paiement

import { PayTech } from "paytech-node-ts-sdk";

const paytech = new PayTech({
  apiKey: process.env.PAYTECH_API_KEY!,
  apiSecret: process.env.PAYTECH_API_SECRET!,
});

const payment = await paytech.createPayment({
  item_name: "Formation niveau avancé",
  item_price: 14900,                     // un nombre, pas une chaîne
  currency: "XOF",
  env: "test",                           // "test" | "prod"
  ipn_url: "https://mon-site.sn/api/paytech/ipn",
  success_url: "https://mon-site.sn/merci",
  cancel_url: "https://mon-site.sn/panier",
  custom_field: { userId: 42 },          // te reviendra dans l'IPN
  target_payment: ["Wave", "Orange Money"],
});

// PayTech répond success: 1 ou 0 — jamais un booléen.
if (payment.success === 1) {
  redirect(payment.redirect_url!);
}

ref_command est généré automatiquement si tu n'en fournis pas, et toujours renvoyé dans la réponse. Conserve aussi payment.token : c'est lui, et non la référence, qui permet d'interroger le statut.


Vérifier une notification IPN

C'est la partie critique. PayTech POSTe sur ton ipn_url pour t'annoncer l'issue du paiement — mais l'URL est publique. Sans vérification, n'importe qui peut t'envoyer un faux sale_complete et faire livrer une commande jamais payée.

import express from "express";
import { verifyIpn, decodeCustomField, isPaymentComplete } from "paytech-node-ts-sdk";

app.post("/api/paytech/ipn", express.json(), async (req, res) => {
  const config = {
    apiKey: process.env.PAYTECH_API_KEY!,
    apiSecret: process.env.PAYTECH_API_SECRET!,
  };

  // 1. Authenticité de l'émetteur
  if (!verifyIpn(req.body, config)) {
    return res.sendStatus(401);
  }

  // 2. Répondre vite : PayTech réessaie si tu tardes
  res.sendStatus(200);

  if (!isPaymentComplete(req.body)) return;

  // 3. Contrôles métier — le SDK ne peut pas les faire à ta place
  const order = await findOrder(req.body.ref_command);
  if (!order) return;
  if (order.status === "paid") return;                       // idempotence
  if (Number(req.body.final_item_price) < order.amount) return; // montant

  const { userId } = decodeCustomField<{ userId: number }>(req.body.custom_field) ?? {};
  await markOrderPaid(order.id, { userId, method: req.body.payment_method });
});

verifyIpn s'appuie sur le HMAC-SHA256 calculé par PayTech : des deux méthodes documentées, c'est la seule qui lie la preuve au contenu du message. Une notification dépourvue de hmac_compute est refusée par défaut. Les comparaisons sont faites en temps constant.

À calibrer une fois. La documentation PayTech écrit le message HMAC « amount|ref_command|api_key » sans préciser quel montant. Le SDK utilise final_item_price par défaut. Si tes vérifications échouent systématiquement alors que les notifications sont légitimes, journalise une charge utile réelle et ajuste :

verifyIpn(req.body, config, { amountField: "item_price" });

Notifications sans HMAC. Si ton compte PayTech en reçoit réellement, le repli historique sur les empreintes SHA-256 des identifiants se réactive explicitement :

verifyIpn(req.body, config, { allowUnsignedLegacyHashes: true });

⚠️ Ce repli n'authentifie pas le contenu de la notification. api_key_sha256 et api_secret_sha256 sont deux constantes, identiques sur toutes les notifications de ton compte : elles ne sont liées ni au montant, ni à ref_command, ni à type_event. Une seule notification légitime qui fuite — journaux applicatifs, observabilité, capture de debug — livre ces deux empreintes de façon définitive, jusqu'à rotation de tes clés PayTech. Un attaquant peut alors forger un sale_complete portant la référence et le montant exacts d'une de ses propres commandes impayées : ni le contrôle du montant ni l'idempotence ne l'arrêtent, puisque les deux valeurs sont authentiques. Traite ce repli comme transitoire — le SDK émet un avertissement Node PAYTECH_IPN_UNSIGNED_FALLBACK la première fois qu'il authentifie une notification par ce biais.


Statut et remboursement

const status = await paytech.getStatus(payment.token!);  // le jeton, pas la référence
const refund = await paytech.refundPayment("CMD-1750000000000-abcd1234");

Gestion des erreurs

Toute réponse HTTP non-2xx lève une PayTechError portant le statut et le corps brut de la réponse. Les appels réseau expirent au bout de 15 secondes par défaut.

import { PayTechError } from "paytech-node-ts-sdk";

try {
  await paytech.getStatus(token);
} catch (err) {
  if (err instanceof PayTechError) {
    console.error(err.status, err.body);
  }
}

Options du constructeur : baseUrl (pour pointer vers un mock en test) et timeoutMs.


Les URL de retour et de notification

Trois URL se transmettent à createPayment, et elles n'obéissent pas aux mêmes règles.

ipn_url — HTTPS obligatoire, vérifié ici. PayTech rejette une adresse en HTTP (ipn_url doit etre en https). Le SDK le vérifie avant l'appel réseau pour vous éviter l'aller-retour :

await paytech.createPayment({ item_name: "Cotisation", item_price: 2000, ipn_url: "http://localhost:4000/ipn" });
// Error: PayTech : ipn_url doit être en HTTPS. Reçu : http://localhost:4000/ipn

success_url et cancel_url — publiques en HTTPS, mais non vérifiées. L'API les accepte en local sans broncher : createPayment réussit et renvoie une URL de redirection. C'est la page de paiement qui refuse, au moment où l'acheteur valide, avec ce message :

Erreur — successRedirectUrl doit être un URL

Le nom du champ est celui de PayTech, pas le nôtre : cherchez success_url dans votre code, pas successRedirectUrl. Le SDK ne les rejette pas — l'API les accepte, et lui interdire ici serait contredire le service qu'il enveloppe.

En développement, un tunnel (cloudflared tunnel --url http://localhost:3000) donne une adresse publique pour les trois.


Migration depuis la 1.2.x

Un seul changement, et il concerne la sécurité. verifyIpn refuse désormais par défaut les notifications dépourvues de hmac_compute, au lieu de retomber silencieusement sur la comparaison des empreintes SHA-256 des identifiants. Ce repli ne liant la preuve à aucun champ de la charge utile, il ne pouvait pas rester le comportement par défaut ; le détail du risque est expliqué dans Vérifier une notification IPN.

| 1.2.0 | 2.0.0 | |---|---| | verifyIpn(payload, config) accepte une notification non signée | la refuse | | requireHmac?: boolean | allowUnsignedLegacyHashes?: boolean (requireHmac déprécié, toujours accepté) | | repli silencieux | avertissement Node PAYTECH_IPN_UNSIGNED_FALLBACK à la première acceptation |

Si tu passais déjà une option, tu n'as rien à faire : requireHmac: true et requireHmac: false gardent exactement le comportement de la 1.x — la seconde reste un opt-in explicite au repli. Seul l'appel sans option change de comportement. requireHmac reste accepté sans échéance de retrait ; fournir les deux options de façon contradictoire lève une TypeError au lieu d'appliquer une règle de précédence implicite.

Comment savoir si tu es concerné. Si tes notifications portent hmac_compute — c'est le cas courant — la mise à jour est transparente. Sinon, tes vérifications se mettront à échouer : journalise une charge utile réelle pour le confirmer, puis choisis entre demander l'activation du HMAC à PayTech (recommandé) et réactiver le repli le temps de la bascule :

verifyIpn(req.body, config, { allowUnsignedLegacyHashes: true });

Le reste de l'API est inchangé.


Migration depuis la 1.1.x

Le paquet est désormais publié en CommonJS et en ESM. La 1.1.0 était ESM seule : require() n'y fonctionnait qu'à partir de Node 22.12, et Jest, qui tourne en CommonJS, ne savait pas la lire sans transpilation (transformIgnorePatterns). Ces deux contournements ne sont plus nécessaires.

| 1.1.0 | 1.2.0 | |---|---| | ESM seul | import et require | | Node ≥ 22.12 pour require() | Node ≥ 18 | | transformIgnorePatterns requis dans Jest | rien à configurer | | ipn_url en HTTP : échec après l'appel réseau | échec immédiat, message explicite |

Aucun changement d'API : les signatures sont identiques.


Migration depuis la 1.0.x

La 1.0.7 était inutilisable — un import échouait sur ERR_MODULE_NOT_FOUND — et embarquait du code applicatif. Ce qui change :

| 1.0.x | 1.1.0 | |---|---| | verifyPayment(ref_command) | getStatus(token) — l'endpoint précédent n'existait pas | | item_price: string | item_price: number | | success: boolean | success: 0 \| 1 | | env obligatoire | optionnel, "prod" par défaut | | dépend de pg et dotenv | zéro dépendance | | import { PayTech } from "paytech-node-ts-sdk/dist/paytech.js" | import { PayTech } from "paytech-node-ts-sdk" | | aucune vérification d'IPN | verifyIpn, decodeCustomField, isPaymentComplete |

Le code applicatif retiré (couche PostgreSQL, service métier) est conservé à titre d'exemple dans examples/paydev/, hors du paquet publié.


Côté navigateur

N'appelle jamais PayTech depuis le front : tes clés se retrouveraient dans le bundle JavaScript, et PayTech désactive de toute façon le CORS sur les routes authentifiées. Pour React et Next.js, utilise paytech-react-hooks, qui appelle une route de ton backend.


Licence

MIT © Adramé Diakhaté