> ## 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. Registering a webhook requires both `retries` and `expectedCode` - omitting either returns HTTP 500, and `retries: 0` means no retry at all. 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. If the merchant has no `productId` yet, do not stop at a placeholder: ask for the API key, the `partnerId` and the shop name, propose defaults for `location` and `timezone` (`Europe/Warsaw`), offer the optional fields once, then create the product for them with `POST /api/product/pay` and take `productId` from `id` in the response. Do the same for the webhook: register it, then assign it to the product. Guided procedure: https://docs.paymove.io/skill.md (Steps 1-2).
> 15. 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

# Sterowanie widgetem

> window.PaymoveWidget: własne przyciski, otwieranie i zamykanie modala z kodu i zdarzenie paymove:completed.

Skrypt wystawia obiekt `window.PaymoveWidget`. Jest celowo mały - konfiguracja zostaje w atrybutach `data-*`, a z kodu tylko otwierasz, zamykasz i sprzątasz widgety.

| Metoda | Działanie |
| - | - |
| `PaymoveWidget.ready()` | Zwraca `Promise`, który rozwiązuje się po zakończeniu inicjalizacji wszystkich wstawionych widgetów |
| `PaymoveWidget.get(id)` | Zwraca widget o danym `data-widget-id` albo `undefined` |
| `widget.open()` | Otwiera modal |
| `widget.close()` | Zamyka modal |
| `widget.destroy()` | Usuwa widget: przycisk, modal, iframe i wszystkie nasłuchy |

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

const widget = window.PaymoveWidget.get("checkout");
widget?.open();
widget?.close();
widget?.destroy();
```

* `ready()` czeka na widgety, które właśnie się uruchamiają. Widget z błędną konfiguracją nie blokuje pozostałych - `ready()` i tak się rozwiąże, a `get()` zwróci dla niego `undefined`.
* Metody `update()` nie ma. Konfiguracji nie zmienisz po starcie - usuń widget i wstaw skrypt na nowo.
* Skrypt używa atrybutu `defer` (albo `async`), więc `window.PaymoveWidget` może jeszcze nie istnieć, gdy wykonuje się Twój kod. Odwołuj się do niego dopiero w `onload` skryptu albo po `DOMContentLoaded`.

## Własny przycisk

Dowolny element z atrybutem `data-paymove-open="<widget-id>"` otwiera modal wskazanego widgetu - bez pisania JavaScriptu:

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<script
  defer
  src="https://go.sandbox.paymove.io/v1/sdk.js"
  data-product-id="EVENT_PRODUCT_ID"
  data-widget-id="tickets"
></script>

<a href="/bilety" data-paymove-open="tickets">Kup bilety</a>
<button type="button" data-paymove-open="tickets">Kup bilety</button>
```

* Kliknięcie w link z `data-paymove-open` otwiera modal zamiast przejścia pod `href`. Ustaw w `href` stronę zapasową - zadziała, gdy skrypt się nie załaduje.
* Kliknięcie z wciśniętym `Ctrl`, `Cmd`, `Shift` albo `Alt` jest pomijane, więc „otwórz w nowej karcie" działa normalnie.
* Elementy z atrybutem `disabled` albo `aria-disabled="true"` są pomijane.
* Atrybut działa także na elementach wewnątrz Shadow DOM i na elementach dodanych do strony później.

Przycisk widoczny, zanim skrypt się załaduje, nie zareaguje na kliknięcie. Jeśli to ma znaczenie, włącz go dopiero po `ready()`:

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<button type="button" data-paymove-open="tickets" disabled id="buy">Kup bilety</button>

<script>
  window.addEventListener("DOMContentLoaded", async () => {
    await window.PaymoveWidget?.ready();
    if (window.PaymoveWidget?.get("tickets")) {
      document.getElementById("buy").disabled = false;
    }
  });
</script>
```

Ten sam efekt z JavaScriptu:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
document.querySelector("#buy").addEventListener("click", () => {
  window.PaymoveWidget?.get("tickets")?.open();
});
```

## Zdarzenie `paymove:completed`

Po udanej płatności widget wysyła na `document` zdarzenie `paymove:completed`:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
document.addEventListener("paymove:completed", (event) => {
  const { widgetId, productId, orderId } = event.detail;
  // np. odśwież listę faktur albo pokaż podziękowanie
});
```

| Pole | Opis |
| - | - |
| `widgetId` | `data-widget-id` widgetu, w którym zapłacono |
| `productId` | `data-product-id` tego widgetu |
| `orderId` | Identyfikator płatności paymove - ten sam, który klient widzi na ekranie potwierdzenia |

* Zdarzenie przychodzi dopiero wtedy, gdy strona płatności potwierdzi płatność w API paymove i wyświetli ekran potwierdzenia.
* Dla jednej płatności przychodzi **raz**.
* Modal po płatności **zostaje otwarty**, żeby klient zobaczył potwierdzenie. Zamyka go przyciskiem „Zakończ", ikoną zamknięcia, kliknięciem w tło albo klawiszem `Esc`.

<Warning>
  `paymove:completed` to zdarzenie w przeglądarce klienta - można je wywołać ręcznie z konsoli. Używaj go do odświeżenia interfejsu, nigdy jako dowodu zapłaty. Zamówienia realizuj i księguj na podstawie [webhooka](/products/docpay/rest-api#6-webhooki).
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.