> ## 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

# Konfiguracja

> Atrybuty data-* przycisku płatności: tekst przycisku, dymek, język, położenie na ekranie, kilka widgetów na jednej stronie.

Widget konfigurujesz wyłącznie atrybutami `data-*` na znaczniku `<script>`. Każdy znacznik tworzy jeden, niezależny widget.

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<script
  defer
  src="https://pay.sandbox.paymove.io/v1/sdk.js"
  data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
  data-widget-id="checkout"
  data-button-label="Zapłać online"
  data-bubble-title="Szybka płatność"
  data-bubble-description="BLIK, Google Pay lub Apple Pay. Bez zakładania konta."
  data-locale="pl"
></script>
```

## Atrybuty

| Atrybut | Opis | Limit / domyślnie |
| - | - | - |
| `data-product-id` | **Wymagany.** Identyfikator produktu (UUID) albo jego publiczny alias (`shortName`) | do 256 znaków |
| `data-widget-id` | Identyfikator instancji - potrzebny do [własnych przycisków](/app-widget/javascript-api#własny-przycisk) i sterowania widgetem z kodu | do 128 znaków: litery, cyfry, `_`, `-`; bez niego generowany automatycznie |
| `data-button-label` | Tekst przycisku. Pusty - przycisk się nie pokazuje | do 120 znaków; domyślnie pusty |
| `data-bubble-title` | Tytuł dymka nad przyciskiem | do 160 znaków; domyślnie pusty |
| `data-bubble-description` | Opis w dymku | do 500 znaków; domyślnie pusty |
| `data-locale` | Język strony płatności w modalu, np. `pl`, `en`, `pl-PL` | domyślnie [wybór automatyczny](#język) |
| `data-external-id` | Otwiera widget od razu na płatności za konkretny dokument (subprodukt). Przyjmuje identyfikator subproduktu nadany przez paymove, a przy `rawExternalIdEnabled: true` także Twój własny `externalId` | do 256 znaków |
| `data-document-entry` | Pokazuje ekran, na którym klient sam wpisuje numer dokumentu. Działa dla produktu bez cennika na poziomie produktu. Flaga bez wartości | nie łączy się z `data-external-id` |
| `data-left` | Odległość przycisku od lewej krawędzi okna | 0-10000 px |
| `data-right` | Odległość od prawej krawędzi | 0-10000 px |
| `data-top` | Odległość od górnej krawędzi | 0-10000 px |
| `data-bottom` | Odległość od dolnej krawędzi | 0-10000 px |

Zasady wspólne dla wszystkich atrybutów:

* **Konfiguracja jest odczytywana raz**, przy starcie skryptu. Zmiana atrybutów później nie zmienia tekstu, języka ani położenia widgetu. Żeby to zrobić, usuń widget (`destroy()`) i wstaw skrypt na nowo.
* **Tekst to zwykły tekst**, nigdy HTML - znaczniki zostaną wyświetlone dosłownie.
* Wartość złożona z samych spacji liczy się jako pusta.
* Błędna wartość albo przekroczony limit blokuje uruchomienie **tylko tej instancji** i zostawia w konsoli przeglądarki komunikat z prefiksem `[PaymoveWidget]`, który wskazuje błędny atrybut.

## Przycisk i dymek

Przycisk pojawia się, gdy `data-button-label` nie jest pusty. Widget nie ma domyślnego tekstu - napis zawsze podajesz Ty, w języku swojej strony.

<Frame caption="Sam przycisk - data-button-label=&#x22;Zapłać online&#x22;">
  <img src="https://mintcdn.com/paymove/ztZW1d97Uc2P1tsz/images/app-widget/pl/button.png?fit=max&auto=format&n=ztZW1d97Uc2P1tsz&q=85&s=2b428aa9dd6d69327fbe1fd51fc6f0c1" alt="Ciemny przycisk widgetu z sygnetem paymove, napisem „Zapłać online” i strzałką" width="285" data-path="images/app-widget/pl/button.png" />
</Frame>

<Frame caption="Dymek nad przyciskiem - data-bubble-title i data-bubble-description">
  <img src="https://mintcdn.com/paymove/ztZW1d97Uc2P1tsz/images/app-widget/pl/button-bubble.png?fit=max&auto=format&n=ztZW1d97Uc2P1tsz&q=85&s=69d8a2943bdbe83e7a808ce0eb72c77f" alt="Dymek „Szybka płatność” z opisem „BLIK, Google Pay lub Apple Pay. Bez zakładania konta.” nad przyciskiem „Zapłać online”" width="315" data-path="images/app-widget/pl/button-bubble.png" />
</Frame>

Dymek pojawia się, gdy ustawisz `data-bubble-title` **albo** `data-bubble-description` (albo oba). Działa tak:

* pokazuje się po najechaniu kursorem na przycisk albo po przejściu do niego klawiszem `Tab`; na ekranach dotykowych się nie pokazuje,
* zostaje widoczny, gdy przesuwasz kursor z przycisku na dymek, i znika, gdy kursor i fokus opuszczą widget,
* `Esc` chowa go na chwilę, a ikona zamknięcia - do końca życia tej instancji widgetu (do przeładowania strony),
* otwarcie modala chowa dymek,
* nie zajmuje miejsca w układzie strony; gdy nad przyciskiem brakuje miejsca, wyświetla się pod nim,
* animacja trwa 200 ms i jest wyłączana, gdy system ma włączone ograniczenie ruchu.

<Info>
  Dymek wymaga przycisku widgetu. Bez `data-button-label` dymek nie jest renderowany, nawet jeśli ustawisz tytuł i opis.
</Info>

### Bez przycisku widgetu

Pomiń `data-button-label`, jeśli modal ma otwierać Twój własny element - link w menu, baner albo przycisk w treści. Widget nie wyświetli wtedy nic, dopóki go nie otworzysz:

```html theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
<script
  defer
  src="https://pay.sandbox.paymove.io/v1/sdk.js"
  data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
  data-widget-id="checkout"
></script>

<button type="button" data-paymove-open="checkout">Wesprzyj nas</button>
```

Szczegóły i wersja z JavaScriptu: [Własny przycisk](/app-widget/javascript-api#własny-przycisk).

## Położenie

Bez atrybutów położenia przycisk przypięty jest do **prawego dolnego rogu** ekranu, z uwzględnieniem bezpiecznych marginesów na urządzeniach z wycięciem ekranu.

Atrybuty `data-left`, `data-right`, `data-top` i `data-bottom` ustawiają odległość przycisku od krawędzi okna przeglądarki (pozycja `fixed`). Przyjmują liczbę albo wartość z `px`: `24` i `24px` znaczą to samo.

<CodeGroup>
  ```html Lewy dolny róg theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
  <script
    defer
    src="https://pay.sandbox.paymove.io/v1/sdk.js"
    data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
    data-button-label="Zapłać online"
    data-left="24"
    data-bottom="24"
  ></script>
  ```

  ```html Wyżej, nad czatem theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
  <script
    defer
    src="https://pay.sandbox.paymove.io/v1/sdk.js"
    data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
    data-button-label="Zapłać online"
    data-right="24"
    data-bottom="96"
  ></script>
  ```

  ```html Lewy górny róg theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
  <script
    defer
    src="https://pay.sandbox.paymove.io/v1/sdk.js"
    data-product-id="d0a834f9-94a4-4b3a-aa10-27d911e3633f"
    data-button-label="Zapłać online"
    data-left="24"
    data-top="96"
  ></script>
  ```
</CodeGroup>

* Każdy atrybut działa samodzielnie. `data-left="24"` przesuwa przycisk do lewej krawędzi i zostawia domyślną odległość od dołu.
* W każdej osi wybierz **jedną** krawędź: `left` albo `right`, `top` albo `bottom`. Podanie obu przeciwnych krawędzi blokuje uruchomienie widgetu.
* Położenie dotyczy wyłącznie przycisku. Dymek dopasowuje się do niego sam, a modal zawsze jest wyśrodkowany.
* Dobierz odległości tak, żeby przycisk nie zasłaniał innych pływających elementów - czatu, banera cookies, przycisku „do góry".

## Język

`data-locale` ustawia język strony płatności w modalu. Przyjmuje kod języka (`pl`, `en`) albo kod z regionem (`pl-PL`, `en-US`).

* Polski i angielski są dostępne dla każdego produktu, a dla części produktów także inne języki, m.in. niemiecki, ukraiński, czeski i słowacki.
* Gdy podany język nie jest dostępny, strona płatności używa języka zapamiętanego wcześniej przez klienta, a potem języka przeglądarki.
* Bez `data-locale` język jest wybierany tak samo.
* Klient może zmienić język w trakcie procesu.
* Teksty, które podajesz Ty - `data-button-label` i dymek - **nie są tłumaczone**. Przekaż je od razu w języku strony.
* Komunikaty samego widgetu (ładowanie, błąd, „Spróbuj ponownie", „Zamknij") biorą język z `data-locale`, potem z atrybutu `lang` na `<html>`, a potem z przeglądarki.

### Teksty z biblioteki i18n

Literał `t("…")` wpisany w HTML nie zostanie obliczony. Na stronie wielojęzycznej zbuduj znacznik skryptu w JavaScripcie i ustaw atrybuty **przed** wstawieniem go do dokumentu:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
const script = document.createElement("script");
script.src = "https://pay.sandbox.paymove.io/v1/sdk.js";
script.setAttribute("data-product-id", productId);
script.setAttribute("data-widget-id", "checkout");
script.setAttribute("data-locale", locale);
script.setAttribute("data-button-label", t("widget.button"));
script.setAttribute("data-bubble-title", t("widget.title"));
script.setAttribute("data-bubble-description", t("widget.description"));
document.head.append(script);
```

Po zmianie języka na stronie usuń widget (`destroy()`) i wstaw skrypt jeszcze raz z nowymi tekstami.

## Kilka widgetów na jednej stronie

Każdy znacznik `<script>` tworzy osobny widget z własnym przyciskiem. Możesz łączyć różne produkty, także z różnych [adresów skryptu](/app-widget#adresy-skryptu) - np. bilety i parking na stronie wydarzenia:

```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"
  data-button-label="Kup bilety"
  data-right="24"
  data-bottom="24"
></script>
<script
  defer
  src="https://park.sandbox.paymove.io/v1/sdk.js"
  data-product-id="PARKING_ID"
  data-widget-id="parking"
  data-button-label="Zapłać za parking"
  data-right="24"
  data-bottom="88"
></script>
```

* Nadaj każdemu widgetowi **inny** `data-widget-id`. Kolejny skrypt z tym samym identyfikatorem jest ignorowany.
* Widgety nie układają się automatycznie - bez atrybutów położenia wszystkie przyciski trafią w ten sam róg i się nałożą. Rozstaw je atrybutami `data-*`.
* Na stronie może być otwarty **tylko jeden modal**. Otwarcie drugiego widgetu zamyka pierwszy.
* Błąd konfiguracji jednego widgetu nie blokuje pozostałych.


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