# Low-Code SDK для JavaScript

> Добавьте на страницу настраиваемую кнопку оплаты, выберите источник суммы и оформление и откройте Paymos без собственной формы.

Low-Code SDK позволяет встроить кнопку оплаты прямо на ваш сайт. При нажатии SDK определяет сумму, создаёт инвойс через публичный SDK-ключ (`pk_...`) и открывает форму оплаты — **модальным окном в iframe** или **переходом на всю страницу**.

Сниппет работает только в проекте, у которого способ подключения — **Low-Code**. Способ вы выбираете при создании проекта, и потом он не меняется; в проекте с любым другим способом `POST /public/v1/sdk` отвечает `403 embed_not_enabled`.

Low-Code SDK — удобная обёртка над той же платёжной страницей, которую `POST /v1/invoices` возвращает в `payment_url`; в режиме iframe SDK открывает её с `?embed=true`. Ручное перенаправление и ручное встраивание в iframe описаны на [Платёжной странице](/ru/docs/hosted-checkout.md).

SDK отправляет POST на `https://paymos.io/public/v1/sdk` — тот же origin, что отдаёт `paymos-widget.js` — и передаёт ключ в заголовке `X-Sdk-Key`.

Если сумма не определена, а вы передали `pos_url`, кнопка открывает **Терминал** по этому адресу — интерфейс с нампадом для ввода суммы вручную. Без `pos_url` клик без суммы завершается ошибкой и событием `paymos:error`.

## Как это работает

:::dash-numbered
- Добавьте скрипт SDK и элемент для монтирования на страницу
- Вызовите `PaymosWidget.mount()` с `api_key`, `project_id` и параметрами
- Клиент нажимает кнопку
- SDK определяет сумму (фиксированная, функция обратного вызова или селектор поля ввода)
- Если сумма есть — SDK создаёт инвойс и открывает страницу оплаты
- Если суммы нет, но задан `pos_url` — SDK открывает Терминал с нампадом по этому адресу
- Если суммы нет и `pos_url` не задан — SDK выбрасывает ошибку и шлёт событие `paymos:error`
- Оплата обрабатывается — вебхук отправляется на зарегистрированный URL вебхука
:::

## Установка

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

## Определение суммы

SDK определяет сумму в момент нажатия на кнопку, используя первый доступный источник:

:::dash-numbered
- `amount` — фиксированная десятичная строка в настройках. Подходит для страниц с одним товаром или фиксированной ценой.
- `get_amount()` — функция обратного вызова, возвращающая сумму десятичной строкой. Используйте, когда итог вычисляется динамически (например, из корзины).
- `amount_selector` — CSS-селектор, указывающий на элемент `<input>`. SDK читает его `.value` в момент клика. Удобно, когда сумму вводит сам клиент.
:::

Если ни один из источников не вернул корректную положительную десятичную строку, SDK открывает **Терминал** по адресу из `pos_url` — нампад для ручного ввода суммы. Если `pos_url` не задан, клик завершается ошибкой `PaymosWidget requires an amount, amountSelector, or price_id.` и страница оплаты не открывается.

```javascript
// 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'
});
```

## Два режима интеграции

SDK предоставляет две точки входа. Выбирайте по задаче:

### `mount(selector, opts)` — кнопку рисует SDK

SDK сам вставляет в указанный элемент готовую стилизованную кнопку оплаты (её текст берётся из параметра `label`, по умолчанию `"Support with crypto"`). Подходит для страниц с одним товаром, простых лендингов, виджетов донатов — везде, где не хочется писать CSS.

```html
<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)` — кнопку рисуете вы, SDK открывает оверлей

Вы полностью владеете разметкой кнопки (ваша дизайн-система, ваш CSS, ваши иконки) и вызываете `PaymosWidget.open(opts)` из обработчика клика. Подходит для страниц с несколькими тарифами, корзин и собственных UI-фреймворков (React, Vue, Svelte).

```html
<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>
```

Оба режима обращаются к одному серверу, отправляют те же вебхуки и те же события в `window`. Разница только в том, кто отвечает за стили кнопки — на `open()` построена ссылка **Открыть демо** в дашборде (три независимо оформленные кнопки на одном общем оверлее).

## Когда использовать SDK, а когда REST API

Low-Code SDK — **браузерный** инструмент. Он ставит кнопку оплаты на статическую HTML-страницу, без серверной части. Подходит для:

- пожертвований и чаевых (сумму выбирает клиент)
- касс и точек продаж (сумму вводит сотрудник)
- виджетов с произвольной суммой («заплатите сколько хотите»)
- демонстраций в тестовой и промежуточной среде

:::warning
Для случаев, где цена фиксирована на вашей стороне (подписки, цифровые товары, физические заказы, курсы, лицензии), сумма в примере ниже редактируется из инструментов разработчика в браузере — клиент может отправить `0.01` за тариф 99 $. В таких случаях создавайте инвойс через [REST API](/ru/docs/invoices/create.md) на сервере, секретным ключом. SDK тут ни при чём: так устроена любая оплата, начинающаяся в браузере (Stripe, LemonSqueezy, PayPal) — публичный ключ на странице, секретный на сервере.
:::

## Параметры

| Параметр | Тип | Обязателен | Описание |
|----------|-----|------------|----------|
| `api_key` | `string` | Да | Публичный SDK-ключ из дашборда (`pk_...`) |
| `project_id` | `string` | Да, если не передан `price_id` | Идентификатор проекта (`prj_...`) |
| `price_id` | `string` | Нет | Фиксированная цена, созданная в дашборде (`price_...`). Сумму и валюту сервер берёт из неё, покупатель их не изменит — рекомендуемый вариант для товаров с фиксированной ценой. Имеет приоритет над всеми источниками суммы ниже |
| `amount` | `string \| number` | Нет | Фиксированная сумма оплаты — десятичная строка или число |
| `get_amount` | `function` | Нет | Функция обратного вызова, возвращающая сумму десятичной строкой в момент клика |
| `amount_selector` | `string` | Нет | CSS-селектор поля ввода с суммой |
| `currency` | `string` | Нет | Код фиатной валюты (`USD`, `EUR` и т.д.). По умолчанию: `USD` |
| `client_id` | `string` | Нет | Ваш внутренний ID клиента (прикрепляется к инвойсу) |
| `mode` | `string` | Нет | `"iframe"` (по умолчанию) — модальное окно. `"redirect"` — полностраничный переход |
| `label` | `string` | Нет | Текст кнопки. По умолчанию: `"Support with crypto"` |
| `width` | `string` | Нет | `"auto"` (по умолчанию) или `"full"` для 100 % ширины |
| `accent_color` | `string` | Нет | Цвет акцента кнопки (hex). По умолчанию: `#ff6b35` |
| `text_color` | `string` | Нет | Цвет текста кнопки (hex). По умолчанию: `#ffffff`. Контраст проверяется только если вы задали и свой `accent_color`: при контрасте 4,5:1 и выше SDK оставит ваш цвет, иначе возьмёт чёрный или белый — что контрастнее. На штатном акцентном цвете проверки нет, значение берётся как есть |
| `radius` | `number` | Нет | Скругление кнопки, 0–32 px. По умолчанию: `18` |
| `frame_title` | `string` | Нет | Доступное название для iframe-элемента. По умолчанию: `"Paymos checkout"` |
| `aria_label` | `string` | Нет | Доступное название модального окна. По умолчанию: `"Paymos checkout"` |
| `pos_url` | `string` | Нет | Адрес вашей страницы POS/Терминала. Если задан, кнопка открывает Терминал, когда сумма не определилась |
| `target` | `string` | Нет | `"_self"` (по умолчанию) или `"_blank"` (только при переходе на всю страницу) |
| `auto_close_delay` | `number` | Нет | Миллисекунды, в течение которых экран успешной оплаты остаётся видимым перед автоматическим закрытием окна. По умолчанию: `0` (выключено). Отменяется любым действием пользователя внутри окна. |
| `haptics` | `boolean` | Нет | Включить короткую тактильную отдачу на поддерживающих платформах (Android, некоторые PWA). iOS Safari игнорирует Vibration API — там отдача не сработает. Уважает `prefers-reduced-motion`. По умолчанию: `true` |
| `timeout` | `number` | Нет | Через сколько миллисекунд прервать запрос на создание инвойса. Значение должно быть положительным, иначе берётся значение по умолчанию: `30000` |
| `debug` | `boolean` | Нет | Писать в консоль полные объекты `Error`, а не только текст сообщения. По умолчанию: `false` |

## Полный пример

```html
<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>
```

## Программный API

Страницу оплаты можно открыть и без кнопки:

```javascript
// 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');
```

## События

SDK отправляет события `CustomEvent` в `window`, чтобы страница могла реагировать на ход оплаты. Все данные лежат в `event.detail`.

| Событие | Когда срабатывает | `event.detail` |
|---|---|---|
| `paymos:opened` | Модальное окно смонтировано, URL для iframe установлен. | `{ url }` |
| `paymos:closed` | Модальное окно удалено (по любой причине, включая срабатывание необязательного таймера `auto_close_delay`). | `{ reason: 'escape' \| 'iframe' \| 'replaced' \| 'api' \| 'success' }` |
| `paymos:succeeded` | Оплата перешла в `paid` или `paid_over`. Отправляется из iframe через `postMessage`. | `{ invoiceId }` |
| `paymos:failed` | Терминальное состояние с неуспехом (`underpaid`, `expired`, `cancelled`). | `{ invoiceId, reason }` |
| `paymos:error` | SDK не смог создать инвойс (сеть/авторизация/CORS/таймаут). | `{ message }` |

`succeeded` / `failed` приходят только в режиме iframe — их отправляет страница оплаты через `window.parent.postMessage`. SDK принимает сообщение только с origin той платёжной страницы, которую сам встроил, и только из её `contentWindow`, поэтому посторонние скрипты на странице мерчанта подделать их не могут.

При переходе на всю страницу браузер уходит с сайта до получения результата — подтверждение берите из вебхука на сервере.

```javascript
// 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();
  // Absolute and on your own domain: a root-relative path in a published
  // code sample gets crawled as one of OUR URLs — Googlebot did exactly that
  // and filed /thank-you?order= as a 404 against paymos.io.
  window.location.href = 'https://your-shop.example/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);
});
```

## Разрешённый домен

SDK сверяет `Origin` страницы с полем **Разрешённый origin** в настройках виджета. Не совпал — запрос отклоняется до того, как появится инвойс.

Значение одно, не список, и по умолчанию оно пустое. Пустое поле означает «отовсюду»: пока вы его не заполнили, кнопку с вашим публичным ключом `pk_...` можно встроить на любой чужой странице. Заполните его до выхода в рабочую среду. Запросы с того же домена, откуда отдаётся сама платёжная страница — например, экран терминала, — проверку пропускают всегда.

## Режим Терминала

Режим включается параметром `pos_url` — укажите в нём адрес страницы Терминала. Тогда, если сумма не определена (нет `amount`, `get_amount()` вернул null, поле по `amount_selector` пусто), SDK откроет этот адрес — нампад для ввода суммы вручную. Подходит для точек продаж, где сумма меняется от транзакции к транзакции.

Без `pos_url` тот же случай завершается ошибкой `PaymosWidget requires an amount, amountSelector, or price_id.` — она приходит на страницу событием `paymos:error`.
