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

# Zwroty

> Zwrot płatności przez API: dostępność funkcji, endpoint, statusy zwrotu, lista zwrotów i ograniczenia.

Zwrot płatności zlecisz samodzielnie, jednym wywołaniem HTTP. Pod tym samym adresem `GET` zwraca listę zwrotów danej płatności, więc po zleceniu zwrotu sprawdzasz jego stan tam, gdzie go zleciłeś.

> [!WARNING]
>
> **Zwroty przez API nie są dostępne dla każdego partnera.** To funkcja włączana indywidualnie, po uprzednim zgłoszeniu do Paymove - nie zakładaj, że Twoje konto ją ma, dopóki tego nie potwierdzisz. Napisz na [integration@paymove.io](mailto:integration@paymove.io), żeby ją włączyć; dopóki nie jest aktywna, każde wywołanie kończy się kodem `403`.

> [!WARNING]
>
> Wywołuj API wyłącznie po stronie serwera. Zwrot przenosi pieniądze - klucz API nigdy nie może trafić do kodu frontendowego.

## 1. Endpointy

```
POST https://gateway-api.sandbox.paymove.io/api/pay/payment/{orderId}/refund
GET  https://gateway-api.sandbox.paymove.io/api/pay/payment/{orderId}/refund
```

Jedna ścieżka, dwie metody: `POST` tworzy zwrot, `GET` zwraca listę zwrotów tej płatności.

| Środowisko | Adres bazowy |
| - | - |
| Sandbox | `https://gateway-api.sandbox.paymove.io` |
| Produkcja | `https://api.paymove.io` |

### Nagłówki

| Nagłówek | Wartość |
| - | - |
| `Content-Type` | `application/json` |
| `X-API-KEY` | Twój klucz API - `sk_test_…` w sandboxie, `sk_live_…` na produkcji |

`POST` wymaga klucza z uprawnieniem do zapisu, `GET` - do odczytu. Klucz bez wymaganego uprawnienia kończy się kodem `401`; właściwy wygenerujesz w [Panelu](https://panel.paymove.io).

> [!NOTE]
>
> Segment `pay` w ścieżce wskazuje bramkę płatniczą. Pozostałe produkty paymove mają własne segmenty (`go`, `parking`, `multi`) - zwrot wykonuj pod segmentem produktu, w ramach którego powstała płatność.

### Skąd wziąć `orderId`

`{orderId}` to **wewnętrzny identyfikator zamówienia nadany przez Paymove**, na przykład `PAY1784798914400`. Masz go w dwóch miejscach:

* w polu `orderId` [payloadu webhooka](/payment-status.md#payload-webhooka),
* w odpowiedzi [zapytania o status płatności](/payment-status.md#sprawdzenie-statusu-zapytaniem).

> [!WARNING]
>
> `orderId` to **nie** jest Twój `externalId` ani 10-znakowy skrót płatności z `redirectUrl`. Ani jednego, ani drugiego nie da się użyć w tej ścieżce. Zapisz `orderId` u siebie przy obsłudze webhooka - inaczej przed każdym zwrotem będziesz musiał odpytać o status płatności.

## 2. Wykonanie zwrotu

```bash theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
curl --request POST \
  --url https://gateway-api.sandbox.paymove.io/api/pay/payment/PAY1784798914400/refund \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  --data '{
    "amountMinor": 15000,
    "email": "klient@example.com",
    "reason": "Klient zrezygnował z zamówienia"
  }'
```

To samo w Node.js:

```javascript theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
const url = `https://gateway-api.sandbox.paymove.io/api/pay/payment/${orderId}/refund`;

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-KEY": process.env.PAYMOVE_API_KEY,
  },
  body: JSON.stringify({
    amountMinor: Math.round(150.0 * 100), // zawsze liczba całkowita w groszach
    email: "klient@example.com",
    reason: "Klient zrezygnował z zamówienia",
  }),
});

if (!response.ok) {
  const error = await response.json(); // { status, message }
  throw new Error(`Paymove ${response.status}: ${error.message}`);
}

const refund = await response.json();
console.log(refund.id, refund.status); // np. "9f2c1b44-…" "PENDING"
```

Najkrótsze poprawne żądanie to **puste body** `{}` - zwraca całą kwotę płatności.

### Parametry body

| Pole | Typ | Wymagane | Opis |
| - | - | - | - |
| `amountMinor` | `integer` | Nie | Kwota zwrotu w groszach. Jeśli ją podasz, musi być **równa pełnej kwocie płatności** - zwroty częściowe nie są obsługiwane. Pominięcie pola zwraca całą kwotę |
| `email` | `string` | Nie | Adres e-mail płatnika. Używany przy autoryzacji poprzedzającej zwrot; bez niego wstawiana jest wartość zastępcza. Płatności przyjęte bez adresu e-mail (np. z terminala QR) zwracają się poprawnie i bez tego pola |
| `reason` | `string` | Nie | Dowolny tekst, zapisywany przy zwrocie i zwracany w polu `comment` |

> [!WARNING]
>
> **Żądanie nie zawiera `productId`.** Produkt - a wraz z nim dane rozliczeniowe - Paymove odczytuje z samej płatności. Jeśli wyślesz `productId`, zostanie po cichu zignorowany, jak [każde nieznane pole](/errors.md#błędy-które-wyglądają-jak-sukces).

> [!NOTE]
>
> Zwróć uwagę na nazwę pola: tutaj kwota to `amountMinor`, podczas gdy przy [tworzeniu płatności](/rest-api.md#parametry-body) jest to `price`. Jednostka jest w obu miejscach ta sama - grosze jako liczba całkowita.

### Odpowiedź

`POST` zwraca jeden zwrot, `GET` tablicę obiektów dokładnie tego samego typu - jeden parser obsłuży oba wywołania.

```json theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
{
  "id": "9f2c1b44-6d2f-4f0e-9a71-2b8c5d3e1a90",
  "type": "REFUND",
  "orderId": "PAY1784798914400-r1",
  "parentOrderId": "PAY1784798914400",
  "amountMinor": 15000,
  "status": "PENDING",
  "paymentMethod": "BLIK",
  "partnerId": "3a7d1f52-8b64-4c19-9e03-7d2a5f8c1b44",
  "productId": "891412c8-8717-4449-9543-e34112bec470",
  "locale": "pl",
  "comment": "Klient zrezygnował z zamówienia",
  "createdAt": "2026-10-06T11:42:03Z",
  "updatedAt": "2026-10-06T11:42:04Z"
}
```

| Pole | Opis |
| - | - |
| `id` | Identyfikator zwrotu w Paymove |
| `type` | Zawsze `REFUND` |
| `orderId` | Identyfikator **samego zwrotu**, w postaci `{orderId płatności}-rN` |
| `parentOrderId` | `orderId` zwracanej płatności - ten, który podałeś w ścieżce |
| `amountMinor` | Zwrócona kwota w groszach |
| `status` | [Status zwrotu](#3-statusy-zwrotu) |
| `paymentMethod` | Metoda płatności, którą zapłacono zwracaną płatność |
| `partnerId` | Twój identyfikator partnera |
| `productId` | UUID produktu, w ramach którego powstała płatność |
| `locale` | Locale płatności |
| `comment` | Tekst przekazany w `reason` |
| `createdAt`, `updatedAt` | Znaczniki czasu w formacie ISO 8601 (UTC) |

> [!WARNING]
>
> **`orderId` w odpowiedzi to identyfikator zwrotu, nie płatności.** Identyfikator płatności znajdziesz w `parentOrderId`. Zapisanie `orderId` zwrotu jako identyfikatora płatności to najłatwiejsza do popełnienia pomyłka na tej stronie.

## 3. Statusy zwrotu

Pole `status` przyjmuje te same wartości co [status płatności](/payment-status.md#statusy):

| Status | Znaczenie | Końcowy |
| - | - | - |
| `INITIALIZED` | Zwrot zapisany, jeszcze nie skierowany do rozliczenia | Nie |
| `PENDING` | Zwrot w toku | Nie |
| `WAITING_FOR_EXTERNAL_ACTION` | Stan przejściowy wewnątrz Paymove | Nie |
| `COMPLETED` | Zwrot rozliczony - **jedyny status oznaczający sukces** | Tak |
| `CANCELED` | Zwrot anulowany | Tak |
| `ERROR` | Zwrot nie powiódł się | Tak |

> [!WARNING]
>
> **Kod `200` nie znaczy, że pieniądze wróciły do klienta.** Oznacza, że Paymove przyjął zwrot do realizacji - typową odpowiedzią jest `status: "PENDING"`. Stan końcowy sprawdzaj przez `GET …/refund`; zwroty rozliczają się zwykle w ciągu kilkudziesięciu sekund.

Udany zwrot ustawia status **całej płatności** na [`REFUNDED`](/payment-status#statusy).

## 4. Lista zwrotów płatności

```bash theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
curl --request GET \
  --url https://gateway-api.sandbox.paymove.io/api/pay/payment/PAY1784798914400/refund \
  --header 'X-API-KEY: sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
```

```json theme={"theme":{"light":"one-dark-pro","dark":"one-dark-pro"}}
[
  {
    "id": "9f2c1b44-6d2f-4f0e-9a71-2b8c5d3e1a90",
    "type": "REFUND",
    "orderId": "PAY1784798914400-r1",
    "parentOrderId": "PAY1784798914400",
    "amountMinor": 15000,
    "status": "COMPLETED",
    "paymentMethod": "BLIK",
    "comment": "Klient zrezygnował z zamówienia",
    "createdAt": "2026-10-06T11:42:03Z",
    "updatedAt": "2026-10-06T11:42:31Z"
  }
]
```

Płatność bez zwrotów zwraca pustą tablicę `[]`. To właściwy endpoint do odpytywania o stan zwrotu - polling po kilku sekundach, aż `status` będzie [końcowy](#3-statusy-zwrotu).

> [!NOTE]
>
> Lista zawiera także **nieudane próby**. Każdy zwrot zapisujemy przed skierowaniem go do rozliczenia, więc próba odrzucona od razu nie przepada - `POST` odpowiada wtedy `400`, a na liście widać ją ze statusem `ERROR`.

## 5. Czego nie da się zwrócić

Każdy z tych przypadków kończy się kodem `400`:

* **Płatność nieukończona.** Zwrócić można wyłącznie płatność ze statusem `COMPLETED`. Płatności `INITIALIZED`, `PENDING`, `CANCELED` i `ERROR` nie ma po co zwracać.
* **Płatność, która ma już zwrot.** Istniejący zwrot w stanie `PENDING` lub `COMPLETED` blokuje kolejne próby - [jeden zwrot na płatność](#6-ograniczenia).
* **Kwota inna niż pełna.** `amountMinor` różne od kwoty płatności jest odrzucane; zwroty częściowe nie są obsługiwane.
* **Części płatności nie da się zwrócić przez API w ogóle** - zależy to od sposobu, w jaki zostały rozliczone. Jeśli dostajesz `400` przy ukończonej płatności i pełnej kwocie, napisz na [integration@paymove.io](mailto:integration@paymove.io) - zwrot wykonamy za Ciebie.

> [!NOTE]
>
> Zwroty płatności **PayPo** są obsługiwane, ale potwierdzenie przychodzi asynchronicznie - odpowiedź `POST` ma zawsze `status: "PENDING"`, nigdy od razu `COMPLETED`.

## 6. Ograniczenia

> [!WARNING]
>
> **Brak zwrotów częściowych i jeden zwrot na płatność.** `amountMinor` musi być równe pełnej kwocie płatności, a druga próba zwrotu tej samej płatności jest odrzucana. Oba ograniczenia wynikają z tego, że zwrot rozlicza się pod referencją samej płatności - drugi byłby nie do odróżnienia od pierwszego.

> [!WARNING]
>
> **Nie ma klucza idempotencji.** Dwa identyczne żądania to dwa zwroty - dziś drugi z nich zatrzyma kontrola „jeden zwrot na płatność", ale nie buduj na tym logiki ponawiania. Zanim ponowisz `POST`, sprawdź `GET …/refund`.

## 7. Kody statusów

| Status | Kiedy |
| - | - |
| `200` | Zwrot przyjęty do realizacji albo lista zwrotów zwrócona poprawnie. **Nie** oznacza, że pieniądze już wróciły |
| `400` | Płatność nieukończona; `amountMinor` różne od pełnej kwoty; płatność ma już zwrot; płatność bez przypisanego produktu, więc nie da się ustalić danych rozliczeniowych; płatności nie da się zwrócić przez API; zwrot odrzucony |
| `401` | Brak nagłówka `X-API-KEY`, klucz nierozpoznany albo klucz bez uprawnienia do zapisu (`POST`) lub odczytu (`GET`) |
| `403` | Zwroty przez API nie są włączone dla Twojego konta, płatność należy do innego partnera albo płatność nie istnieje |
| `404` | Niespójność danych po stronie Paymove - płatność istnieje, ale brakuje jej rekordu źródłowego. Zgłoś to na [integration@paymove.io](mailto:integration@paymove.io); to nie jest błąd po Twojej stronie |

> [!NOTE]
>
> Płatność, która nie istnieje, i płatność należąca do innego partnera zwracają **ten sam kod `403`**. Jest to zamierzone - `404` potwierdzałoby, które identyfikatory zamówień istnieją w Paymove. Nie wnioskuj z `403` o tym, czy płatność istnieje.

Własność płatności jest częścią wyszukiwania, a nie kontrolą po nim: płatność szukamy po `orderId` **i** po partnerze, do którego należy Twój klucz. Nie da się więc zwrócić płatności innego partnera ani skierować zwrotu na jego dane rozliczeniowe.

## 8. Zalecany przepływ

1. Przy obsłudze webhooka zapisz u siebie `orderId` płatności obok swojego `externalId`.
2. Przed zwrotem upewnij się, że płatność ma status `COMPLETED` i że nie zwróciłeś jej już wcześniej - `GET …/refund`.
3. Wyślij `POST …/refund`. Zapisz `id` i `orderId` zwrotu z odpowiedzi.
4. Odpytuj `GET …/refund`, aż status zwrotu będzie końcowy. `COMPLETED` oznacza zwrot rozliczony; `ERROR` - odrzucony.
5. Dopiero po `COMPLETED` oznacz zwrot jako zrealizowany u siebie.

## Co dalej

> **[Statusy płatności](/payment-status.md)**
>
> Skąd wziąć `orderId` i jak sprawdzić status płatności.

> **[Kody błędów](/errors.md)**
>
> Format błędów i mechanizmy, których API nie posiada.

> **[REST API](/rest-api.md)**
>
> Tworzenie płatności, którą potem zwracasz.

> **[Wsparcie](/support.md)**
>
> Kontakt, gdy zwrotu nie da się wykonać przez API.


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