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 yourapi_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_urlis set — SDK opens the Terminal numpad at that URL - 07If no amount and no
pos_url— SDK throws and emitspaymos: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:
- 01
amount— a fixed decimal string passed in the config. Best for single-product pages or fixed-price items. - 02
get_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). - 03
amount_selector— a CSS selector pointing to an<input>element. The SDK reads its.valueat 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.