Accepter MonCash sur Shopify
MonCash n'est pas un payment provider officiel Shopify. Ce guide décrit le pattern Draft Order + Redirect : seule voie propre et conforme pour encaisser des paiements MonCash sur une boutique Shopify en Haïti.
Commencez en Sandbox
Avant d'implémenter le code ci-dessous avec votre clé live (sk_proj_…), testez d'abord en mode Sandbox avec sk_test_proj_…. Le code est identique — vous ne changez que la valeur de la variable d'environnement. Lisez le Guide Sandbox avant de continuer.
À lire avant de commencer
Ce que ce pattern donne :
- Les commandes apparaissent normalement dans Shopify Admin une fois payées.
- Inventaire, fulfillment, taxes, expédition : tout reste géré par Shopify.
- Pas de modification du checkout natif Shopify (donc compatible Basic, Shopify, Advanced).
Ce qu'il ne fait pas :
- Pas d'intégration dans le checkout 3-étapes natif. Le client clique sur un bouton custom AVANT le checkout standard.
- Pas de Shop Pay ni d'auto-fill des coordonnées (ce serait possible uniquement via Shopify Plus + Checkout Extensibility).
Architecture Draft Order + Redirect
- Le client ajoute des produits au panier sur votre thème Shopify.
- Au lieu d'aller au checkout, il clique "Payer avec MonCash".
- Le bouton POST le contenu du panier (line items) à votre backend.
- Le backend crée une Draft Order dans Shopify (status
open). - Le backend appelle
POST /pay-createavecreferenceId = draft_order.id. - Le backend renvoie
paymentUrlau navigateur — qui redirige vers MonCashConnect. - Au webhook
payment.completed, le backend appelle/draft_orders/{id}/complete.json— Shopify convertit la draft en commande payée.
Prérequis
- Une boutique Shopify (Basic ou supérieur)
- Accès administrateur pour créer une custom app
- Un backend Node.js déployé (Render, Railway, Fly, etc.)
- Un projet MonCashConnect avec clé secrète et webhook secret — créer un projet
Créer la custom app Shopify
- Dans Shopify Admin : Settings → Apps and sales channels → Develop apps → Create an app. Nommez-la MonCash Gateway.
- Onglet Configuration → Admin API access scopes, activez :
write_draft_ordersread_draft_ordersread_ordersread_products
- Cliquez Install app dans l'onglet API credentials, puis copiez le Admin API access token (commence par
shpat_) — il n'est affiché qu'une fois.
votre-boutique.myshopify.com), il est requis dans toutes les requêtes Admin API.Backend Node — Express + SDK
Installation :
npm install express dotenv @moncashconnect/sdkFichier .env :
MCC_SECRET_KEY=sk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MCC_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SHOPIFY_DOMAIN=votre-boutique.myshopify.com
SHOPIFY_ADMIN_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_URL=https://api.votresite.comFichier server.js :
import "dotenv/config";
import express from "express";
import { MonCashClient, constructEvent, MonCashError } from "@moncashconnect/sdk";
const app = express();
const client = new MonCashClient(process.env.MCC_SECRET_KEY);
// Helper Shopify Admin API
async function shopify(path, init = {}) {
const r = await fetch(`https://${process.env.SHOPIFY_DOMAIN}/admin/api/2024-10${path}`, {
...init,
headers: {
"X-Shopify-Access-Token": process.env.SHOPIFY_ADMIN_TOKEN,
"Content-Type": "application/json",
...(init.headers ?? {}),
},
});
if (!r.ok && r.status !== 422) {
throw new Error(`Shopify ${r.status}: ${await r.text()}`);
}
return { status: r.status, body: await r.json() };
}
// JSON sauf pour le webhook
app.use((req, res, next) => {
if (req.path === "/webhooks/moncash") return next();
express.json()(req, res, next);
});
// ── POST /shopify/start-payment ───────────────────────────────────────
// Reçoit { line_items, customer } depuis le bouton storefront
app.post("/shopify/start-payment", async (req, res) => {
const { line_items, customer } = req.body ?? {};
if (!Array.isArray(line_items) || line_items.length === 0) {
return res.status(400).json({ error: "line_items requis" });
}
try {
// 1) Créer la draft order Shopify
const draft = await shopify("/draft_orders.json", {
method: "POST",
body: JSON.stringify({
draft_order: {
line_items,
customer,
tags: "moncash",
note: "Paiement via MonCashConnect",
},
}),
});
const draftOrder = draft.body.draft_order;
const totalHTG = Math.round(Number(draftOrder.total_price));
// 2) Créer le paiement MonCash
const payment = await client.createPayment(totalHTG, String(draftOrder.id), {
returnUrl: `${process.env.APP_URL}/shopify/return`,
customerName: customer?.first_name
? `${customer.first_name} ${customer.last_name ?? ""}`.trim()
: undefined,
customerEmail: customer?.email,
});
res.json({ paymentUrl: payment.paymentUrl });
} catch (err) {
if (err instanceof MonCashError) {
return res.status(err.statusCode).json({ error: err.message });
}
res.status(500).json({ error: String(err) });
}
});
// ── GET /shopify/return ───────────────────────────────────────────────
// Page de retour — affiche un statut basique
app.get("/shopify/return", async (req, res) => {
const ref = String(req.query.ref ?? "");
if (!ref) return res.status(400).send("Référence manquante");
const tx = await client.getPaymentStatus(ref).catch(() => null);
res.send(`<h1>Statut : ${tx?.status ?? "inconnu"}</h1><p>Votre commande sera marquée payée dès confirmation MonCash.</p>`);
});
// ── POST /webhooks/moncash ────────────────────────────────────────────
app.post(
"/webhooks/moncash",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const event = constructEvent(
req.body,
req.headers["x-mcc-signature"] ?? "",
req.headers["x-mcc-timestamp"] ?? "",
process.env.MCC_WEBHOOK_SECRET,
);
if (event.event === "payment.completed") {
// reference = draftOrder.id
const r = await shopify(
`/draft_orders/${event.reference}/complete.json?payment_pending=false`,
{ method: "PUT" },
);
// 422 = déjà complete, on traite comme succès idempotent
if (r.status !== 200 && r.status !== 422) {
throw new Error(`Shopify complete failed: ${r.status}`);
}
}
// payment.failed : on laisse la draft order ouverte
res.sendStatus(200);
} catch (err) {
if (err instanceof MonCashError) {
return res.status(err.statusCode).send(err.message);
}
console.error(err);
res.sendStatus(500);
}
},
);
app.listen(process.env.PORT ?? 3001);Bouton storefront (thème Liquid)
Dans sections/cart-template.liquid (ou dans une section dédiée), ajoutez le bouton :
<button type="button" id="pay-moncash" class="btn btn--primary">
Payer avec MonCash
</button>
<script>
document.getElementById("pay-moncash").addEventListener("click", async () => {
const cart = await fetch("/cart.js").then((r) => r.json());
const line_items = cart.items.map((it) => ({
variant_id: it.variant_id,
quantity: it.quantity,
}));
const res = await fetch("https://api.votresite.com/shopify/start-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ line_items, customer: null }),
});
if (!res.ok) {
alert("Erreur lors de l'initiation du paiement.");
return;
}
const { paymentUrl } = await res.json();
window.location.assign(paymentUrl);
});
</script>customer, utilisez l'objet Liquid {{ customer | json }} si l'utilisateur est connecté à son compte Shopify.Configurer le webhook MonCashConnect
Dans votre tableau de bord MonCashConnect, ajoutez l'URL publique de votre backend Node :
https://api.votresite.com/webhooks/moncashActivez les événements payment.completed et payment.failed.
Liste de contrôle avant production
Le scope Admin API write_draft_orders est bien activé sur l'app Shopify custom
SHOPIFY_ADMIN_TOKEN et MCC_SECRET_KEY sont en variables d'environnement, jamais dans le thème Liquid
L'URL https://api.votresite.com/webhooks/moncash est configurée dans MonCashConnect
Le handler webhook traite 422 comme succès idempotent (draft order déjà complétée)
Vous avez testé un parcours complet en Sandbox (sk_test_proj_…) avant la production
Le bouton 'Payer avec MonCash' est visible sur la page panier (cart), pas seulement sur les fiches produit
Les totaux Shopify (taxes, livraison) sont reflétés dans le montant envoyé à MonCashConnect
Vous avez décidé d'une politique sur les Draft Orders non payées (auto-archive après X heures via un cron)
FAQ
MonCashConnect est-il une passerelle Shopify officielle ?+
Pourquoi pas Shopify Payments Custom ?+
Puis-je utiliser le checkout Shopify standard plutôt qu'un bouton custom ?+
Comment gérer les frais de livraison et taxes ?+
Faut-il un compte marchand Digicel séparé ?+
Que faire si le webhook arrive en double ?+
Lectures recommandées :