ROKIConnect
Connectors Start API SDKs Migrate MCP Portal ES

Charging on Wix with ROKI

Wix does not let anyone publish a payment gateway without its approval, so there is nothing to install in one click. What you can do today is paste this code into your site with Velo and charge by card.

First: you may not need this

If what you sell has a fixed price - a ticket, a consultation, a membership - the payment button solves it in five minutes without writing a line of code. This recipe is for the other case: when the amount depends on what the visitor chooses and has to be calculated on the spot.

Which one is yours

The amount is always the samePayment button. No code.
The visitor picks quantity or optionsThis recipe. The amount is calculated in the backend.
You have a Wix store with a cartNeither one yet: the Wix cart only accepts gateways Wix has approved. Write to us and we will tell you when that opens.

Why the amount lives in the backend and not on the page

Anyone can open the browser console and change anything on your page, a number included. If the price is calculated there, someone can pay L 1.00 for something worth L 1,000.00 and the charge goes through perfectly. That is why the calculation file is a .jsw, which in Velo runs on the Wix server: the visitor sends what they picked, not what it costs.

Before charging for real

Run the whole flow with an sk_test_ key and a small amount, and do not call it done until you see the webhook reach you: that is the only thing that confirms a charge. If the crypto import gives you trouble on your Wix plan, install crypto-js from the editor package manager and use its HMAC - the rest of the code does not change.

How it is done

  1. In the Wix editor turn on Velo (top right, "Dev Mode").
  2. In the left panel, under Backend, create a file called roki.jsw and paste the first block of code.
  3. Under Backend as well, create http-functions.js and paste the second block: that is what receives ROKI's confirmation.
  4. Store your secret key in Settings → Secrets Manager under the name ROKI_SECRET_KEY, and the webhook signing secret as ROKI_WEBHOOK_SECRET. Never write it inside the code: page code can be read by anyone.
  5. On the page where the button goes, paste the third block into that page's code panel, and adjust the element names to yours.
  6. Publish the site. In the ROKI portal register the webhook URL, which will be https://YOURSITE.com/_functions/rokiWebhook, and copy the signing secret into the Secrets Manager.
  7. Test with an sk_test_ key before charging for real.

1. The backend that creates the payment

Save it as backend/roki.jsw. The .jsw matters: it is what makes this function run on the Wix server and not in your customer's browser.

// backend/roki.jsw
// Corre en el servidor de Wix. El navegador NUNCA manda el monto: manda que eligio.
import { fetch } from 'wix-fetch';
import { getSecret } from 'wix-secrets-backend';

// El catalogo vive aca, del lado del servidor. Si tenes muchos productos, esto puede salir
// de una coleccion de Wix - lo que no puede es salir del navegador.
const PRECIOS = {
  entrada_general: 250,
  entrada_vip: 600,
};

export async function crearPago(articulo, cantidad) {
  const precio = PRECIOS[articulo];
  if (!precio) throw new Error('Articulo desconocido');

  const unidades = Math.min(Math.max(parseInt(cantidad, 10) || 1, 1), 20);
  const monto = precio * unidades;

  const llave = await getSecret('ROKI_SECRET_KEY');
  const referencia = 'wix-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);

  // Vence en 48 horas. La API lee las fechas en hora de Honduras (UTC-6), no en UTC.
  const vence = new Date(Date.now() + 48 * 3600 * 1000 - 6 * 3600 * 1000)
    .toISOString().slice(0, 19).replace('T', ' ');

  const r = await fetch('https://aura.roki.systems/api/connect/v1/payments', {
    method: 'post',
    headers: {
      'Authorization': 'Bearer ' + llave,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      // Reintentar no crea un segundo cobro: con la misma llave, ROKI devuelve el mismo pago.
      'Idempotency-Key': referencia,
    },
    body: JSON.stringify({
      amount: monto,
      external_reference: referencia,
      name: articulo + ' x' + unidades,
      expires_at: vence,
      metadata: { articulo, unidades: String(unidades) },
    }),
  });

  const datos = await r.json();
  if (r.status !== 201 && r.status !== 200) {
    console.error('ROKI respondio ' + r.status, datos);
    throw new Error('No se pudo crear el pago');
  }

  // Si ROKI nombra campos que ignoro, es un nombre equivocado nuestro. Aparece en el log.
  if (datos.warnings && datos.warnings.length) {
    console.warn('ROKI ignoro campos:', datos.warnings);
  }

  return { url: datos.checkout_url, referencia };
}

2. The webhook, which is what confirms

Save it as backend/http-functions.js. The visitor coming back to your page means their browser came back, not that the money moved: the only thing that confirms a charge is this webhook, with the signature verified.

// backend/http-functions.js
// La URL queda en https://TUSITIO.com/_functions/rokiWebhook
import { ok, badRequest } from 'wix-http-functions';
import { getSecret } from 'wix-secrets-backend';
import { createHmac, timingSafeEqual } from 'crypto';

export async function post_rokiWebhook(request) {
  // El cuerpo CRUDO. Volver a serializar el JSON cambia el orden de las claves y la firma no
  // coincide nunca: es el error mas comun al escribir un webhook.
  const crudo = await request.body.text();
  const cabecera = request.headers['roki-signature'] || '';
  const secreto = await getSecret('ROKI_WEBHOOK_SECRET');

  const m = /t=(\d+)\s*,\s*v1=([a-f0-9]{64})/i.exec(cabecera);
  if (!m) return badRequest({ body: 'firma ausente' });

  // Una entrega firmada hace tres dias no sirve para repetir un cobro.
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) {
    return badRequest({ body: 'firma vencida' });
  }

  const esperada = createHmac('sha256', secreto).update(m[1] + '.' + crudo).digest('hex');
  // En tiempo constante: comparar byte a byte filtra la firma por lo que tarda en fallar.
  const a = Buffer.from(esperada), b = Buffer.from(m[2].toLowerCase());
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return badRequest({ body: 'firma invalida' });
  }

  const evento = JSON.parse(crudo);
  // El campo es 'type'. El cuerpo es { id, type, created_at, data }.
  if (evento.type !== 'payment.approved') {
    return ok({ body: 'ignorado' });
  }

  const pago = evento.data || {};
  // Aca va lo tuyo: marcar la reserva, mandar el correo, apuntar la venta.
  // El identificador que conviene guardar es transaction_id (UUID), que es el que despues
  // aceptan anular y reembolsar - no el id numerico.
  console.log('Pagado', pago.external_reference, pago.transaction_id, pago.amount);

  // Siempre 200, y rapido: ROKI reintenta hasta que le contesten.
  return ok({ body: 'ok' });
}

3. The button on your page

It goes in the page's code panel. Change #botonPagar and #cantidad to whatever your elements are called in the editor.

// Panel de codigo de la pagina
import { crearPago } from 'backend/roki';
import wixLocation from 'wix-location';

$w.onReady(() => {
  $w('#botonPagar').onClick(async () => {
    $w('#botonPagar').disable();
    try {
      // Se manda QUE eligio, nunca cuanto cuesta.
      const { url } = await crearPago('entrada_general', $w('#cantidad').value);
      wixLocation.to(url);
    } catch (e) {
      console.error(e);
      $w('#botonPagar').enable();
    }
  });
});

Worth knowing

It is installed per siteIt is not a plugin: it is your own code inside your own site. If you run several Wix sites, it goes into each one.
Wix charges for its part separatelyVelo comes with Wix Premium plans. A free site has no backend, and without a backend this recipe does not apply: there, use the payment button.
The secret key never goes in the codeWix page code can be read by anyone who knows where to look. That is why it lives in the Secrets Manager, which only the backend reads.
The catalogue lives in the backendIn the example the prices are in a constant inside the .jsw file. If you have many, store them in a Wix collection and read them from there - what does not change is that the price comes from the server.
If Wix opens its APIWhen that door opens we will publish the real connector, one-click and code-free. This recipe will keep working the same.