MobupayMobupay
Chargement de vos clés API…

Wix

Moyen de paiement personnalisé via Velo (per-site), sur Wix Studio.

Validé de bout en bout

À savoir avant de commencer

  • Velo n'est disponible que dans Wix Studio (studio.wix.com). L'éditeur IA classique n'expose pas le mode développeur.
  • L'installation de l'appli Wix Stores est requise (sinon le type de plugin de service « Payment » n'apparaît pas).
  • Un forfait Wix Premium est nécessaire pour accepter les paiements. En gratuit, le moyen de paiement se connecte mais le paiement en ligne est refusé.
  • Une clé API Mobupay (sk_test_… / sk_live_…). Utiliser un marchand à vrai contrat carte (pas un marchand bac à sable) pour un paiement carte réel.

1. Créer le plugin de paiement

  1. 1Ouvrez le site dans Wix Studio, activez le panneau de code (icône {}).
  2. 2Installez l'appli Wix Stores sur le site.
  3. 3Panneau de code, Back-end, Plugin de service, ajoutez un plugin de type Payment, nommez-le mobupay.
  4. 4Deux fichiers sont générés : le fichier de config et le fichier principal (voir ci-dessous).

2. Fichier de config

export function getConfig() {
  return {
    title: 'Mobupay',
    paymentMethods: [
      { hostedPage: { title: 'Carte bancaire (Mobupay)', billingAddressMandatoryFields: [] } },
    ],
    credentialsFields: [
      { simpleField: { name: 'apiKey', label: 'Clé API Mobupay (sk_live_… / sk_test_…)' } },
    ],
  };
}

3. Fichier principal

Réglez MOBUPAY_API_BASE (https://api.mobupay.nc) et SITE_WEBHOOK_URL (l'URL de votre site publié suivie de /_functions/mobupayWebhook).

import { getSecret } from 'wix-secrets-backend';

const MOBUPAY_API_BASE = 'https://api.mobupay.nc';
const SITE_WEBHOOK_URL = 'https://VOTRE-SITE/_functions/mobupayWebhook';

// Boutique Mobupay à laquelle rattacher les encaissements de CE site : son code
// (par exemple 'PAITA') ou son identifiant 'sto_...'. FACULTATIF, vide par défaut.
// Une valeur inconnue est refusée en UNKNOWN_STORE : n'y mettez jamais une valeur
// de remplissage, et ne la déduisez jamais du nom du site (un rapprochement faux
// ne fait échouer aucun paiement, il fausse seulement vos comptes).
const SITE_STORE_ID = '';

export async function connectAccount(options) {
  const apiKey = (options?.credentials?.apiKey || '').trim();
  if (!/^sk_(test|live)_[A-Za-z0-9]{16,}$/.test(apiKey)) {
    return { errorCode: 'INVALID_CREDENTIALS', errorMessage: 'Clé API Mobupay invalide.' };
  }
  return {
    credentials: { apiKey },
    accountId: apiKey.slice(0, 11),
    accountName: apiKey.startsWith('sk_live_') ? 'Mobupay (production)' : 'Mobupay (test)',
  };
}

export async function createTransaction(options) {
  const apiKey = options?.merchantCredentials?.apiKey;
  const order = options?.order || {};
  const desc = order.description || {};
  const wixTransactionId = options.wixTransactionId;
  const storeId = String(SITE_STORE_ID || '').trim();

  let token = '';
  try { token = await getSecret('mobupayWebhookToken'); } catch (e) {}
  const notificationUrl =
    SITE_WEBHOOK_URL + '?wixTransactionId=' + encodeURIComponent(wixTransactionId) +
    (token ? '&t=' + encodeURIComponent(token) : '');

  let res, data;
  try {
    res = await fetch(MOBUPAY_API_BASE + '/api/v1/payments/links', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + apiKey,
        'Idempotency-Key': wixTransactionId,
      },
      body: JSON.stringify({
        order: { reference: order._id || wixTransactionId, amount: Number(desc.totalAmount), currency: desc.currency },
        redirectUrl: order.returnUrls?.successUrl,
        notificationUrl,
        externalId: wixTransactionId,
        // storeId est un champ de PREMIER NIVEAU, jamais une clé de l'objet order :
        // le schéma de commande supprime les clés inconnues sans rien dire.
        ...(storeId ? { storeId } : {}),
      }),
    });
    data = await res.json();
  } catch (err) {
    return { pluginTransactionId: wixTransactionId, reasonCode: 3000, errorCode: 'PROVIDER_UNREACHABLE', errorMessage: 'Mobupay injoignable.' };
  }
  if (!res.ok || !data?.linkUrl) {
    return { pluginTransactionId: wixTransactionId, reasonCode: 3000, errorCode: 'PROVIDER_ERROR', errorMessage: 'Création du paiement Mobupay échouée.' };
  }
  return { pluginTransactionId: data.paymentId, redirectUrl: data.linkUrl };
}

export async function refundTransaction(options) {
  const apiKey = options?.merchantCredentials?.apiKey;
  const paymentId = options?.pluginTransactionId;
  const amount = options?.refundAmount; // unité mineure

  let res, data;
  try {
    res = await fetch(MOBUPAY_API_BASE + '/api/v1/payments/' + encodeURIComponent(paymentId) + '/refund', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + apiKey, 'Idempotency-Key': options.wixRefundId },
      // amountCents, et NON amount : l'API supprime les clés inconnues, donc un
      // amount serait perdu en silence et le remboursement partirait EN TOTALITÉ.
      body: JSON.stringify(amount ? { amountCents: amount } : {}),
    });
    data = await res.json();
  } catch (err) {
    return { pluginRefundId: options.wixRefundId, reasonCode: 3025, errorCode: 'REFUND_UNREACHABLE', errorMessage: 'Mobupay injoignable.' };
  }
  if (!res.ok) {
    return { pluginRefundId: options.wixRefundId, reasonCode: 3025, errorCode: 'REFUND_FAILED', errorMessage: 'Remboursement Mobupay refusé.' };
  }
  return { pluginRefundId: (data?.refundId || data?.id) || options.wixRefundId };
}

4. Webhook backend

Créez un fichier backend nommé exactement http-functions.js (ce nom expose les endpoints /_functions/…). Le premier endpoint confirme le paiement, le second confirme un remboursement.

Le paiement carte réel est notifié par payment.authorized, pas par payment.captured. Le code ci-dessous confirme sur les deux, et il faut qu'il en soit ainsi : un webhook qui n'écoute que payment.captured laisse la commande non confirmée en production, alors que l'acheteur a payé et a vu la page de remerciement.

import { ok, badRequest, forbidden } from 'wix-http-functions';
import wixPaymentProviderBackend from 'wix-payment-provider-backend';
import { getSecret } from 'wix-secrets-backend';

const H = { headers: { 'Content-Type': 'application/json' } };

async function tokenValid(request) {
  let expected = '';
  try { expected = await getSecret('mobupayWebhookToken'); } catch (e) {}
  if (!expected) return true;
  return request.query?.t === expected;
}

// Le flux carte RÉEL émet payment.authorized ; payment.captured vient du bac à sable
// ou d'une capture différée. Confirmez sur les DEUX : n'écouter que payment.captured
// laisse la commande non confirmée en production, alors que l'acheteur a bien payé et
// a bien vu la page de remerciement.
export async function post_mobupayWebhook(request) {
  if (!(await tokenValid(request))) return forbidden(H);
  let body;
  try { body = await request.body.json(); } catch (e) { return badRequest(H); }
  const type = body?.type;
  const data = body?.data || {};
  const wixTransactionId = data.externalId || request.query?.wixTransactionId;
  const pluginTransactionId = data.paymentId;
  if ((type === 'payment.authorized' || type === 'payment.captured')
      && wixTransactionId && pluginTransactionId) {
    await wixPaymentProviderBackend.submitEvent({
      event: { transaction: { wixTransactionId, pluginTransactionId } },
    });
  }
  return ok(H);
}

// Confirmation asynchrone d'un remboursement. Le remboursement lui-même part de
// refundTransaction ; cet endpoint le confirme à Wix quand Mobupay le notifie.
export async function post_mobupayRefundWebhook(request) {
  if (!(await tokenValid(request))) return forbidden(H);
  let body;
  try { body = await request.body.json(); } catch (e) { return badRequest(H); }
  const type = body?.type;
  const data = body?.data || {};
  const wixTransactionId = data.externalId || request.query?.wixTransactionId;
  const wixRefundId = request.query?.wixRefundId;
  if ((type === 'payment.refunded' || type === 'payment.partially_refunded')
      && wixTransactionId && wixRefundId) {
    await wixPaymentProviderBackend.submitEvent({
      event: {
        refund: {
          wixTransactionId,
          wixRefundId,
          pluginRefundId: data.refundId || data.paymentId,
          amount: data.amount,
        },
      },
    });
  }
  return ok(H);
}

5. Connecter et tester

  1. 1Publiez le site, corrigez SITE_WEBHOOK_URL avec l'URL publiée, republiez. Le backend Velo n'est actif qu'une fois le site publié : republiez après chaque modification de http-functions.js.
  2. 2Tableau de bord, Recevoir des paiements, Voir plus d'options : Mobupay, Connecter, collez la clé sk_….
  3. 3Passez une commande et payez avec la carte de test Monext 5476 4309 9999 9892 (CVV 123, expiration future).

Sur un forfait gratuit, Wix affiche « N'accepte pas les paiements » et bloque le règlement : passez le site en Premium pour finaliser un paiement réel.

Plusieurs boutiques (facultatif)

Si votre compte Mobupay compte plusieurs boutiques, la voie recommandée est de rattacher la clé API à sa boutique depuis votre espace marchand : les encaissements de ce site sont alors fléchés tout seuls et vous n'avez rien à régler ici.

Si une même clé équipe plusieurs sites Wix, renseignez SITE_STORE_ID avec le code de la boutique (par exemple PAITA) ou son identifiant sto_…, puis republiez le site. La valeur part telle quelle : une boutique inconnue est refusée en UNKNOWN_STORE, jamais ignorée. N'y mettez donc pas de valeur de remplissage, et ne la déduisez pas du nom du site : un rapprochement faux ne fait échouer aucun paiement, il fausse seulement vos comptes.