ShopifyShopify Admin API 2024-10 · Custom App · Node 18+ · @moncashconnect/sdk

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.

Sandbox

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

MonCashConnect n'est pas une passerelle Shopify approuvée. Il n'apparaîtra jamais dans Settings → Payments → Add payment method. Le seul moyen propre d'accepter MonCash sur Shopify est de créer une app custom qui crée des Draft Orders et redirige le client vers MonCashConnect.

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

  1. Le client ajoute des produits au panier sur votre thème Shopify.
  2. Au lieu d'aller au checkout, il clique "Payer avec MonCash".
  3. Le bouton POST le contenu du panier (line items) à votre backend.
  4. Le backend crée une Draft Order dans Shopify (status open).
  5. Le backend appelle POST /pay-create avec referenceId = draft_order.id.
  6. Le backend renvoie paymentUrl au navigateur — qui redirige vers MonCashConnect.
  7. 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

  1. Dans Shopify Admin : Settings → Apps and sales channels → Develop apps → Create an app. Nommez-la MonCash Gateway.
  2. Onglet Configuration → Admin API access scopes, activez :
    • write_draft_orders
    • read_draft_orders
    • read_orders
    • read_products
  3. 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.
Notez aussi le shop domain (ex : votre-boutique.myshopify.com), il est requis dans toutes les requêtes Admin API.

Backend Node — Express + SDK

Installation :

npm install express dotenv @moncashconnect/sdk

Fichier .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.com

Fichier 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>
Pour personnaliser 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/moncash

Activez les événements payment.completed et payment.failed.

La signature HMAC est calculée sur le body brut. Si vous changez de framework côté backend, vérifiez qu'aucun middleware ne parse le JSON avant la vérification HMAC.

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 ?+
Non. Shopify ne référence pas MonCash dans sa liste de payment providers. L'intégration passe nécessairement par une app custom + l'API Draft Orders. C'est légal et conforme aux Terms of Service Shopify tant que les commandes sont bien créées dans Shopify Admin.
Pourquoi pas Shopify Payments Custom ?+
Le 'Manual payment method' apparaît bien dans Shopify Checkout mais bloque l'utilisateur sur une page d'instructions — il ne permet pas de rediriger automatiquement vers une URL externe comme paymentUrl. Le pattern Draft Order est plus fluide pour le client.
Puis-je utiliser le checkout Shopify standard plutôt qu'un bouton custom ?+
Pas directement. Shopify Checkout est en partie verrouillé (sauf Shopify Plus + Checkout Extensibility). Le pattern documenté ici remplace le checkout standard par une page de paiement MonCash externe, puis recrée la commande en Shopify une fois payée.
Comment gérer les frais de livraison et taxes ?+
Calculez-les côté backend avant la création de la Draft Order. Shopify renvoie le total dans la réponse — utilisez ce total comme amount pour /pay-create afin que le montant MonCash corresponde exactement à la commande.
Faut-il un compte marchand Digicel séparé ?+
Non. MonCashConnect est une passerelle sur l'infrastructure Bazik — une seule inscription suffit pour encaisser les paiements MonCash de votre boutique Shopify.
Que faire si le webhook arrive en double ?+
Vérifiez l'état de la Draft Order avant de la compléter. L'endpoint /draft_orders/{id}/complete.json retourne 422 si elle est déjà completed — votre code doit gérer ce cas comme un succès idempotent.