Wix
Moyen de paiement personnalisé via Velo (per-site), sur Wix Studio.
À 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
- 1Ouvrez le site dans Wix Studio, activez le panneau de code (icône
{}). - 2Installez l'appli Wix Stores sur le site.
- 3Panneau de code, Back-end, Plugin de service, ajoutez un plugin de type Payment, nommez-le
mobupay. - 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
- 1Publiez le site, corrigez
SITE_WEBHOOK_URLavec l'URL publiée, republiez. Le backend Velo n'est actif qu'une fois le site publié : republiez après chaque modification dehttp-functions.js. - 2Tableau de bord, Recevoir des paiements, Voir plus d'options : Mobupay, Connecter, collez la clé
sk_…. - 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.