> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paymove.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> ## paymove integration rules for AI agents
>
> These rules are verified against the live API. Follow them exactly.
>
> 1. Amounts are INTEGERS in grosze (minor units). `1000` means 10.00 PLN. PLN is the only supported currency and there is no currency field in the API.
> 2. A float `price` (e.g. `12.99`) is SILENTLY TRUNCATED to 12 grosze and still returns HTTP 200. Always send an integer: `Math.round(pln * 100)`.
> 3. Authenticate with the `X-API-KEY` header. Never `Authorization: Bearer`. Sandbox keys start with `sk_test_`, production keys with `sk_live_`.
> 4. Server-side only. A call from a merchant page is rejected. Never put the key in frontend code.
> 5. Unknown request fields are silently ignored and still return HTTP 200 — a wrong body shape looks like success. Match the documented shape exactly.
> 6. A missing `externalId` returns HTTP 500 `Something went wrong`, not 400. A missing `price` or `details.returnUrl` returns HTTP 200 and a usable `redirectUrl` — no error at all. Validate the body yourself before sending it.
> 7. Always verify the `X-Paymove-Signature` header on incoming webhooks before trusting them: https://docs.paymove.io/en/webhook-signature.md
> 8. Errors are `{"status": <int>, "message": "<text>"}`. Branch on the HTTP status only — never on `message`, which is unstable and leaks internal class names.
> 9. There is no rate limiting, no HTTP 429, no HTTP 422, no `Idempotency-Key` header and no API versioning. Do not write code that handles them.
> 10. Re-POSTing an `externalId` that already exists returns HTTP 200 with the ORIGINAL `redirectUrl` and silently discards EVERY field you send — the new price, description and details are all ignored. Use `PATCH /api/pay/product/{productId}/subproduct/{externalId}` to change a price.
> 11. Webhooks fire only on `COMPLETED` by default, and `retries` defaults to `0` (no retries) unless you set it explicitly. Delivery counts as successful when the HTTP status equals `expectedCode` — the response body is never inspected.
> 12. Never fulfil an order on the `returnUrl` redirect. The checkout does not redirect there by itself: the customer has to click "back to shop", and inside the widget modal that redirect never happens. Fulfil only in the webhook handler, after verifying the signature.
> 13. Do not pin a version of `@paymove-io/sdk` — install the latest.
> 14. Full documentation index: https://docs.paymove.io/llms.txt · Copy-paste quickstart: https://docs.paymove.io/en/quickstart.md · Agent skill: https://docs.paymove.io/skill.md

# Widget przeglądarkowy

> sdk.js — przycisk i pasek Paymove na stronie sklepu oraz checkout otwierany w modalu, bez wychodzenia ze sklepu.

Widget to jeden plik `sdk.js`, który dokłada do sklepu dwie rzeczy:

* **element `<paymove-checkout>`** — przycisk płatności albo pasek informacyjny z sygnetem Paymove i logotypami metod, gotowy do wstawienia w koszyku lub na liście metod płatności,
* **modal z checkoutem** — ten sam adres, który dziś otwierasz przekierowaniem, wyświetlony w iframie na stronie sklepu.

Widget nie tworzy płatności. Płatność powstaje po stronie serwera — przez [SDK Node.js](/sdk/javascript) albo [REST API](/rest-api) — a widget dostaje gotowy `redirectUrl` z odpowiedzi.

<Warning>
  Klucz API zostaje na serwerze. Widget przyjmuje wyłącznie `redirectUrl`, nigdy `apiKey`. Wywołanie API z przeglądarki i tak odrzuci CORS.
</Warning>

## 1. Podłączenie skryptu

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<script src="https://checkout.paymove.io/v1/sdk.js" async></script>
```

Skrypt rejestruje element `<paymove-checkout>` i wystawia obiekt `window.Paymove`. Cały interfejs siedzi w Shadow DOM, więc style sklepu nie mieszają się ze stylami widgetu i odwrotnie.

Przy `async` skrypt może dojechać po Twoim kodzie — poczekaj na `ready()`:

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
await window.Paymove?.ready();
```

Ścieżka `/v1/` to alias wersjonujący - ten sam plik serwowany jest też pod `/sdk.js`. Używaj `/v1/`, dzięki czemu ewentualna przyszła wersja `/v2/` nie zepsuje istniejących integracji.

<Info>
  Adres skryptu odpowiada środowisku checkoutu. Build `sdk.js` osadza w modalu wyłącznie własną domenę, więc skrypt z produkcji nie otworzy checkoutu sandboxowego i odwrotnie — pobieraj go z tej samej domeny, z której przychodzi `redirectUrl`.
</Info>

## 2. Element `<paymove-checkout>`

Najkrótsza wersja to sam znacznik w HTML:

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<paymove-checkout label="Zapłać · 149,00 zł" methods="BLIK,GPAY,APAY"></paymove-checkout>
```

Element renderuje przycisk z sygnetem Paymove, a pod nim kartę z logotypami metod płatności. Konfigurujesz go atrybutami:

| Atrybut            | Wartości                   | Domyślnie                     | Opis                                                                      |
| ------------------ | -------------------------- | ----------------------------- | ------------------------------------------------------------------------- |
| `mode`             | `payment`, `readonly`      | `payment`                     | `payment` to przycisk, `readonly` to sam pasek informacyjny               |
| `label`            | dowolny tekst              | `Zapłać` / `Płatności Online` | Napis na przycisku lub pasku                                              |
| `methods`          | lista po przecinku         | —                             | Metody, których logotypy pokazujesz                                       |
| `methods-label`    | dowolny tekst              | `Dostępne metody płatności`   | Opis w karcie metod pod przyciskiem                                       |
| `size`             | `small`, `medium`, `large` | `medium`                      | Wysokość przycisku: 40 / 48 / 56 px                                       |
| `variant`          | `dark`, `light`, `outline` | `dark`                        | Wariant kolorystyczny przycisku                                           |
| `logo`             | `black`, `white`           | dobierany do wariantu         | Wersja sygnetu Paymove                                                    |
| `arrow`            | `true`, `false`            | `true`                        | Podwójna strzałka na końcu przycisku                                      |
| `full-width`       | `true`, `false`            | `false`                       | Rozciąga element na całą szerokość rodzica                                |
| `interactive`      | `true`, `false`            | `false`                       | Tylko dla `readonly` — czyni pasek lub kafelek klikalnym                  |
| `method`           | kod metody, np. `BLIK`     | —                             | Tylko dla `readonly` — zamiast paska renderuje kafelek pojedynczej metody |
| `description`      | dowolny tekst              | opis z katalogu SDK           | Kafelek metody — własny opis pod nazwą                                    |
| `hide-label`       | `true`, `false`            | `false`                       | Kafelek metody — chowa nazwę; logo jest zawsze                            |
| `hide-description` | `true`, `false`            | `false`                       | Kafelek metody — chowa opis                                               |

Tekst po znaku `·` renderuje się pogrubiony, więc `label="Zapłać · 149,00 zł"` da „Zapłać · **149,00 zł**".

Widoczne są trzy pierwsze logotypy, reszta zwija się do znacznika `+N`.

| Metoda                       | Wartość w `methods` / `method` |
| ---------------------------- | ------------------------------ |
| BLIK                         | `BLIK`                         |
| Google Pay                   | `GPAY`                         |
| Apple Pay                    | `APAY`                         |
| Płatność kartą               | `CARD`                         |
| Przelew tradycyjny           | `TRANSFER`                     |
| Przelew online (Pay by Link) | `PAY_BY_LINK`                  |
| PayPo                        | `PAYPO`                        |

<Info>
  Sygnetu Paymove nie da się ukryć ani przemalować — do wyboru są dwie wersje: czarna i biała. Resztę wyglądu dostosujesz — patrz sekcja **Wygląd**.
</Info>

## 3. Tryb readonly

`mode="readonly"` renderuje sam pasek: sygnet, Twój tekst i logotypy metod. Domyślnie jest nieinteraktywny — to element informacyjny, taki jak pozycja „Płatności online" na liście metod dostawy i płatności w koszyku.

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<paymove-checkout
  mode="readonly"
  label="Płatności online"
  methods="BLIK,GPAY,APAY,PAY_BY_LINK,PAYPO"
  full-width
></paymove-checkout>
```

Gdy pasek ma sam otwierać płatność, dodaj `interactive` i podepnij kontroler (sekcja niżej). Element emituje wtedy zdarzenie `paymove:click`, obsługuje `Enter` i spację, i wystawia poprawne role dla czytników ekranu.

### Kafelki pojedynczych metod

Atrybut `method` zamienia pasek w **kafelek jednej metody**: logo, nazwa i opis z wbudowanego katalogu SDK — sklep nie utrzymuje własnych logotypów ani tekstów. Poniższe przykłady są żywe, renderuje je `sdk.js` załadowany na tej stronie:

<Frame>
  <div style={{ background: '#ffffff', borderRadius: '12px', padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: '16px', width: '100%', maxWidth: '360px' }}>
    <div data-paymove-example="mode=&#x22;readonly&#x22; method=&#x22;BLIK&#x22;" />

    <div data-paymove-example="mode=&#x22;readonly&#x22; method=&#x22;GPAY&#x22;" />

    <div data-paymove-example="mode=&#x22;readonly&#x22; method=&#x22;APAY&#x22;" />
  </div>
</Frame>

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<paymove-checkout mode="readonly" method="BLIK"></paymove-checkout>
<paymove-checkout mode="readonly" method="GPAY"></paymove-checkout>
<paymove-checkout mode="readonly" method="APAY"></paymove-checkout>
```

Logo jest zawsze. Nazwę i opis możesz schować (`hide-label`, `hide-description`) albo nadpisać (`label`, `description`):

<Frame>
  <div style={{ background: '#ffffff', borderRadius: '12px', padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: '16px', width: '100%', maxWidth: '360px' }}>
    <div data-paymove-example="mode=&#x22;readonly&#x22; method=&#x22;GPAY&#x22; hide-description" />

    <div data-paymove-example="mode=&#x22;readonly&#x22; method=&#x22;BLIK&#x22; hide-label hide-description" />

    <div data-paymove-example="mode=&#x22;readonly&#x22; method=&#x22;PAYPO&#x22; description=&#x22;Kup teraz, zapłać później&#x22;" />
  </div>
</Frame>

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<paymove-checkout mode="readonly" method="GPAY" hide-description></paymove-checkout>
<paymove-checkout mode="readonly" method="BLIK" hide-label hide-description></paymove-checkout>
<paymove-checkout mode="readonly" method="PAYPO" description="Kup teraz, zapłać później"></paymove-checkout>
```

Kafelek domyślnie nie ma tła, ramki ani paddingu — wtapia się w wiersz Twojej listy metod, a wiersze, ramki i radiobuttony zostają po stronie sklepu. Zmiennymi CSS z sekcji **Wygląd** (`--paymove-bg`, `--paymove-border-color`, `--paymove-padding`, `--paymove-radius`) zrobisz z niego samodzielną kartę. Z atrybutem `interactive` kafelek jest klikalny i emituje `paymove:click` — przy schowanych tekstach nazwa metody zostaje w `aria-label`, więc czytniki ekranu widzą go poprawnie.

## 4. Otwarcie checkoutu w modalu

Modalem steruje kontroler z `window.Paymove`:

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<paymove-checkout id="pay" label="Zapłać · 149,00 zł" methods="BLIK,GPAY,APAY" full-width></paymove-checkout>

<script>
  const checkout = window.Paymove.createCheckout({
    fetchCheckoutUrl: async () => {
      const response = await fetch('/api/payments', { method: 'POST' });
      const { redirectUrl } = await response.json();
      return redirectUrl;
    },
    onSuccess: ({ externalId }) => console.log('opłacone', externalId),
    onCancel: () => console.log('klient zamknął modal'),
  });

  checkout.mount('#pay');
</script>
```

`mount()` przejmuje kliknięcia w element i sam przełącza przycisk w stan ładowania na czas pobierania adresu. Jeśli `redirectUrl` masz już w momencie renderowania strony, podaj go zamiast `fetchCheckoutUrl`:

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
const checkout = window.Paymove.createCheckout({ checkoutUrl: redirectUrl });
```

<Info>
  `fetchCheckoutUrl` jest wygodniejsze: płatność powstaje dopiero w chwili kliknięcia, więc nie zostawiasz porzuconych płatności po klientach, którzy tylko oglądali koszyk.
</Info>

### Konfiguracja `createCheckout`

| Pole               | Opis                                                         |
| ------------------ | ------------------------------------------------------------ |
| `checkoutUrl`      | Gotowy `redirectUrl` z Twojego backendu                      |
| `fetchCheckoutUrl` | Funkcja zwracająca `redirectUrl`, wywoływana przy kliknięciu |
| `flow`             | `modal` (domyślnie) albo `redirect`                          |
| `triggerPayment`   | Metoda uruchamiana od razu po otwarciu checkoutu             |
| `onReady`          | Checkout zgłosił gotowość i modal pokazał treść              |
| `onSuccess`        | Płatność zakończona powodzeniem — `{ externalId }`           |
| `onComplete`       | Modal zamknięty po udanej płatności — `{ externalId }`       |
| `onCancel`         | Modal zamknięty bez płatności                                |
| `onError`          | Błąd checkoutu — `{ code, message }`                         |

Konfiguracja przyjmuje też wszystkie pola wyglądu przycisku (`label`, `methods`, `variant`…), więc możesz opisać element wyłącznie w JavaScripcie i zamontować go w pustym `<div>`.

<Warning>
  `externalId` w `onSuccess` i `onComplete` to **`details.orderId`**, jeśli przekazałeś je przy tworzeniu płatności - a 10-znakowy hash Paymove dopiero wtedy, gdy `orderId` nie ustawiłeś. Jeśli dopasowujesz zamówienie po tej wartości, ustawiaj `details.orderId` konsekwentnie, żeby zawsze dostawać ten sam identyfikator.
</Warning>

### Kontroler

| Metoda          | Działanie                                                                                     |
| --------------- | --------------------------------------------------------------------------------------------- |
| `mount(target)` | Podpina się pod selektor lub element — wstawia `<paymove-checkout>`, jeśli w środku go nie ma |
| `unmount()`     | Odpina się od elementu                                                                        |
| `open()`        | Otwiera modal (albo przekierowuje przy `flow: 'redirect'`)                                    |
| `close()`       | Zamyka modal                                                                                  |
| `update(patch)` | Podmienia część konfiguracji, np. `label` po zmianie kwoty w koszyku                          |
| `destroy()`     | Sprząta wszystko: modal, nasłuchy i element                                                   |

Gdy chcesz tylko otworzyć modal — bez własnego przycisku — użyj skrótu:

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
window.Paymove.openCheckout(redirectUrl, {
  onSuccess: ({ externalId }) => console.log('opłacone', externalId),
});
```

## 5. Przekierowanie zamiast modala

To ta sama konfiguracja z jednym polem więcej:

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
window.Paymove.createCheckout({
  checkoutUrl: redirectUrl,
  flow: 'redirect',
}).mount('#pay');
```

Klient trafia na checkout Paymove, a po płatności widzi ekran potwierdzenia z przyciskiem „Wróć do sklepu", który przenosi go pod `returnUrl` przekazany przy tworzeniu płatności. Modal i przekierowanie różnią się wyłącznie tym polem, więc możesz przełączać się między nimi bez zmian w reszcie integracji.

<Warning>
  W trybie `modal` `returnUrl` nie jest używany w ogóle - przycisk powrotu zamyka modal i wywołuje `onComplete`. Jeśli Twoja integracja opiera się na handlerze `returnUrl`, w modalu nigdy się on nie wykona. Źródłem prawdy pozostaje [webhook](/webhook-signature).
</Warning>

## 6. Jak zachowuje się modal

* Na desktopie to panel o szerokości **398 px**, wyśrodkowany, na przyciemnionym tle. Wysokość dopasowuje się do treści checkoutu.
* Przy szerokości okna do **640 px** modal wysuwa się z dołu jak natywny arkusz: maksymalnie **90% wysokości okna**, z belką do przeciągania — przeciągnięcie w dół zamyka płatność.
* W trakcie ładowania widać statyczny sygnet Paymove. Krzyżyka wtedy nie ma — pojawia się dopiero, gdy checkout nie odezwie się przez 10 sekund, jako wyjście awaryjne.
* Po załadowaniu zamykanie przejmuje nagłówek checkoutu. Zamknąć można też klawiszem `Esc` i kliknięciem w tło.
* Strona pod modalem nie przewija się, a po zamknięciu wraca w to samo miejsce.
* Na czas płatności checkout blokuje przewijanie tła i sam raportuje swoją wysokość, więc modal nie skacze przy zmianie kroku.
* Modal potrafi zamknąć się sam po udanej płatności, ale sterujesz tym **przy tworzeniu subproduktu**, polem [`details.autoclose`](/rest-api#parametry-body) (w milisekundach), a nie z poziomu SDK. Checkout pokazuje wtedy odliczanie w przycisku „Wróć do sklepu" i po jego upływie zamyka modal, wywołując `onComplete`. Dowolna interakcja klienta anuluje odliczanie.

<Warning>
  Część banków w Pay by Link i PayPo zabrania osadzania swoich stron w iframie. Checkout otwiera je wtedy w nowym oknie i po powrocie wraca do modala — nie wymaga to niczego po stronie sklepu, ale nie blokuj wyskakujących okien we własnym kodzie.
</Warning>

## 7. Wygląd

Kolory, promień i typografię przycisku ustawisz konfiguracją albo bezpośrednio zmiennymi CSS na elemencie:

| Pole konfiguracji | Zmienna CSS              |
| ----------------- | ------------------------ |
| `color`           | `--paymove-bg`           |
| `textColor`       | `--paymove-color`        |
| `borderColor`     | `--paymove-border-color` |
| `radius`          | `--paymove-radius`       |
| `fontSize`        | `--paymove-font-size`    |
| `padding`         | `--paymove-padding`      |
| `fontFamily`      | `--paymove-font-family`  |

```html theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
<paymove-checkout
  label="Zapłać"
  style="--paymove-bg: #0a0d14; --paymove-radius: 6px"
></paymove-checkout>
```

Widget dziedziczy krój pisma ze strony sklepu, dopóki nie ustawisz `--paymove-font-family`.

## 8. React i Next.js

```jsx theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
'use client';

import Script from 'next/script';
import { useEffect, useRef } from 'react';

export function PayButton({ amount }) {
  const host = useRef(null);
  const checkout = useRef(null);

  useEffect(() => {
    let cancelled = false;

    window.Paymove?.ready().then((sdk) => {
      if (cancelled) return;
      checkout.current = sdk.createCheckout({
        label: `Zapłać · ${amount}`,
        methods: ['BLIK', 'GPAY', 'APAY'],
        fullWidth: true,
        fetchCheckoutUrl: async () => {
          const response = await fetch('/api/payments', { method: 'POST' });
          const { redirectUrl } = await response.json();
          return redirectUrl;
        },
        onSuccess: ({ externalId }) => router.push(`/zamowienie/${externalId}`),
      });
      checkout.current.mount(host.current);
    });

    return () => {
      cancelled = true;
      checkout.current?.destroy();
    };
  }, [amount]);

  return (
    <>
      <Script src="https://checkout.paymove.io/v1/sdk.js" strategy="afterInteractive" />
      <div ref={host} />
    </>
  );
}
```

Po zmianie kwoty wystarczy `checkout.current.update({ label: 'Zapłać · 199,00 zł' })` — element przerysuje się bez montowania od nowa.

<Info>
  W Reakcie montuj widget w pustym `<div>`, a nie w JSX-owym `<paymove-checkout>`. Element i tak trzyma treść w Shadow DOM, więc React nie ma czym zarządzać, a przy hydratacji unikasz ostrzeżeń o niezgodności drzewa.
</Info>

## 9. Ograniczenia

* `sdk.js` otwiera w modalu wyłącznie własną domenę checkoutu. Adres z innej domeny kończy się błędem `INVALID_CHECKOUT_URL`, zanim cokolwiek się pokaże.
* Płatność zawsze tworzy Twój backend — widget nie zna klucza API i nie odpytuje API Paymove.
* Zamówienie realizuj dopiero po zweryfikowanym [webhooku](/webhook-signature). `onSuccess` to sygnał dla interfejsu, nie potwierdzenie księgowania.
* Modal wymaga, żeby strona sklepu mogła osadzić checkout w iframie — checkout wysyła nagłówek `Content-Security-Policy` z `frame-ancestors https:`, więc sklep musi działać po HTTPS (poza `localhost` w developmencie).
