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

# Payment statuses

> Payment statuses, what the webhook payload contains, and how to query a payment's state.

## Statuses

| Status                        | Meaning                                        | Terminal |
| ----------------------------- | ---------------------------------------------- | -------- |
| `INITIALIZED`                 | Payment created, the customer has not paid yet | No       |
| `PENDING`                     | Payment in progress on the provider's side     | No       |
| `COMPLETED`                   | Payment succeeded - **fulfil the order**       | Yes      |
| `CANCELED`                    | The customer cancelled the payment             | Yes      |
| `ERROR`                       | The payment failed                             | Yes      |
| `REFUNDED`                    | The payment was refunded                       | Yes      |
| `WAITING_FOR_EXTERNAL_ACTION` | Transient internal Paymove state               | No       |

<Info>
  The status is always transmitted as a **string** - `"COMPLETED"`, not a number. The numeric values found in older integrations are an internal database representation and appear neither in the API nor in webhooks.
</Info>

<Warning>
  `WAITING_FOR_EXTERNAL_ACTION` is a transient state, set briefly while Paymove finishes processing. It is not terminal and does not mean the payment needs customer action. You may observe it by polling at an unlucky moment - treat it like `PENDING`.
</Warning>

Note the spelling `CANCELED` - with a single "l".

## When the webhook arrives

<Warning>
  **By default Paymove sends a webhook only for the `COMPLETED` status.** Notifications for `CANCELED`, `ERROR` or `REFUNDED` must be enabled by Paymove for your specific product. If you need them, write to [integration@paymove.io](mailto:integration@paymove.io).
</Warning>

This means that in the default configuration the absence of a webhook does not distinguish a failed payment from one still in progress. If you must detect failed payments, use the [status query](#querying-the-status) or ask for full notifications to be enabled.

## Webhook payload

The shape depends on whether you set a `requestTemplate` when registering the webhook.

### Without a `requestTemplate`

Paymove sends the full payment object. Empty fields are omitted, so a given notification may contain fewer of them:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "id": "891412c8-8717-4449-9543-e34112bec470",
  "name": "MerchantShop",
  "fullName": "Merchant Shop Sp. z o.o.",
  "shortName": "MShop",
  "location": "PL",
  "externalId": "order-123",
  "orderId": "PAY1784798914400",
  "price": 950,
  "status": "COMPLETED",
  "paymentMethod": "BLIK",
  "email": "customer@example.com",
  "date": 1783246791.745352526,
  "requestId": "8f2b1c44-0d7e-4a91-b2c3-5e7f9a1d3c60"
}
```

| Field                                       | Description                                             |
| ------------------------------------------- | ------------------------------------------------------- |
| `id`                                        | The UUID of your product (store), not of the payment    |
| `name`, `fullName`, `shortName`, `location` | Product data from the configuration                     |
| `externalId`                                | **Your** order identifier - use it to match the payment |
| `orderId`                                   | Paymove's internal order identifier                     |
| `price`                                     | Amount in grosze, **minus the Paymove fee**             |
| `status`                                    | Payment status as a string                              |
| `paymentMethod`                             | The payment method used                                 |
| `email`                                     | The customer's email, when known                        |
| `date`                                      | Payment creation time (Unix epoch)                      |
| `requestId`                                 | Request identifier, useful when contacting support      |

<Warning>
  **`price` in this payload is the amount net of the fee**, not the amount charged to the customer. Do not use it to verify that the customer paid the right sum - compare against the value you stored when creating the payment.
</Warning>

<Info>
  The payload contains no `event` or `type` field and is not wrapped in an envelope. It is a flat object, and you identify the kind of event from the `status` field.
</Info>

### With a `requestTemplate`

The payload is exactly what your template renders. For the template `{"orderId": "{{externalId}}", "price": "{{price}}"}` you receive:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "orderId": "order-123",
  "price": "1000"
}
```

A template can use **every field of the default payload** - including `{{status}}`, `{{paymentMethod}}`, `{{email}}`, `{{date}}` and `{{orderId}}`, not just `{{externalId}}` and `{{price}}`. Note that values substituted into a text template arrive in the payload as **strings**.

<Info>
  A template gives you control over the payload shape and contains exactly what you write into it. If you need to distinguish statuses, add `{{status}}` to the template or leave `requestTemplate` unset altogether.
</Info>

## Querying the status

Useful as a supplement to the webhook - for example when the customer is back on `returnUrl` but the notification has not arrived yet.

<Warning>
  This endpoint lives under a **different base URL from the rest of the API**. It is not routed through `gateway-api.sandbox.paymove.io` or `api.paymove.io` - calling it there returns `404` with a `text/plain` body reading `No route found for: GET …`, which is not even the standard `{status, message}` shape.
</Warning>

| Environment | Base URL for the status query        |
| ----------- | ------------------------------------ |
| Sandbox     | `https://pay-api.sandbox.paymove.io` |
| Production  | `https://pay-api.paymove.io`         |

```bash theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
curl "https://pay-api.sandbox.paymove.io/api/payment/product/{productId}/subproduct/{paymentHash}/status"
```

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "status": "COMPLETED",
  "orderId": "PAY1784798914400"
}
```

| Parameter     | Description                                                                    |
| ------------- | ------------------------------------------------------------------------------ |
| `productId`   | The UUID of your product, or its `shortName`                                   |
| `paymentHash` | The 10-character payment hash from the `externalId` parameter in `redirectUrl` |

If no payment exists for the given hash you receive:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "status": 404,
  "message": "Payments for externalId ec6RtwTZKb not found"
}
```

<Warning>
  `paymentHash` is **not** the `externalId` you passed when creating the payment. It is a value generated by Paymove and returned inside `redirectUrl` - for example `ec6RtwTZKb` in `https://checkout.sandbox.paymove.io/{productId}?externalId=ec6RtwTZKb`. Store it when you create the payment.
</Warning>

<Info>
  This endpoint requires no API key. Do not pass sensitive data to it, and do not treat its response alone as the only proof of payment in critical flows - the trustworthy confirmation is a verified webhook.
</Info>

## Recommended flow

1. Create the payment and store your `externalId` together with the payment hash from `redirectUrl`.
2. Redirect the customer to the checkout.
3. On the `returnUrl` page show a "we are processing your payment" message - no fulfilment.
4. Wait for the webhook, [verify its signature](/en/webhook-signature) and fulfil the order idempotently.
5. If the webhook has not arrived by the time the customer returns, query the status so you can show the right message immediately.

## Refunds

Refunds are handled by Paymove - **there is no public API endpoint for issuing them**. If you need to refund a payment, contact [integration@paymove.io](mailto:integration@paymove.io). Once refunded, the payment takes the `REFUNDED` status.

## What's next

<CardGroup cols={2}>
  <Card title="Signature verification" icon="shield-check" href="/en/webhook-signature">
    A mandatory step before fulfilling an order.
  </Card>

  <Card title="Webhook configuration" icon="webhook" href="/en/webhooks">
    Registering a webhook and assigning it to a product.
  </Card>
</CardGroup>
