Widget integration

Embed the payment widget

Drop the rezolvepay.js SDK onto your site with a single <script> tag, mint a one-time widget token on your server, then hand it to RezolvePay.mount(container, { widgetToken }). That's the whole integration. Your merchant API key never leaves your server.

Step 1: Mint a one-time widget token on your backend

From your server, POST /api/v1/checkout/init with your merchant credentials and the order details (amount, currency, order_id). The response contains a single-use widget_token (valid 15 minutes) that already encodes those values.

Step 2: Render the page with the token

Inject the widget_token into your page's render context (template variable, hydrated state, etc.). It's a single-use credential — never log it, never reuse it.

Step 3: Mount the widget

Add the SDK <script> tag, then call RezolvePay.mount(container, { widgetToken, onSuccess, onContinueToShop }). The shopper enters their email, picks card (or a saved card on return), and pays.

onSuccess({ paymentId }) fires immediately when the payment is confirmed — use this to update your order state or trigger server-side fulfilment. The widget remains visible.

onContinueToShop() fires when the shopper clicks Continue to shop on the success screen — use this to navigate away or unmount the widget.

Security: Never include your merchant API key in client-side code. The SDK doesn't need it — only the widget_token minted server-side. Each token is single-use and expires in 15 minutes.

Script tag with SRI

<script
  src="https://secure.pay.rezolve.com/widget/v1/rezolvepay.js"
  integrity="sha384-2hRiRVi3l9lbPGALjos5p52GU1p50YayAvxE0ASktedAs2Rq9soTFmNV4F/uFReL"
  crossorigin="anonymous"
></script>

Load the script from this host exactly as shown — it is not the same host as your API calls. A passkey belongs to one domain, and the loader creates the checkout on whichever host it was loaded from, so a script tag pointing anywhere else saves the shopper's passkey against the wrong domain and we can never recognise her again. Nothing errors when that happens, which is why the host matters. Every other example on this page keeps using your API base URL.

Mount the widget

HTML

<!-- 1. Load the SDK (see "Script tag with SRI" above) -->
<!-- 2. Mount point for the widget -->
<div id="rezolve-pay-container"></div>

<script>
  // widgetToken is rendered into the page by your server
  // (see the Node.js / cURL example below).
  RezolvePay.mount("#rezolve-pay-container", {
    widgetToken: "WIDGET_TOKEN_FROM_SERVER",
    customerEmail: "[email protected]", // optional pre-fill

    // Fires immediately when payment is confirmed (notification hook).
    // Update your order state here; the widget stays visible.
    onSuccess: ({ paymentId }) => {
      console.log("Payment confirmed:", paymentId);
    },

    // Fires when the shopper clicks "Continue to shop" (navigation hook).
    // Navigate away or unmount the widget here.
    onContinueToShop: () => {
      window.location.href = "/order-confirmed";
    },

    onFailure: ({ message }) => { console.error(message); },
    onCancel:  () => { /* shopper closed the widget */ },
  });
</script>

TypeScript

// Type definitions (until @rezolvepay/sdk ships):
declare global {
  interface Window {
    RezolvePay: {
      mount(
        selector: string | Element,
        opts: {
          widgetToken: string;
          customerEmail?: string;
          /** Fires immediately when payment is confirmed. Widget stays visible. */
          onSuccess?: (r: { paymentId: string }) => void;
          /** Fires when the shopper clicks "Continue to shop". Use for navigation. */
          onContinueToShop?: () => void;
          onFailure?: (r: { message: string }) => void;
          onCancel?:  () => void;
        },
      ): { unmount: () => void } | null;
    };
  }
}

export function mountCheckout(
  containerId: string,
  widgetToken: string,
  customerEmail?: string,
) {
  return window.RezolvePay.mount(`#${containerId}`, {
    widgetToken,
    customerEmail,
    onSuccess: ({ paymentId }) => {
      console.log("Payment confirmed:", paymentId);
    },
    onContinueToShop: () => {
      window.location.href = "/order-confirmed";
    },
    onFailure: ({ message }) => console.error(message),
  });
}

React

import { useEffect, useRef } from "react";

interface CheckoutWidgetProps {
  /** Single-use widget token minted by YOUR backend via POST /api/v1/checkout/init */
  widgetToken: string;
  customerEmail?: string;
  /** Fires immediately when payment is confirmed. Widget stays visible. */
  onSuccess?: (paymentId: string) => void;
  /** Fires when the shopper clicks "Continue to shop". Use for navigation. */
  onContinueToShop?: () => void;
  onFailure?: (message: string) => void;
}

export function CheckoutWidget({
  widgetToken, customerEmail, onSuccess, onContinueToShop, onFailure
}: CheckoutWidgetProps) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current || !window.RezolvePay) return;
    const handle = window.RezolvePay.mount(containerRef.current, {
      widgetToken,
      customerEmail,
      onSuccess: ({ paymentId }) => onSuccess?.(paymentId),
      onContinueToShop: () => onContinueToShop?.(),
      onFailure: ({ message }) => onFailure?.(message),
    });
    // Return cleanup to prevent listener leaks if the component unmounts.
    // onSuccess fires immediately (notification hook), not on unmount, so widget
    // stays visible until onContinueToShop (shopper-triggered navigation).
    return () => handle?.unmount?.();
  }, [widgetToken, customerEmail, onSuccess, onContinueToShop, onFailure]);

  return <div ref={containerRef} />;
}

// Add the <script src=".../widget/v1/rezolvepay.js"> tag to your index.html
// (see "Script tag with SRI" above) so window.RezolvePay is defined at runtime.

Server-side: mint a widget token

Run this on your backend. The X-Merchant-Api-Key never leaves your server. Return the resulting widget_token to your fetchWidgetToken SDK callback.

Node.js

// Server-side: mint a widget token, then render the page with it.
// Your merchant API key never reaches the browser.

import express from "express";

const app = express();
app.use(express.json());

app.get("/checkout/:orderId", async (req, res) => {
  const order = await loadOrder(req.params.orderId);  // your own logic

  const upstream = await fetch(`https://pay.rezolve.com/api/v1/checkout/init`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Merchant-Id":      process.env.REZOLVEPAY_MERCHANT_ID,
      "X-Merchant-Api-Key": process.env.REZOLVEPAY_MERCHANT_API_KEY,
    },
    body: JSON.stringify({
      amount:   order.amountMinor,    // e.g. 2599
      currency: order.currency,       // e.g. "GBP"
      order_id: order.id,             // your order reference
    }),
  });

  if (!upstream.ok) {
    return res.status(upstream.status).json(await upstream.json());
  }
  const { widget_token } = await upstream.json();

  // Render your checkout page, embedding the token in the script context.
  res.render("checkout", { widgetToken: widget_token, customerEmail: order.email });
});

Use the value for your environment from Environments.

cURL

# Mint a widget token (server-side only)
curl -X POST "https://pay.rezolve.com/api/v1/checkout/init" \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Id: $REZOLVEPAY_MERCHANT_ID" \
  -H "X-Merchant-Api-Key: $REZOLVEPAY_MERCHANT_API_KEY" \
  -d '{ "amount": 2599, "currency": "GBP", "order_id": "ORD-4421" }'

# Response
# { "widget_token": "wt_xxxxxxxxxxxxxxxxx" }

Use the value for your environment from Environments.

Widget postMessage events

The widget communicates with its host page via window.postMessage with source: 'rezolve-pay-widget'. The SDK callbacks (onSuccess, onContinueToShop, etc.) are wrappers around these raw events.

Event typeWhen firedSDK callback
widget:successPayment confirmed by provider. Widget remains visible.onSuccess({ paymentId })
widget:continue-to-shopShopper clicked Continue to shop on the success screen. SDK cleans up after firing.onContinueToShop()
widget:failurePayment declined. SDK cleans up.onFailure({ message })
widget:cancelShopper cancelled. SDK cleans up.onCancel()
widget:resizeWidget height changed. SDK resizes the iframe automatically.—

Webhook signature verification

Each outbound webhook includes X-RezolvePay-Signature, X-RezolvePay-Timestamp, and X-RezolvePay-Event-Id headers. Verify the signature with the signing secret from your merchant settings.

import { createHmac } from "crypto";

function verifyWebhookSignature(
  rawBody: string,
  secret: string,
  headers: {
    "x-rezolvepay-signature": string;
    "x-rezolvepay-timestamp": string;
  },
): boolean {
  const expected = createHmac("sha256", secret)
    .update(headers["x-rezolvepay-timestamp"] + "." + rawBody)
    .digest("hex");
  return expected === headers["x-rezolvepay-signature"];
}

Did this page help you?