Adaptateur Chariow pour Node, paiements, licences, webhooks. Zéro dépendance, framework-agnostique (Next.js, Express, Fastify, Hono…).
Encaisse partout : Mobile Money en Afrique (MTN, Moov, Orange, Wave…), cartes et PayPal à l'international. Les moyens de paiement s'adaptent au pays de l'acheteur. Bonsoir
Tu démarres de zéro ?
npx github:phileas98/create-chariow-kit mon-appgénère un projet Next.js complet.
npm install chariow-kit
# ou, avant publication npm :
npm install github:phileas98/chariow-kitCHARIOW_API_KEY=sk_live_... # dashboard Chariow → API
PULSE_SIGNING_SECRET=whsec_... # Chariow → ton Pulse → Secret de signature
L'ID produit (prd_...) n'est pas une variable d'env : passe-le en
argument. Le produit doit être publié (sinon 404).
import { createCheckout } from "chariow-kit";
const { checkoutUrl } = await createCheckout({
productId: "prd_xxxx",
email: "client@mail.com",
firstName: "Jean",
lastName: "Dupont",
phoneNumber: "0162016549",
countryCode: "BJ",
customerIp: req.ip, // → moyens de paiement locaux
customMetadata: { userId: "u_123" }, // te revient dans le webhook
redirectUrl: "https://app.com/merci?sale={sale_id}",
});
// redirige le client vers checkoutUrlstep vaut payment (→ checkoutUrl), completed (produit gratuit) ou
already_purchased.
Le modèle : le client paie, reçoit une clé, la colle dans ton app, tu débloques.
import { validateOrActivate } from "chariow-kit";
// Dans TA route API — jamais côté navigateur (la clé sk_live_ y serait exposée)
const { valid, reason, license } = await validateOrActivate(
licenseKey,
deviceId // empreinte machine : limite le nb d'appareils
);
if (!valid) {
// reason: not_found | expired | revoked | activation_limit_reached | inactive
return Response.json({ valid: false, reason }, { status: 403 });
}
return Response.json({
valid: true,
plan: license.product?.name,
expiresAt: license.expiresAt, // null = licence à vie
devicesLeft: license.activations.remaining,
});validateOrActivate() lit la licence et l'active si c'est sa première
utilisation. Si ton produit est configuré avec requires_activation: false
(mode SaaS), les licences arrivent déjà actives — le helper le gère.
Autres fonctions : getLicense, validateLicense, activateLicense,
revokeLicense, getLicenseActivations, listLicenses.
const { activations } = await getLicenseActivations(licenseKey);
// chaque entrée : IP + pays, navigateur/plateforme, device, datePour un cours ou un contenu, sans licence :
import { hasPurchased } from "chariow-kit";
const { hasAccess } = await hasPurchased(user.email, "prd_cours");
if (!hasAccess) return Response.json({ error: "Achat requis" }, { status: 403 });handlePulse() fait tout : signature, déduplication, dispatch.
import { handlePulse } from "chariow-kit";
export async function POST(request) {
const result = await handlePulse({
rawBody: await request.text(), // corps BRUT obligatoire
headers: request.headers,
isDuplicate: (id) => db.deliveries.exists(id),
markProcessed: (id) => db.deliveries.insert(id),
on: {
"successful.sale": async (p) => {
await grantAccess(p.email, p.productId, p.sale.metadata);
},
"license.issued": async (p) => sendKeyByEmail(p.email, p.license.key),
"license.nearing_expiry": async (p) => sendRenewal(p.email, p.daysUntilExpiry),
"license.expired": async (p) => revokeAccess(p.license.key),
},
});
return new Response(result.reason ?? "OK", { status: result.status });
}// express.raw() sur CETTE route : il faut le corps brut
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const result = await handlePulse({ rawBody: req.body, headers: req.headers, on: {…} });
res.status(result.status).send("OK");
});successful.sale, abandoned.sale, failed.sale, license.issued,
license.activated, license.nearing_expiry, license.expired,
license.revoked, affiliate.joined.
Déduplique sur l'en-tête x-pulse-delivery-id, pas sur sale.id : une même
vente produit légitimement une livraison par Pulse abonné. handlePulse() s'en
charge via isDuplicate / markProcessed.
Un pulse de test n'a pas ce header (getDeliveryId() renvoie null) et
porte un champ note (pulse.isTest).
5 tentatives (10 s, 100 s, ~17 min, ~2 h 47), timeout 30 s chacune. Après le 5ᵉ échec, Chariow désactive le Pulse et t'envoie un email : il faut le réactiver dans le dashboard. Réponds vite et en 2xx.
- Clé API serveur uniquement : jamais dans le navigateur, jamais dans du code client. C'est pourquoi la validation de licence doit être server-side.
- Toujours vérifier la signature avant de livrer quoi que ce soit : ton endpoint est public, n'importe qui peut y poster.
- Ne te fie pas à la redirection après paiement : la source de vérité est le Pulse.
- Sur erreur réseau,
validateLicense()lève au lieu de renvoyervalid: falseà toi de décider (période de grâce hors ligne plutôt que couper l'accès d'un client légitime).
- Un seul produit par checkout : pas de panier multi-lignes. Pour plusieurs articles en un paiement : produit Bundle.
- Page de paiement hébergée : on redirige, on n'embarque pas la saisie.
- Types Service / Coaching / prix libre non supportés par l'API checkout.
- Endpoint Pulse en HTTPS obligatoire.
npm testMIT.