Embed Fygaro Link into Wix Checkout
Tutorial: Connecting Fygaro Payments to WIX Checkout
Disclaimer
Ensure you back up your site before implementing this integration and thoroughly test it before enabling it in production.
Connecting Fygaro with Wix to collect payments requires:
- A Fygaro subscription plan with Hook capabilities.
- A Wix account with an online store/e-commerce feature.
Step 1: Access Wix Website Editor
- Log in to your Wix dashboard.
- Open the website editor.
- * ---
Step 2: Enable Developer Mode
- In the top menu bar of the site editor, click on Dev Mode.
- Follow the activation steps to enable Developer Mode.
- * ---
Step 3: Install Required NPM Package
- Once Developer Mode is enabled, click on Packages & App in the left menu bar.
- Click the + sign next to npm and install the
jsonwebtokenpackage.
- * ---
Step 4: Create Backend Files
- In the left menu bar, select Backend and Public.
- Click the + sign under Backend, then click Add .js File.
- Name the file
fygaro.web.jsand include the following code:
// Working ample Code Block, update as needed.
import { Permissions, webMethod } from "wix-web-module";
import jwt from 'jsonwebtoken'; // Install "jsonwebtoken" package in Wix Velo settings
import crypto from "crypto";
import { fetch } from "wix-fetch";
import wixSecretsBackend from "wix-secrets-backend";
export const getFJWT = webMethod(
Permissions.Anyone,
async (api_public, api_secrete, cartTotal, taxes, currency, orderNumber) => {
const SECRET_KEY = api_secrete;
const KID = api_public;
var payload = {
amount: cartTotal,
tax: taxes,
currency: currency,
custom_reference: orderNumber,
};
if(taxes == 0){
payload = {
amount: cartTotal,
currency: currency,
custom_reference: orderNumber,
};
}
const options = {
header: { kid: KID },
algorithm: 'HS256',
};
return jwt.sign(payload, SECRET_KEY, options);
}
);
/* -----------------------------
Refund API (external payment)
-------------------------------- */
const FYGARO_BASE = "https://api.fygaro.com/api/v1/external/payment";
function to2dp(amount) {
// expects number like 12.3 -> "12.30"
return Number(amount).toFixed(2);
}
//refund function
export async function fygaroRefund({
apiKeyKid, // Fygaro API key (UUID string) -> JWT header kid
apiSecret, // Fygaro shared secret -> HS256 signing key
transactionId, // original Fygaro tx id (UUID)
amount, // optional: number (major units). If undefined => full refund
}) {
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 300; // 5 minutes
const payload = {
transactionId,
iat,
exp,
...(amount !== undefined ? { amount: to2dp(amount) } : {}),
};
const token = jwt.sign(payload, apiSecret, {
algorithm: "HS256",
header: { kid: apiKeyKid, typ: "JWT" },
});
const res = await fetch(`${FYGARO_BASE}/refund/`, {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const text = await res.text();
let data;
try { data = JSON.parse(text); } catch { data = { raw: text }; }
if (!res.ok) {
// Bubble up something useful for Wix logs
throw new Error(`Fygaro refund failed (${res.status}): ${JSON.stringify(data)}`);
}
// Expected 200: { transactionId, amount } :contentReference[oaicite:7]{index=7}
return data;
}
/* -----------------------------
Secrets Manager helpers
-------------------------------- */
function hookSecretName(keyId) {
return `fygaro_hook_secret_${keyId}`;
}
export async function getFygaroHookSecret(keyId) {
// returns the secret value by name
return wixSecretsBackend.getSecret(hookSecretName(keyId));
}
export async function upsertFygaroHookSecret(keyId, secretValue) {
const name = hookSecretName(keyId);
// listSecretInfo() returns array of { id, name, description? }
const infos = await wixSecretsBackend.listSecretInfo();
const existing = (infos || []).find((s) => s.name === name);
if (existing?.id) {
// updateSecret(secretId, { value })
await wixSecretsBackend.updateSecret(existing.id, { value: secretValue });
return { action: "updated", name };
}
// createSecret({ name, value }) — keep minimal
await wixSecretsBackend.createSecret({ name, value: secretValue });
return { action: "created", name };
}
/* -----------------------------
Webhook signature helpers
-------------------------------- */
export function normalizeHeaders(h = {}) {
const out = {};
Object.keys(h).forEach((k) => (out[String(k).toLowerCase()] = h[k]));
return out;
}
export function parseFygaroSignature(sigHeader = "") {
const parts = String(sigHeader).split(",").map((p) => p.trim());
let t = null;
const v1 = [];
for (const part of parts) {
const idx = part.indexOf("=");
if (idx === -1) continue;
const k = part.slice(0, idx).trim();
const v = part.slice(idx + 1).trim();
if (k === "t") t = v;
if (k === "v1") v1.push(v);
}
return { t, v1 };
}
export function constantTimeEqualHex(aHex, bHex) {
if (typeof aHex !== "string" || typeof bHex !== "string") return false;
if (aHex.length !== bHex.length) return false;
let diff = 0;
for (let i = 0; i < aHex.length; i++) diff |= aHex.charCodeAt(i) ^ bHex.charCodeAt(i);
return diff === 0;
}
export function computeFygaroExpectedSignature({ secret, t, rawBody }) {
const message = `${t}.${rawBody}`;
return crypto.createHmac("sha256", secret).update(message, "utf8").digest("hex");
}
- Click the + sign again, but this time select Expose Site API.
- Include the following code in the new file (https-functions.js):
// Working sample Code Block, update as needed
import { ok, badRequest, unauthorized, serverError } from 'wix-http-functions';
import crypto from "crypto";
import wixPaymentProviderBackend from "wix-payment-provider-backend";
import { getSecret } from "wix-secrets-backend";
function normalizeHeaders(h = {}) {
const out = {};
Object.keys(h).forEach((k) => (out[String(k).toLowerCase()] = h[k]));
return out;
}
function parseFygaroSignature(sigHeader = "") {
const parts = String(sigHeader).split(",").map((p) => p.trim());
let t = null;
const v1 = [];
for (const part of parts) {
const idx = part.indexOf("=");
if (idx === -1) continue;
const k = part.slice(0, idx).trim();
const v = part.slice(idx + 1).trim();
if (k === "t") t = v;
if (k === "v1") v1.push(v);
}
return { t, v1 };
}
// Constant-time compare for hex strings (no timingSafeEqual dependency)
function constantTimeEqualHex(aHex, bHex) {
if (typeof aHex !== "string" || typeof bHex !== "string") return false;
if (aHex.length !== bHex.length) return false;
let diff = 0;
for (let i = 0; i < aHex.length; i++) {
diff |= aHex.charCodeAt(i) ^ bHex.charCodeAt(i);
}
return diff === 0;
}
//Hook URL> https://www.mysite.com/_functions/updateTransaction
// or https://user.wix.com/_functions/updateTransaction
export async function post_updateTransaction(request) {
try {
const headers = normalizeHeaders(request.headers);
// ✅ RAW BODY (must be exact bytes)
const buf = await request.body.buffer();
const rawBody = buf.toString("utf8");
const sigHeader = headers["fygaro-signature"];
const keyId = headers["fygaro-key-id"];
if (!sigHeader || !keyId) {
console.log("Missing headers:", { hasSig: !!sigHeader, hasKeyId: !!keyId });
return badRequest({ body: { error: "Missing Fygaro-Signature or Fygaro-Key-ID" } });
}
// 🔐 Secrets Manager: create secrets like "fygaro_hook_secret_<keyId>"
const secretName = `fygaro_hook_secret_${keyId}`;
const secret = await getSecret(secretName);
if (!secret) {
console.log("Unknown keyId (no secret found):", { keyId, secretName });
return unauthorized({ body: { error: "Unknown Fygaro-Key-ID (no matching secret configured)" } });
}
const { t, v1 } = parseFygaroSignature(sigHeader);
if (!t || !v1.length) {
console.log("Malformed signature header:", { sigHeaderPreview: sigHeader.slice(0, 40) });
return badRequest({ body: { error: "Malformed Fygaro-Signature header" } });
}
// Replay window (recommended by Fygaro)
const now = Math.floor(Date.now() / 1000);
const ts = parseInt(t, 10);
if (!Number.isFinite(ts) || Math.abs(now - ts) > 300) {
console.log("Stale timestamp:", { ts, now, driftSec: Math.abs(now - ts) });
return unauthorized({ body: { error: "Stale timestamp" } });
}
// Compute HMAC as per Fygaro doc: message = t + "." + raw_body
const message = `${t}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(message, "utf8").digest("hex");
const valid = v1.some((sig) => constantTimeEqualHex(expected, sig));
if (!valid) {
console.log("Invalid signature:", {
keyId,
t,
v1Count: v1.length,
bodyLen: rawBody.length,
});
return unauthorized({ body: { error: "Invalid signature" } });
}
// ✅ Verified — now parse JSON
const payload = JSON.parse(rawBody);
console.log("Verified Fygaro payload keys:", Object.keys(payload || {}));
// New payload contains transactionId + customReference :contentReference[oaicite:1]{index=1}
if (!payload?.customReference || !payload?.transactionId) {
return badRequest({ body: { error: "Missing customReference or transactionId in payload" } });
}
await wixPaymentProviderBackend.submitEvent({
event: {
transaction: {
wixTransactionId: payload.customReference,
pluginTransactionId: payload.transactionId,
},
},
});
return ok({ body: { status: "ok" } });
} catch (err) {
console.error("Webhook processing failed:", err?.stack || err);
return serverError({ body: { error: String(err?.message || err) } });
}
}
- * ---
Step 5: Configure Payment Plugin
- Navigate to the Service Plugins section.
- Click the + sign and select the Payments option.
- When prompted for a name, enter
FGW(all uppercase).
- * ---
Step 6: Edit FGW Configuration Files
- Locate the created files and open
FGW-config.js. - Copy and paste the following code into the file:
// Working sample Code Block, update as needed
import * as paymentProvider from 'interfaces-psp-v1-payment-service-provider';
/** @returns {import('interfaces-psp-v1-payment-service-provider').PaymentServiceProviderConfig} */
export function getConfig() {
//throw new Error('getConfig was not implemented');
return {
title: 'Fygaro Payments',
paymentMethods: [{
hostedPage: {
title: 'Fygaro Payments',
logos: {
white: {
svg: 'https://static-app.fygaro.com/static/img/10542c9bde49a22d7779.svg'
//png: 'https://freesvg.org/img/15930333081593032446pitr_Bananas_icon.png'
},
colored: {
svg: 'https://static-app.fygaro.com/static/img/10542c9bde49a22d7779.svg'
//png: 'https://freesvg.org/img/15930333081593032446pitr_Bananas_icon.png'
}
}
}
}],
credentialsFields: [{
simpleField: {
name: 'clientId',
label: 'API id'
}
},
{
simpleField: {
name: 'clientSecret',
label: 'API secret'
}
},
{
simpleField: {
name: 'clientURL',
label: 'Botton URL'
}
},
{
dropdownField: {
name: 'taxFlag',
label: 'Send Tax Detail',
options: [
{
key: '0',
value: 'No'
},
{
key: '1',
value: 'Yes'
}
]
}
}
]
}
}
- Open the
FGW.jsfile and paste the following code:
// Working sample Code Block, update as needed
import * as paymentProvider from 'interfaces-psp-v1-payment-service-provider';
import { getFJWT, fygaroRefund, upsertFygaroHookSecret, createNewSecret } from 'backend/fygaro.web';
/**
* This payment plugin endpoint is triggered when a merchant provides required data to connect their PSP account to a Wix site.
* The plugin has to verify merchant's credentials, and ensure the merchant has an operational PSP account.
* @param {import('interfaces-psp-v1-payment-service-provider').ConnectAccountOptions} options
* @param {import('interfaces-psp-v1-payment-service-provider').Context} context
* @returns {Promise<import('interfaces-psp-v1-payment-service-provider').ConnectAccountResponse | import('interfaces-psp-v1-payment-service-provider').BusinessError>}
*/
export const connectAccount = async (options, context) => {
const { credentials } = options;
const keyId = credentials?.clientId;
const secret = credentials?.clientSecret;
if (keyId && secret) {
try {
await upsertFygaroHookSecret(keyId, secret);
} catch (e) {
console.log("Fygaro secret upsert failed:", e?.message || e);
}
}
return { credentials };
};
/**
* This payment plugin endpoint is triggered when a buyer pays on a Wix site.
* The plugin has to process this payment request but prevent double payments for the same `wixTransactionId`.
* @param {import('interfaces-psp-v1-payment-service-provider').CreateTransactionOptions} options
* @param {import('interfaces-psp-v1-payment-service-provider').Context} context
* @returns {Promise<import('interfaces-psp-v1-payment-service-provider').CreateTransactionResponse | import('interfaces-psp-v1-payment-service-provider').BusinessError>}
*/
export const createTransaction = async (options, context) => {
const {merchantCredentials, order, wixTransactionId} = options;
const api_public = merchantCredentials.clientId;
const api_secrete = merchantCredentials.clientSecret;
const redirect_url = merchantCredentials.clientURL;
const taxFlag = merchantCredentials.taxFlag;
//Check for taxes
var taxes = 0;
if(taxFlag == 1){
if(order.description.charges.tax !== undefined && order.description.charges.tax > 0){
taxes = parseInt(order.description.charges.tax) / Math.pow(10,2);
}
}
var totalAmount = parseInt(order.description.totalAmount) / Math.pow(10,2);
const currency = order.description.currency;
const orderNumber = wixTransactionId;
const fygaroCheckout = await getFJWT(api_public, api_secrete, totalAmount, taxes, currency, orderNumber);
return {
"pluginTransactionId": orderNumber,
"redirectUrl": `${redirect_url}?jwt=${fygaroCheckout}`
}
};
/**
* This payment plugin endpoint is triggered when a merchant refunds a payment made on a Wix site.
* The plugin has to process this refund request but prevent double refunds for the same `wixRefundId`.
* @param {import('interfaces-psp-v1-payment-service-provider').RefundTransactionOptions} options
* @param {import('interfaces-psp-v1-payment-service-provider').Context} context
* @returns {Promise<import('interfaces-psp-v1-payment-service-provider').CreateRefundResponse | import('interfaces-psp-v1-payment-service-provider').BusinessError>}
*/
function minorToMajor(minor) {
// Wix often provides amounts as minor units (e.g. cents).
// If you already get major units, remove this conversion.
return Number(minor) / 100;
}
export const refundTransaction = async (options, context) => {
const { merchantCredentials, pluginTransactionId, refund, wixRefundId } = options;
const api_public = merchantCredentials.clientId; // kid
const api_secrete = merchantCredentials.clientSecret; // HS256 secret
// 1) This should be the original Fygaro transactionId you saved as pluginTransactionId
const fygaroTransactionId = pluginTransactionId;
// 2) Refund amount
// If Wix provides minor units, convert. If it provides major, pass directly.
// If you want "full refund" behavior, pass undefined when refund amount is not specified.
const refundAmountMajor =
refund?.amount !== undefined ? minorToMajor(refund.amount) : undefined;
// 3) Call Fygaro refund API
const result = await fygaroRefund({
apiKeyKid: api_public,
apiSecret: api_secrete,
transactionId: fygaroTransactionId,
amount: refundAmountMajor,
});
// 4) Return back to Wix (shape may vary slightly depending on your PSP interface version)
return {
pluginRefundId: result.transactionId, // refund transaction id from Fygaro
};
};
- Click on the Publish button in the top-right corner of the editor.
Step 7: Connect Fygaro Payments
- Return to the Wix Dashboard.
- Go to Settings and locate the Accept Payments option.
- Scroll through the payment options to find Fygaro and click on Connect.
- Enter your API Key and API Secret obtained from Fygaro (refer to this tutorial to learn how to obtain your API keys).
- In the Button URL field, paste the Fygaro Payment Button link (refer to this tutorial to learn how to create a variable amount payment button).
- Click Connect.
Step 8: Configure Your Payment Button in Fygaro
When creating your payment button in Fygaro:
- Activate the JWT option in Advanced Settings.
- In the Hook field, enter your Wix site URL followed by
/_functions/updateTransaction. Example:
https://www.mysite.com/_functions/updateTransaction
- Save your payment button.
Final Step: Test Your Integration
To ensure everything is set up correctly:
- Run a test transaction of USD $1 (or the local currency equivalent).
- Confirm that the payment processes successfully.
Now you're ready to start accepting payments with Fygaro on your Wix site!
Updated on: 01/14/2026
Thank you!
