@gopaycz/gopay-js-sdk 1.6.6 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,56 @@
1
+ # [1.8.0](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/compare/1.7.1...1.8.0) (2026-07-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * suppress js-yaml CVE-1121860 pending age-gate (audit) GPOMA-2398 ([2cc80b6](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/2cc80b69082d3b851b17cc52da8a7a76a8e7eb41))
7
+
8
+
9
+ ### Features
10
+
11
+ * **ci:** introduce internal README and stop mirroring it to GitHub GPOMA-2398 ([11c0ff9](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/11c0ff99c1e56f867c37fd2c67b9e9328ac6ec08))
12
+
13
+ ## [1.7.1](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/compare/1.7.0...1.7.1) (2026-06-29)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * add fancy npm badges GPOMA-2375 ([9a8f3a7](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/9a8f3a76661d069a5b23791759062e3b39a8b21a))
19
+
20
+ # [1.7.0](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/compare/1.6.7...1.7.0) (2026-06-29)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * use api from production link GPOMA-2375 ([51e1574](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/51e157458f0f54a18a1460f4754df3c5103a560f))
26
+ * **audit:** suppress git-raw-commits deprecation GPOMA-2375 ([35f2379](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/35f237916d8581427bb9b358ffa20e0c07e9fc82))
27
+ * **browser-sdk:** guard attachPayment() against re-call while card form mounted D1 GPOMA-2375 ([41fa86d](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/41fa86d19957c6e1b0c94c491f2dfb8db438330e))
28
+ * **cards,polling,wallets,http:** code bugs B1 B2 B3 B6 D3 GPOMA-2375 ([55228e3](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/55228e3c5e0de30fa0934fddb31ff53ef6f2b5a7))
29
+ * **cards:** use cardFormSessionActive flag to close race in isCardFormMounted (D1) GPOMA-2375 ([b1d85c5](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/b1d85c5531b512c2d7160c693fe22c92e40d1cf9))
30
+ * **polling:** forward abort signal to per-poll client.get() GPOMA-2375 ([5d36270](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/5d36270589fe92e6b690276e47dd474bf99d58e8))
31
+ * update push instructions GPOMA-2375 ([160ce7e](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/160ce7e98c1143abe34e081153cc5bec7c418027))
32
+
33
+
34
+ ### Features
35
+
36
+ * **api:** wire AbortSignal through public payment methods (D4) GPOMA-2375 ([a23bd08](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/a23bd08e6b99fa89901a7813f581adfdfae91a2d))
37
+ * **errors,polling:** attach chargeState to CHARGE_FAILED error for catch callers D2 GPOMA-2375 ([26f1f38](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/26f1f38470c47b008684de17eb742f0c820ba1a3))
38
+ * **http:** add AbortSignal to RequestOptions and thread into all fetch calls D4 GPOMA-2375 ([81c2721](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/81c27211fabefc5de47b7211aad9b73403bc5755))
39
+ * **payments:** add awaitPaymentStatus() polling helper for QR and bank-transfer flows M1 GPOMA-2375 ([10b447a](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/10b447a8202607afc9b737aa44f4ae7ed8a1fce3))
40
+ * **scopes:** export GoPayScopes constants and combineScopes helper M2 GPOMA-2375 ([74b94f8](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/74b94f84e2c815789c4842d088e7ddc43eba8e64))
41
+
42
+ ## [1.6.7](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/compare/1.6.6...1.6.7) (2026-06-24)
43
+
44
+
45
+ ### Bug Fixes
46
+
47
+ * clarify theming link GPOMA-2364 ([dca521f](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/dca521f92fce78714d5ae5ee0ae2052f75f160f4))
48
+ * rename env filename GPOMA-2364 ([5eed047](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/5eed047b701a3350370c6da2467bdcb2bbacf356))
49
+ * rename remaining GP_GW_JS_SDK_* refs and make BASE_URL optional in e2e config GPOMA-2364 ([4e4774b](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/4e4774b1a8e4f5948f8674e3805568ba9aa4b445))
50
+ * run tests on local node GPOMA-2364 ([a54e7fa](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/a54e7facda750dcc423fdca0ae7142b93cc9344d))
51
+ * update env example GPOMA-2364 ([fe2a3ea](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/fe2a3eaa86ab5ef2c77ed3e2016f3069b5d5ff2e))
52
+ * update README env var names to GOPAY_PAYMENTS_V4_ prefix GPOMA-2364 ([cb3a07a](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/commits/cb3a07a056a10fd3d90f0d20fa147e7ddfb4a34f))
53
+
1
54
  ## [1.6.6](https://bitbucket.org/gp-gopay/gp-gw-js-sdk/compare/1.6.5...1.6.6) (2026-06-17)
2
55
 
3
56
 
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # @gopaycz/gopay-js-sdk
2
2
 
3
- GoPay JavaScript SDK for server-side use (Node.js) — wraps the GoPay Payments API v4.0.
3
+ [![npm](https://img.shields.io/npm/v/@gopaycz/gopay-js-sdk)](https://www.npmjs.com/package/@gopaycz/gopay-js-sdk)
4
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gp-gopay_gp-gw-js-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=gp-gopay_gp-gw-js-sdk)
5
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=gp-gopay_gp-gw-js-sdk&metric=coverage)](https://sonarcloud.io/summary/new_code?id=gp-gopay_gp-gw-js-sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ GoPay JavaScript SDK for server-side use (Node.js) — wraps the new [GoPay Payments API v4](https://api-docs.gopay.com/).
4
9
 
5
10
  > **Building a browser integration?** Use [`@gopaycz/gopay-js-sdk-browser`](../browser-sdk/README.md) — it handles in-browser card encryption, direct-charge flows, and exposes an IIFE bundle for CDN use.
6
11
 
@@ -50,14 +55,15 @@ const payment = await sdk.createPayment('YOUR_GOID', {
50
55
  Once the customer completes the payment flow and returns to your site, charge the payment using the instrument they provided:
51
56
 
52
57
  ```ts
53
- // Card token (obtained via sdk.mountCardForm() returns an object, use .token)
58
+ // Card token: obtained via the browser SDK card form (see @gopaycz/gopay-js-sdk-browser)
59
+ // and forwarded to your server after tokenization via sdk.tokenizeEncryptedCard()
54
60
  const charge = await sdk.chargePayment(payment.id, {
55
61
  payment_instrument: {
56
62
  payment_instrument: 'PAYMENT_CARD',
57
63
  input: {
58
64
  input_type: 'CARD_TOKEN',
59
65
  card_token: cardToken.token,
60
- challenge_preferrence: 'AUTO',
66
+ challenge_preference: 'AUTO',
61
67
  },
62
68
  },
63
69
  });
@@ -82,7 +88,7 @@ The SDK uses OAuth2. **Client credentials (`client_id` / `client_secret`) must n
82
88
  Your backend holds the credentials. Call `authenticate()` once on startup — the SDK stores the token and refreshes it transparently before expiry:
83
89
 
84
90
  ```ts
85
- import { createGoPaySDK } from '@gopaycz/gopay-js-sdk';
91
+ import { createGoPaySDK, GoPayScopes } from '@gopaycz/gopay-js-sdk';
86
92
 
87
93
  const sdk = createGoPaySDK({ environment: 'production' });
88
94
 
@@ -90,13 +96,27 @@ await sdk.authenticate({
90
96
  grant_type: 'client_credentials',
91
97
  client_id: process.env.GOPAY_CLIENT_ID,
92
98
  client_secret: process.env.GOPAY_CLIENT_SECRET,
93
- scope: 'payment:write payment:read',
99
+ scope: GoPayScopes.PAYMENT_WRITE + ' ' + GoPayScopes.PAYMENT_READ,
100
+ // or: scope: combineScopes(GoPayScopes.PAYMENT_WRITE, GoPayScopes.PAYMENT_READ)
101
+ // or just a plain string: scope: 'payment:write payment:read'
94
102
  });
95
103
 
96
104
  // All subsequent calls attach the Bearer token automatically.
97
105
  const payment = await sdk.createPayment(goid, params);
98
106
  ```
99
107
 
108
+ ### Scopes reference
109
+
110
+ | Scope | Constant | Used by |
111
+ |---|---|---|
112
+ | `payment:write` | `GoPayScopes.PAYMENT_WRITE` | `createPayment`, `chargePayment`, `refundPayment`, recurrences, payment links |
113
+ | `payment:read` | `GoPayScopes.PAYMENT_READ` | `getPaymentStatus`, `getChargeState`, `listRefunds`, `getRefund`, recurrence status |
114
+ | `card:write` | `GoPayScopes.CARD_WRITE` | `getBrowserKeys()` — allows the browser SDK to present the card form |
115
+ | `card:read` | `GoPayScopes.CARD_READ` | `getCardDetails`, `deleteCard` |
116
+ | `payment:charge` | `GoPayScopes.PAYMENT_CHARGE` | Browser SDK only (`attachPayment` / `payment_credentials` grant) |
117
+
118
+ For `getBrowserKeys()` to work, include `card:write` in your server-side `authenticate()` call. `shareableKey` must also be set in the SDK config (see [Browser keys](#browser-keys) below). Include `payment:write` (and any other scopes) as needed for the rest of your integration.
119
+
100
120
  ---
101
121
 
102
122
  ## Configuration
@@ -153,6 +173,28 @@ The one exception is `startApplePaySession`: it returns `void` instead of a Prom
153
173
  | `isAuthenticated()` | Returns `true` if a token is currently stored. Does not check expiry — expired tokens are refreshed transparently on the next API call. |
154
174
  | `logout()` | Clear all stored tokens and credentials. All subsequent API calls will throw until the SDK is re-authenticated. |
155
175
 
176
+ #### Browser keys
177
+
178
+ `getBrowserKeys()` returns `{ shareable_key, client_id }` — pass both to the browser SDK via your own API endpoint.
179
+
180
+ **Prerequisites:**
181
+ - The `shareableKey` option must be set when creating the SDK:
182
+ ```ts
183
+ const sdk = createGoPaySDK({
184
+ environment: 'production',
185
+ shareableKey: process.env.GOPAY_SHAREABLE_KEY, // issued in GoPay admin alongside client_id
186
+ });
187
+ ```
188
+ - The token scope must include `card:write` (obtain `shareableKey` from the GoPay admin portal):
189
+ ```ts
190
+ await sdk.authenticate({
191
+ ...,
192
+ scope: 'payment:write payment:read card:write',
193
+ });
194
+ ```
195
+
196
+ Throws `AUTH_CREDENTIALS_MISSING` if either prerequisite is missing.
197
+
156
198
  ### Payments
157
199
 
158
200
  | Method | Description |
@@ -313,127 +355,72 @@ document.body.appendChild(img);
313
355
 
314
356
  ### Cards
315
357
 
358
+ > **SDK boundary:** card data encryption and the card form UI live in the **browser SDK** ([`@gopaycz/gopay-js-sdk-browser`](../browser-sdk/README.md)), not here. The browser encrypts the card inside a GoPay-hosted iframe and returns an `encryptedPayload`; your server calls `tokenizeEncryptedCard` to convert it to a card token, then charges the payment. Raw card data never touches your server or this SDK.
359
+
316
360
  | Method | Description |
317
361
  |---|---|
318
- | `mountCardForm(container, options?)` | Fetches the GoPay-hosted iframe URL from `GET /encryption/card-form-url`, mounts it into `container`, and returns `Promise<CardFormController>`. The controller exposes a `result` promise (resolves to the card token on submit), `setTheme()`, `setLocale()`, `submit()`, and `isValid` for runtime control. Handles iframe creation, internal communication, and `POST /cards/tokens` internally. Requires the `card:write` scope. |
319
- | `getCardDetails(cardId)` | Retrieve details of a stored permanent card token (`GET /cards/tokens/{cardId}`). Returns masked PAN, expiry, scheme, fingerprint, and the reusable token. Requires the `card:read` scope. |
320
- | `deleteCard(cardId)` | Delete a stored permanent card token (`DELETE /cards/tokens/{cardId}`). Returns `void`. |
321
-
322
- #### `mountCardForm` options
323
-
324
- | Option | Type | Default | Description |
325
- |---|---|---|---|
326
- | `theme` | `CardFormTheme` | `DEFAULT_CARD_FORM_THEME` | Visual theme (colors, font sizes, spacing, button styles). All fields are optional — the iframe applies built-in defaults for any omitted field. |
327
- | `locale` | `string` | `navigator.language` | BCP 47 language tag for form labels and placeholders. The region subtag is ignored (`'cs-CZ'` → `'cs'`). Unknown locales fall back to English. Supported: `bg` `cs` `de` `en` `es` `et` `fr` `hr` `hu` `it` `lt` `lv` `nl` `pl` `pt` `ro` `ru` `sk` `sl` `uk`. |
328
- | `submitMode` | `'internal' \| 'external'` | `'internal'` | `'internal'` — the iframe renders its own submit button. `'external'` — the iframe hides the button; the parent controls submission via `cardForm.submit()` and receives validity changes via `onValidityChange`. |
329
- | `permanent` | `boolean` | `false` | When `true`, a permanent (reusable) card token is created that can be charged again for future payments. When `false` (default), a single-use token is created. |
330
- | `onValidityChange` | `(isValid: boolean) => void` | — | Called whenever the overall form validity changes in external submit mode. Use this to enable/disable your external submit button. |
331
-
332
- > **Send theme and locale at init time.** Both options are applied before the form is first painted, avoiding a flash of unstyled content. Use `cardForm.setTheme()` and `cardForm.setLocale()` on the returned controller if you need to update them after mounting.
333
-
334
- `CardFormTheme` is fully typed — hover in your editor to see every available field. Notable fields:
362
+ | `tokenizeEncryptedCard(encryptedPayload)` | Convert the encrypted payload returned by the browser SDK into a card token (`POST /cards/tokens`). Requires `card:write` scope. Returns `{ token, card_id? }`. |
363
+ | `getCardDetails(cardId)` | Retrieve details of a stored permanent card token (`GET /cards/tokens/{cardId}`). Returns masked PAN, expiry, scheme, fingerprint, and the reusable token. Requires `card:read` scope. |
364
+ | `deleteCard(cardId)` | Delete a stored permanent card token (`DELETE /cards/tokens/{cardId}`). Returns `void`. Requires `card:read` scope. |
335
365
 
336
- | Field | Type | Default | Description |
337
- |---|---|---|---|
338
- | `fontFamily` | `string` | `system-ui, sans-serif` | CSS font-family stack applied to all form text (labels, inputs, errors, submit button). Use system font stacks — e.g. `"Inter, system-ui, sans-serif"` — to match your page without loading external fonts. |
366
+ ### Flow A server-side card charge
339
367
 
340
- > **Color format restriction.** All color fields in `CardFormTheme` must use one of the following CSS formats: `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb(...)`, `rgba(...)`, `hsl(...)`, `hsla(...)`, or `transparent`. Values that do not match are silently replaced with `unset` (browser default). Named colors (e.g. `red`) and other CSS values are not accepted.
341
-
342
- Theme presets are exported from the package:
368
+ The browser encrypts the card; your server tokenizes and charges. This is the recommended flow when your server handles the charge (e.g. to save the token for future use).
343
369
 
344
370
  ```ts
345
- import {
346
- GoPaySDK,
347
- DEFAULT_CARD_FORM_THEME,
348
- DARK_CARD_FORM_THEME,
349
- } from '@gopaycz/gopay-js-sdk';
350
-
351
- // Default theme, locale from navigator.language (options can be omitted entirely)
352
- const cardForm = await sdk.mountCardForm(container);
353
- const cardToken = await cardForm.result;
354
-
355
- // Dark theme with Czech locale
356
- const cardForm = await sdk.mountCardForm(container, {
357
- theme: DARK_CARD_FORM_THEME,
358
- locale: 'cs',
359
- });
360
- const cardToken = await cardForm.result;
361
-
362
- // Custom theme — override individual fields, keep the rest as defaults
363
- const cardForm = await sdk.mountCardForm(container, {
364
- theme: { ...DEFAULT_CARD_FORM_THEME, submitBackgroundColor: '#your-brand-color' },
365
- });
366
-
367
- // Update theme or locale at runtime (e.g. user toggles dark mode or language)
368
- darkModeToggle.addEventListener('change', () => cardForm.setTheme(DARK_CARD_FORM_THEME));
369
- langSelect.addEventListener('change', e => cardForm.setLocale(e.target.value));
371
+ // ── Browser (using @gopaycz/gopay-js-sdk-browser) ──────────────────────────
372
+ import { createGoPayBrowserSDK, collectBrowserData } from '@gopaycz/gopay-js-sdk-browser';
370
373
 
371
- const cardToken = await cardForm.result;
372
- ```
373
-
374
- #### External submit button
375
-
376
- Use `submitMode: 'external'` when you want the submit button to live outside the iframe — for example to match your own checkout UI. The iframe hides its built-in button, reports validity changes via `onValidityChange`, and waits for `cardForm.submit()` to trigger encryption.
374
+ const browserSdk = createGoPayBrowserSDK({ environment: 'production', shareableKey, clientId });
375
+ const controller = await browserSdk.mountCardForm(container, { flow: 'return-payload' });
376
+ const { encryptedPayload } = await controller.result;
377
377
 
378
- ```ts
379
- const payBtn = document.getElementById('pay-btn');
378
+ // Collect browser data for 3DS — must be done in the browser
379
+ const browserData = collectBrowserData();
380
380
 
381
- const cardForm = await sdk.mountCardForm(container, {
382
- submitMode: 'external',
383
- onValidityChange(isValid) {
384
- // Enable/disable your own submit button based on form validity.
385
- payBtn.disabled = !isValid;
386
- },
381
+ // Forward both to your server endpoint
382
+ await fetch('/api/charge', {
383
+ method: 'POST',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ paymentId, encryptedPayload, browserData }),
387
386
  });
388
387
 
389
- payBtn.addEventListener('click', () => {
390
- cardForm.submit(); // triggers encryption inside the iframe
391
- });
392
-
393
- // Await the result exactly the same as in internal submit.
394
- const cardToken = await cardForm.result;
395
- ```
396
-
397
- `cardForm.isValid` is also available as a synchronous getter in case you need to read the current validity state imperatively (e.g. inside a click handler before calling `submit()`).
398
-
399
- > Calling `submit()` in internal submit mode throws a `GoPaySDKError`. Calling it after the form completes or is unmounted is a no-op.
400
-
401
- ### Card Pay
388
+ // ── Server (using @gopaycz/gopay-js-sdk) ───────────────────────────────────
389
+ import { createGoPaySDK } from '@gopaycz/gopay-js-sdk';
402
390
 
403
- Card number encryption must never run in publicly reachable JavaScript — doing so would expose the raw PAN to any script on the merchant's page. The SDK uses a GoPay-hosted iframe for this step so that raw card data never touches merchant code.
391
+ const sdk = createGoPaySDK({ environment: 'production' });
392
+ await sdk.authenticate({ ..., scope: 'payment:write payment:read card:write' });
404
393
 
405
- ```ts
406
- // 1. Create a payment session first (server-side).
407
- const payment = await sdk.createPayment(goid, params);
394
+ // Handler for POST /api/charge
395
+ const { encryptedPayload, paymentId, browserData } = req.body;
408
396
 
409
- // 2. Mount the iframe the SDK fetches the hosted form URL from the API,
410
- // mounts the iframe, then awaits the user submitting the card form,
411
- // and calls POST /cards/tokens automatically.
412
- const container = document.getElementById('card-form-container');
413
- const cardForm = await sdk.mountCardForm(container);
414
- const cardToken = await cardForm.result;
397
+ // 1. Convert encrypted payload to a card token
398
+ const cardToken = await sdk.tokenizeEncryptedCard(encryptedPayload);
415
399
 
416
- // 3. Charge the payment with the resulting card token.
417
- const charge = await sdk.chargePayment(payment.id, {
400
+ // 2. Charge the payment always forward browserData from the browser for 3DS
401
+ const charge = await sdk.chargePayment(paymentId, {
418
402
  payment_instrument: {
419
403
  payment_instrument: 'PAYMENT_CARD',
420
404
  input: {
421
405
  input_type: 'CARD_TOKEN',
422
406
  card_token: cardToken.token,
423
- challenge_preferrence: 'AUTO',
407
+ challenge_preference: 'AUTO',
408
+ browser_data: browserData, // required for 3DS — do not omit
424
409
  },
425
410
  },
426
411
  });
427
412
 
428
413
  if (charge.action?.redirect_url) {
429
- // 3DS authentication required — redirect the customer.
430
- window.location.href = charge.action.redirect_url;
414
+ // 3DS authentication required — redirect the customer
415
+ res.json({ redirectUrl: charge.action.redirect_url });
431
416
  }
432
417
  ```
433
418
 
419
+ > **`browser_data` is required for 3DS.** The server SDK does not auto-collect it (only the browser SDK can). Always forward it from the browser via `collectBrowserData()`. Without it, 3DS context is absent and approval rates degrade.
420
+
434
421
  ### Saved cards
435
422
 
436
- When `mountCardForm` is called with `permanent: true`, the resulting `cardToken.token` is a permanent token tied to a card ID (`cardToken.card_id`). You can retrieve or delete it later:
423
+ To save a card for future payments, pass `permanent: true` to `mountCardForm` in the browser SDK. The resulting `cardToken.card_id` can be stored and used to charge again without re-entering card details:
437
424
 
438
425
  ```ts
439
426
  // Retrieve details of a stored card (masked PAN, expiry, scheme, etc.)
@@ -452,8 +439,6 @@ const charge = await sdk.chargePayment(payment.id, {
452
439
  await sdk.deleteCard(card.card_id);
453
440
  ```
454
441
 
455
- `GET /encryption/public-key` and the JWE construction it enables are **intentionally not part of this SDK's API surface**.
456
-
457
442
  ---
458
443
 
459
444
  ### Recurrences
@@ -600,6 +585,23 @@ try {
600
585
  | `status` | `number` | HTTP status code (e.g. `401`, `422`). |
601
586
  | `body` | `unknown` | Parsed JSON response body, or raw text if JSON parsing failed. |
602
587
 
588
+ A 422 error from `chargePayment` with an invalid amount looks like:
589
+
590
+ ```json
591
+ {
592
+ "date_time": "2024-03-15T14:22:00.000+0100",
593
+ "error_code": 400,
594
+ "error_name": "PAYMENT_VALIDATION_ERROR",
595
+ "message": [
596
+ {
597
+ "field": "amount",
598
+ "message": "INVALID",
599
+ "description": "Value 0 is out of allowed range [1, 99999999]"
600
+ }
601
+ ]
602
+ }
603
+ ```
604
+
603
605
  ### Centralised error handling with `onError`
604
606
 
605
607
  Pass an `onError` callback to the SDK config to intercept every error in one place — useful for logging or alerting — without replacing per-call `catch` blocks: