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

# Error codes

> Every error the payment gateway returns, what causes it — and the mechanisms the API does not have.

## Error response shape

The gateway returns errors in one fixed format:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "status": 401,
  "message": "Invalid API key"
}
```

| Field     | Description                                |
| --------- | ------------------------------------------ |
| `status`  | The HTTP status code, repeated in the body |
| `message` | A human-readable description               |

<Warning>
  **Do not base logic on the `message` text.** Branch on the HTTP status only. The messages change without notice, and some of them contain internal backend class names - for example `PaySubProductEntity not found`. There is no separate machine-readable error code.
</Warning>

Two departures from the shape above are worth knowing:

**Malformed JSON in the request** - caught before the data reaches business logic:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "status": 400,
  "message": "Malformed JSON request. Please check your request body."
}
```

**An unexpected failure on the Paymove side** - always with the same generic message:

```json theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
{
  "status": 500,
  "message": "Something went wrong"
}
```

## Error table

| Status | `message`                                                 | Cause                                                                                | What to do                                                                                                                                                         |
| ------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `Malformed JSON request. Please check your request body.` | The body is not valid JSON, or an enum field holds an unknown value                  | Check the syntax and field types                                                                                                                                   |
| `400`  | `Bank account not found for this partner`                 | The partner's settlement account is not configured                                   | Contact Paymove                                                                                                                                                    |
| `401`  | `Missing credentials`                                     | The `X-API-KEY` header is absent                                                     | Add the header                                                                                                                                                     |
| `401`  | `Invalid API key`                                         | The key was not recognised, expired or was revoked                                   | Check that you use the key for the right environment: `sk_test_` in sandbox, `sk_live_` in production. Generate a new key in the [panel](https://panel.paymove.io) |
| `403`  | `Product <uuid> does not belong to you`                   | The `productId` belongs to another partner                                           | Check that the `productId` matches the key you used                                                                                                                |
| `404`  | `PayProductEntity not found`                              | No product exists with that `productId`                                              | Verify the `productId`                                                                                                                                             |
| `404`  | `WebhookEntity not found`                                 | No webhook exists with that `webhookId`                                              | Verify the `webhookId`                                                                                                                                             |
| `404`  | `PaySubProductEntity not found`                           | No payment exists with that identifier                                               | Check that you use the right identifier                                                                                                                            |
| `500`  | `Something went wrong`                                    | An internal failure, **a missing `externalId`**, **or a malformed UUID in the path** | Check the body is complete and `productId` well-formed first - only then retry                                                                                     |

## What this API does not have

This section matters as much as the table above. The gateway **does not provide** the mechanisms below - writing code that handles them is dead weight and misleading:

| Mechanism                                    | State                                                             |
| -------------------------------------------- | ----------------------------------------------------------------- |
| Rate limiting and status `429`               | Do not exist - the gateway does not throttle calls                |
| Status `422`                                 | Never returned. Validation failures surface as `400` or `500`     |
| `Idempotency-Key` header                     | Does not exist. `externalId` plays the role of an idempotency key |
| API versioning                               | None. There is no `/v1/` prefix and no version header             |
| `Deprecation` / `Sunset` headers             | Never sent                                                        |
| Machine-readable error code (`code`, `type`) | Does not exist. Only `status` and `message` are available         |

## Errors that look like successes

The most dangerous category: the API returns `200` even though the request was wrong. Check these before declaring the integration working.

<Warning>
  **A decimal amount is silently truncated.** `"price": 12.99` is stored as `12` grosze and the response is `200`. Convert with `Math.round(amount * 100)`.
</Warning>

<Warning>
  **Unknown fields are ignored.** Sending `amount` instead of `price`, or `currency` or `returnUrl` at the top level instead of inside `details`, raises no error - the payment is created with incomplete data.
</Warning>

<Warning>
  **A reused `externalId` returns the old payment.** The response is `200` with the previously created `redirectUrl`, and the new amount is discarded. Change amounts via `PATCH /api/pay/product/{productId}/subproduct/{externalId}`.
</Warning>

<Warning>
  **A missing `externalId` returns `500`, not `400`.** Before treating a `500` as transient and retrying, check that your body contains `externalId`.
</Warning>

<Warning>
  **A missing `price` or `details.returnUrl` raises no error at all.** You get `200` and a working `redirectUrl` - to a payment with no amount, or with no way back to your shop. You have to check those fields yourself, before sending the request.
</Warning>

<Warning>
  **A malformed UUID in the path also returns `500`.** A typo in `productId` does not produce a readable `400` - you get the generic `Something went wrong`.
</Warning>

## Handling errors in code

```javascript theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
const response = await fetch(url, options);

if (!response.ok) {
  const error = await response.json(); // { status, message }

  switch (response.status) {
    case 401:
    case 403:
      // Configuration problem - retrying will not help
      throw new PaymentConfigError(error.message);
    case 404:
      throw new PaymentNotFoundError(error.message);
    case 400:
      // Bad request - fix the data, do not retry
      throw new PaymentRequestError(error.message);
    case 500:
      // Verify the body is complete first, only then retry
      throw new PaymentServerError(error.message);
    default:
      throw new Error(`Paymove ${response.status}: ${error.message}`);
  }
}
```

<Info>
  Retrying makes sense only for `500` and network failures, and only after confirming the request was complete. Statuses `400`, `401`, `403` and `404` indicate a problem on your side - a retry returns the same result.
</Info>

## SDK errors

The [Node.js SDK](/en/sdk/javascript) wraps the responses above in typed exceptions:

| Class                    | When                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `PaymoveValidationError` | An argument was rejected by the SDK before the request was sent. The `field` property names the parameter |
| `PaymoveApiError`        | The API returned a non-2xx status. `statusCode` and `responseBody` hold the original response             |
| `PaymoveNetworkError`    | The request never completed. `cause` holds the underlying exception                                       |

<Warning>
  The SDK's `amount` validation only checks that the value is a number greater than zero. An amount of `49.99` passes that check and the API truncates it to 49 grosze.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="REST API" icon="code" href="/en/rest-api">
    The correct request structure and full responses.
  </Card>

  <Card title="Signature verification" icon="shield-check" href="/en/webhook-signature">
    Error handling on the webhook-receiving side.
  </Card>
</CardGroup>
