Skip to content

Integrations

On this page

JavaScript Widget SDK

Add a configurable Pay button to any web page, choose the amount source and theme, and open Paymos checkout without building a backend form.

The Widget SDK lets you embed a payment button directly on your website. When a customer clicks it, the SDK resolves the payment amount, creates an invoice via your public SDK key (pk_...), and opens the checkout form — either as an iframe modal or a full-page redirect.

The Widget SDK is a convenience wrapper around the same hosted checkout page used by payment_url; in iframe mode it opens that checkout with ?embed=true. See Hosted Checkout for the raw redirect and manual iframe contract.

Under the hood the SDK posts to https://paymos.io/public/v1/sdk — the same origin that serves paymos-widget.js — with your key in the X-Sdk-Key header. If you self-host, set api_url to your own endpoint.

If no amount is available and you passed a pos_url, the button opens that Terminal page instead — a numpad where the amount is entered manually. Without pos_url, a click with no resolvable amount throws and emits paymos:error.

How it works

  • 01Add the SDK script and a mount point to your page
  • 02Call PaymosWidget.mount() with your api_key, project_id, and options
  • 03Customer clicks the button
  • 04SDK resolves the amount (static value, callback, or input selector)
  • 05If amount is available — SDK creates an invoice and opens checkout
  • 06If no amount and pos_url is set — SDK opens the Terminal numpad at that URL
  • 07If no amount and no pos_url — SDK throws and emits paymos:error
  • 08Payment is processed — webhook sent to your registered webhook URL

Installation

<script src="https://paymos.io/v1/paymos-widget.js"></script>
<div id="paymos-widget"></div>

Resolving the amount

The SDK resolves the payment amount at the moment the button is clicked, using the first available source:

  • 01amount — a fixed decimal string passed in the config. Best for single-product pages or fixed-price items.
  • 02get_amount() — a callback function that returns the amount as a decimal string. Use this when the total is computed dynamically (e.g. from a shopping cart).
  • 03amount_selector — a CSS selector pointing to an <input> element. The SDK reads its .value at click time. Useful when the customer types the amount themselves.

If none of the above returns a valid positive decimal string, the SDK opens the Terminal at your pos_url — a numpad where the amount is entered manually. If you didn't set pos_url, the click throws PaymosWidget requires an amount, amountSelector, or price_id. and no checkout opens.

// Fixed price (price_id) — RECOMMENDED for fixed products.
// Create the price once in the dashboard; the server resolves the amount and
// currency from it, so a buyer cannot change them in DevTools. No amount in the embed.
PaymosWidget.mount('#paymos-widget', {
  api_key: 'YOUR_PUBLIC_KEY',
  price_id: 'price_YOUR_PRICE_ID'
});

// Fixed amount — client-supplied (editable in the browser; use for donations / free-amount)
PaymosWidget.mount('#paymos-widget', {
  api_key: 'YOUR_PUBLIC_KEY',
  project_id: 'prj_YOUR_PROJECT_ID',
  amount: '25.00',
  currency: 'USD'
});

// Dynamic amount — computed at click time
PaymosWidget.mount('#paymos-widget', {
  api_key: 'YOUR_PUBLIC_KEY',
  project_id: 'prj_YOUR_PROJECT_ID',
  get_amount: () => calculateCartTotal().toFixed(2),
  currency: 'USD'
});

// Input selector — reads value from an <input> element
PaymosWidget.mount('#paymos-widget', {
  api_key: 'YOUR_PUBLIC_KEY',
  project_id: 'prj_YOUR_PROJECT_ID',
  amount_selector: '#donation-amount',
  currency: 'USD'
});

Two integration modes

The SDK exposes two entry points. Pick whichever fits your page:

mount(selector, opts) — SDK renders the button

The SDK injects a fully-styled, ready-to-use Pay button into the target element (its text comes from the label option, default "Support with crypto"). Best for single-product pages, simple landings, donation widgets — anywhere you want zero CSS work.

<script src="https://paymos.io/v1/paymos-widget.js"></script>
<div id="paymos-widget"></div>
<script>
  PaymosWidget.mount('#paymos-widget', {
    api_key: 'pk_live_…',
    project_id: 'prj_…',
    amount: '25.00',
    currency: 'USD',
    label: 'Support with crypto'
  });
</script>

open(opts) — you render the button, SDK opens the overlay

You keep full control of the button DOM (your design system, your CSS, your icons) and call PaymosWidget.open(opts) from a click handler. Best for pricing pages with multiple plans, shopping carts, custom UI frameworks (React, Vue, Svelte components).

<button id="buy-growth">Subscribe to Growth — $79/mo</button>
<script>
  document.getElementById('buy-growth').addEventListener('click', function () {
    PaymosWidget.open({
      api_key: 'pk_live_…',
      project_id: 'prj_…',
      amount: 79,
      currency: 'USD',
      client_id: 'plan:growth'
    });
  });
</script>

Both modes hit the same backend, fire the same webhooks, and emit the same window events. The choice is purely about button styling responsibility — open() is what the dashboard's Open Demo link launches (a pricing page with three independently-styled buttons, all routed through one shared overlay).

When to use the SDK vs the REST API

The Widget SDK is a browser-side tool — it puts a Pay button on a static HTML page without a backend. That makes it perfect for:

  • Donations / tip jars (the customer picks the amount)
  • POS / cashier-driven flows (staff types the amount)
  • Free-amount widgets ("name your price")
  • Internal sandbox / staging demos

For anything where the price is fixed on your side (subscriptions, digital goods, physical orders, courses, licences), remember the amount lives in the page and is editable from DevTools — a determined customer can submit 0.01 for your $99 plan. Use the REST API from your server with the secret key instead. The SDK isn't broken; this is the standard split shared by every client-side checkout (Stripe, LemonSqueezy, PayPal): publishable key on the page, secret key on the server.

Configuration

Parameter Type Required Description
api_key string Yes Public SDK key from the dashboard (pk_...)
project_id string Yes, unless price_id is provided Project identifier (prj_...)
price_id string No Fixed price created in the dashboard (price_...). The server resolves amount and currency from it, so the buyer can't edit them — recommended for fixed products. Takes precedence over every amount source below
amount string | number No Fixed payment amount as a decimal string or number
get_amount function No Callback returning the amount as a decimal string at click time
amount_selector string No CSS selector of an input with the amount
currency string No Fiat currency code (USD, EUR, etc.). Default: USD
client_id string No Your internal customer ID (attached to the invoice)
mode string No "iframe" (default) — modal overlay. "redirect" — full-page navigation
label string No Button text. Default: "Support with crypto"
width string No "auto" (default) or "full" for 100% width
accent_color string No Button accent color (hex). Default: #ff6b35
text_color string No Button text color (hex). Default: #ffffff. Overridden automatically when its contrast against accent_color falls below WCAG AA (4.5
) — the SDK then picks black or white, whichever contrasts better
radius number No Button border-radius, 0-32 px. Default: 18
frame_title string No Accessible title for the iframe element. Default: "Paymos checkout"
aria_label string No Accessible name for the modal dialog. Default: "Paymos checkout"
pos_url string No URL of your POS/Terminal page. When set, the button opens the Terminal if no amount resolves
target string No "_self" (default) or "_blank" (only for redirect mode)
api_url string No Override the endpoint. Defaults to the origin that serves paymos-widget.js (e.g. https://paymos.io/public/v1/sdk). Use only for self-hosting or local dev.
auto_close_delay number No Milliseconds to keep the success screen visible before the modal self-dismisses. Default: 0 (disabled). Cancelled by any user interaction inside the modal.
haptics boolean No Enable short haptic feedback on supporting platforms (Android, some PWAs). iOS Safari ignores the Vibration API — no-op there. Honors prefers-reduced-motion. Default: true
timeout number No Milliseconds before the invoice-creation request is aborted. Values must be positive; anything else falls back to the default. Default: 30000
debug boolean No Log full Error objects to the console instead of the message alone. Default: false

Full example

<script src="https://paymos.io/v1/paymos-widget.js"></script>

<div id="paymos-widget"></div>

<script>
  PaymosWidget.mount('#paymos-widget', {
    api_key: 'YOUR_PUBLIC_KEY',
    project_id: 'prj_YOUR_PROJECT_ID',
    amount: '25.00',
    currency: 'USD',
    label: 'Support with crypto',
    mode: 'iframe',
    accent_color: '#FF6B35',
    text_color: '#FFFFFF',
    radius: 20
  });
</script>

Programmatic API

You can also open checkout without mounting a button:

// Open checkout programmatically (no button)
await PaymosWidget.open({
  api_key: 'YOUR_PUBLIC_KEY',
  project_id: 'prj_YOUR_PROJECT_ID',
  amount: '50.00',
  currency: 'USD',
  mode: 'iframe'
});

// Close the modal
PaymosWidget.close();

// Remove the button and clean up
PaymosWidget.destroy('#paymos-widget');

Events

The SDK dispatches CustomEvents on window so your page can react to the checkout lifecycle. All payloads live on event.detail.

Event When it fires event.detail
paymos:opened The modal is mounted and the iframe URL has been set. { url }
paymos:closed The modal is removed (any reason, including the optional auto_close_delay timer firing). { reason: 'escape' | 'iframe' | 'replaced' | 'api' | 'success' }
paymos:succeeded The customer's payment reached paid or paid_over. Emitted from inside the iframe via postMessage. { invoiceId }
paymos:failed Terminal failure state (underpaid, expired, cancelled). { invoiceId, reason }
paymos:error The SDK couldn't even create the invoice (network/auth/CORS/timeout). { message }

The succeeded / failed events fire only when the checkout is opened in iframe mode — they're delivered by the payment page calling window.parent.postMessage. The SDK accepts a message only from the origin of the checkout URL it embedded, and only from that iframe's own contentWindow, so no third party on the merchant page can spoof them.

For redirect-mode checkouts the page navigates away before the result is known, so use a webhook on your server to confirm settlement.

// 1. The customer paid — auto-close the modal and redirect to a thank-you page.
window.addEventListener('paymos:succeeded', (e) => {
  console.log('Paid:', e.detail.invoiceId);
  PaymosWidget.close();
  window.location.href = '/thank-you?order=' + e.detail.invoiceId;
});

// 2. Payment failed (underpaid / expired / cancelled).
window.addEventListener('paymos:failed', (e) => {
  console.warn('Payment failed:', e.detail.invoiceId, e.detail.reason);
  // e.detail.reason: 'underpaid' | 'expired' | 'cancelled'
});

// 3. Modal lifecycle.
window.addEventListener('paymos:opened',  (e) => console.log('Opened',  e.detail.url));
window.addEventListener('paymos:closed',  (e) => console.log('Closed',  e.detail.reason));
//   e.detail.reason: 'escape' | 'iframe' | 'replaced' | 'api' | 'success'

// 4. Invoice creation failed (network down, bad config, server error).
window.addEventListener('paymos:error', (e) => {
  console.error('SDK error:', e.detail.message);
});

Allowed origin

The SDK only works on domains that match the Allowed Origin configured in your widget settings. Requests from other origins are rejected.

Terminal fallback

The fallback is opt-in: point pos_url at your Terminal page. When no valid amount is resolved (no amount, get_amount() returns null, or amount_selector input is empty), the SDK opens that URL — a numpad where the amount is entered manually. This is useful for point-of-sale scenarios where the total varies per transaction.

Leave pos_url unset and the same case throws PaymosWidget requires an amount, amountSelector, or price_id. instead, which reaches your page as a paymos:error event.