@garuhq/node 4.1.0 → 5.1.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
@@ -3,6 +3,55 @@
3
3
  All notable changes to `@garuhq/node` are documented in this file. Format:
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
5
5
 
6
+ ## [5.0.0] — 2026-09-09
7
+
8
+ **Breaking:** requests that omit `idempotencyKey` no longer carry an
9
+ `X-Idempotency-Key` header. No API contract changed and no gateway endpoint
10
+ requires the header, so most integrations need no code change — but if you were
11
+ relying on a key always being present, you must now pass one, which is the only
12
+ way it ever gave you real protection. See below.
13
+
14
+ ### Fixed
15
+
16
+ - **The SDK no longer invents an idempotency key.** Every write that takes
17
+ `idempotencyKey` (`charges.create`, `charges.refund`, `customers.create`,
18
+ `products.create`, `scheduledCharges.create`, `installmentPlans.create`,
19
+ `installmentPlans.requestRefund`, `webhookEvents.resend`) used to generate a
20
+ fresh UUIDv4 when you omitted one. That protected nothing: an idempotency key
21
+ only works if the SAME key comes back on a retry, and a key invented per call
22
+ is different every time. The header is now sent only when you pass a key.
23
+
24
+ This was not theoretical. On 2026-09-08 an integrator's HTTP client timed out
25
+ at 30s on a card charge that was still being created, retried, drew a fresh
26
+ UUIDv4, and charged a real buyer twice.
27
+
28
+ Docstrings that promised "Safe to retry: the same key returns the original
29
+ charge for 24h" were describing a guarantee the code did not provide. They now
30
+ say what actually happens.
31
+
32
+ ### Changed
33
+
34
+ - Behaviour change, no API change: requests where you omit `idempotencyKey` now
35
+ carry no `X-Idempotency-Key` header. Nothing rejects a missing key — the header
36
+ is optional on every gateway endpoint — and the gateway also gained a duplicate
37
+ guard that replays the original charge when the same buyer, product, rail and
38
+ instalment count arrive again within 60 seconds.
39
+
40
+ **To get real protection, pass a key derived from your own domain** so a retry
41
+ reproduces it:
42
+
43
+ ```ts
44
+ await garu.charges.create({
45
+ productId,
46
+ paymentMethod: 'creditCard',
47
+ customer,
48
+ idempotencyKey: `booking:${booking.id}:charge`
49
+ });
50
+ ```
51
+
52
+ - `generateIdempotencyKey()` is still exported and unchanged. It is only useful
53
+ if you store the result and reuse it across retries of the same operation.
54
+
6
55
  ## [4.1.0] — 2026-08-22
7
56
 
8
57
  ### Added
package/dist/index.cjs CHANGED
@@ -76,6 +76,15 @@ var GaruRateLimitError = class extends GaruAPIError {
76
76
  this.retryAfterSec = retryAfterSec;
77
77
  }
78
78
  };
79
+ var GaruDuplicateChargeError = class extends GaruAPIError {
80
+ /** How long to wait before sending the same request again. */
81
+ retryAfterSec;
82
+ constructor(code, message, status, requestId, body, retryAfterSec) {
83
+ super(code, message, status, requestId, body);
84
+ this.name = "GaruDuplicateChargeError";
85
+ this.retryAfterSec = retryAfterSec;
86
+ }
87
+ };
79
88
  var GaruServerError = class extends GaruAPIError {
80
89
  constructor(message, status, requestId, body) {
81
90
  super("server_error", message, status, requestId, body);
@@ -93,6 +102,13 @@ function mapApiError(status, body, requestId, retryAfterSec) {
93
102
  if (status === 429) {
94
103
  return new GaruRateLimitError(message, status, requestId, body, retryAfterSec);
95
104
  }
105
+ if (status === 409) {
106
+ const code = readDuplicateChargeCode(body);
107
+ if (code) {
108
+ const wait = retryAfterSec ?? readRetryAfterFromBody(body) ?? DEFAULT_DUPLICATE_RETRY_SEC;
109
+ return new GaruDuplicateChargeError(code, message, status, requestId, body, wait);
110
+ }
111
+ }
96
112
  if (status >= 500) return new GaruServerError(message, status, requestId, body);
97
113
  return new GaruAPIError("api_error", message, status, requestId, body);
98
114
  }
@@ -105,6 +121,15 @@ function extractMessage(body) {
105
121
  }
106
122
  return null;
107
123
  }
124
+ var DEFAULT_DUPLICATE_RETRY_SEC = 5;
125
+ function readDuplicateChargeCode(body) {
126
+ const code = body?.error;
127
+ return code === "charge_in_progress" || code === "charge_already_processed" ? code : null;
128
+ }
129
+ function readRetryAfterFromBody(body) {
130
+ const value = body?.retryAfter;
131
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
132
+ }
108
133
 
109
134
  // src/http.ts
110
135
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
@@ -188,8 +213,8 @@ function backoffDelay(attempt, retryAfterSec) {
188
213
  function sleep(ms) {
189
214
  return new Promise((resolve) => setTimeout(resolve, ms));
190
215
  }
191
- function generateIdempotencyKey() {
192
- return crypto.randomUUID();
216
+ function idempotencyHeaders(key) {
217
+ return key ? { "X-Idempotency-Key": key } : {};
193
218
  }
194
219
 
195
220
  // src/resources/charges.ts
@@ -201,9 +226,11 @@ var Charges = class {
201
226
  /**
202
227
  * Create a charge (PIX, boleto, or credit card).
203
228
  *
204
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
205
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
206
- * returns the original charge for 24h.
229
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
230
+ * original charge for 24h. Derive it from something stable in your own
231
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
232
+ * no key is sent — the SDK does NOT invent one, because a key generated per
233
+ * call is different every time and protects nothing.
207
234
  *
208
235
  * @example
209
236
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -234,9 +261,22 @@ var Charges = class {
234
261
  * }
235
262
  * });
236
263
  * // charge.amount is the base price; charge.chargedTotal is what was charged.
264
+ *
265
+ * @example
266
+ * // Handling a duplicate. A 409 means an identical charge is already being
267
+ * // processed, or already went through — NOT that this one failed. Send the
268
+ * // same request again after the wait and you get the ORIGINAL charge back.
269
+ * // The SDK will not retry it for you.
270
+ * try {
271
+ * await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
272
+ * } catch (err) {
273
+ * if (err instanceof GaruDuplicateChargeError) {
274
+ * await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
275
+ * // retry the same call
276
+ * }
277
+ * }
237
278
  */
238
279
  async create(params) {
239
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
240
280
  const body = {
241
281
  productId: params.productId,
242
282
  paymentMethod: params.paymentMethod,
@@ -245,7 +285,7 @@ var Charges = class {
245
285
  if (params.card) body.card = params.card;
246
286
  if (params.checkoutSessionToken) body.checkoutSessionToken = params.checkoutSessionToken;
247
287
  if (params.additionalInfo !== void 0) body.additionalInfo = params.additionalInfo;
248
- return this.post("/api/v1/charges", body, { "X-Idempotency-Key": idempotencyKey });
288
+ return this.post("/api/v1/charges", body, idempotencyHeaders(params.idempotencyKey));
249
289
  }
250
290
  /**
251
291
  * Retrieve a charge by uuid.
@@ -286,23 +326,24 @@ var Charges = class {
286
326
  * settles.
287
327
  *
288
328
  * For Pix/boleto (which open a refund request instead of an automated
289
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
290
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
291
- * card, which reverses automatically and has no manual request to
292
- * duplicate.
329
+ * reversal), pass `idempotencyKey` to make a retry return the original
330
+ * request rather than opening a second one; omit it and no key is sent.
331
+ * Ignored for card, which reverses automatically and has no manual request
332
+ * to duplicate.
293
333
  *
294
334
  * @example
295
335
  * await garu.charges.refund('6f1c9b2e-...'); // full
296
336
  * await garu.charges.refund('6f1c9b2e-...', { amount: 10.0 }); // R$10,00
297
337
  */
298
338
  async refund(uuid, params = {}) {
299
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
300
339
  const body = {};
301
340
  if (params.amount !== void 0) body.amount = params.amount;
302
341
  if (params.reason !== void 0) body.reason = params.reason;
303
- return this.post(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, {
304
- "X-Idempotency-Key": idempotencyKey
305
- });
342
+ return this.post(
343
+ `/api/v1/charges/${encodeURIComponent(uuid)}/refund`,
344
+ body,
345
+ idempotencyHeaders(params.idempotencyKey)
346
+ );
306
347
  }
307
348
  /**
308
349
  * Cancel an unpaid charge.
@@ -343,10 +384,16 @@ var InstallmentPlans = class {
343
384
  }
344
385
  http;
345
386
  /**
346
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
347
- * you don't pass `idempotencyKey`), which matters more here than anywhere
348
- * else in the API: this call registers a REAL boleto at the bank, so a
349
- * blind retry can put two payable barcodes in one buyer's hands.
387
+ * Sell a product as a carnê.
388
+ *
389
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
390
+ * original plan for 24h. Derive it from something stable in your own
391
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
392
+ * no key is sent — the SDK does NOT invent one, because a key generated per
393
+ * call is different every time and protects nothing.
394
+ * This matters more here than anywhere else in the API: the call registers a
395
+ * REAL boleto at the bank, so a retry without a key can put two payable
396
+ * barcodes in one buyer's hands.
350
397
  *
351
398
  * @example
352
399
  * const carne = await garu.installmentPlans.create({
@@ -372,12 +419,11 @@ var InstallmentPlans = class {
372
419
  * });
373
420
  */
374
421
  async create(params) {
375
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
376
422
  const { idempotencyKey: _omit, ...body } = params;
377
423
  return this.http.call(
378
424
  (signal) => this.http.client.POST("/api/v1/installment-plans", {
379
425
  body,
380
- headers: { "X-Idempotency-Key": idempotencyKey },
426
+ headers: idempotencyHeaders(params.idempotencyKey),
381
427
  signal
382
428
  }).then((r) => r)
383
429
  );
@@ -520,10 +566,9 @@ var InstallmentPlans = class {
520
566
  * team. Transfer the money to the buyer yourself, then close it with
521
567
  * `garu.refundRequests.confirm`.
522
568
  *
523
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
524
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
525
- * a second pending request for the same carnê, so this is mainly
526
- * defense-in-depth for the request-in-flight window.
569
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
570
+ * key is sent. The backend already dedupes a second pending request for the
571
+ * same carnê, so this is defense-in-depth rather than the main guard.
527
572
  *
528
573
  * @example
529
574
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -533,12 +578,11 @@ var InstallmentPlans = class {
533
578
  * request.amount; // defaults to everything the carnê collected
534
579
  */
535
580
  async requestRefund(uuid, params = {}) {
536
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
537
581
  const { idempotencyKey: _omit, ...body } = params;
538
582
  return this.http.call(
539
583
  (signal) => this.http.client.POST(`/api/v1/installment-plans/${uuid}/refund-requests`, {
540
584
  body,
541
- headers: { "X-Idempotency-Key": idempotencyKey },
585
+ headers: idempotencyHeaders(params.idempotencyKey),
542
586
  signal
543
587
  }).then((r) => r)
544
588
  );
@@ -646,9 +690,11 @@ var Customers = class {
646
690
  /**
647
691
  * Register a customer for the current seller.
648
692
  *
649
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
650
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
651
- * returns the originally-created/matched customer for 24h.
693
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
694
+ * original customer for 24h. Derive it from something stable in your own
695
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
696
+ * no key is sent — the SDK does NOT invent one, because a key generated per
697
+ * call is different every time and protects nothing.
652
698
  *
653
699
  * @example
654
700
  * const customer = await garu.customers.create({
@@ -661,12 +707,11 @@ var Customers = class {
661
707
  * customer.uuid;
662
708
  */
663
709
  async create(params) {
664
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
665
710
  const { idempotencyKey: _omit, ...body } = params;
666
711
  return this.http.call(
667
712
  (signal) => this.http.client.POST("/api/v1/customers", {
668
713
  body,
669
- headers: { "X-Idempotency-Key": idempotencyKey },
714
+ headers: idempotencyHeaders(params.idempotencyKey),
670
715
  signal
671
716
  }).then((r) => r)
672
717
  );
@@ -911,10 +956,11 @@ var Products = class {
911
956
  * product (HTTP 201). Only `name` is required; everything else falls back
912
957
  * to seller/server defaults.
913
958
  *
914
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
915
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
916
- * retry on transient failures safe: a retried POST returns the original
917
- * product instead of creating a duplicate.
959
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
960
+ * original product for 24h. Derive it from something stable in your own
961
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
962
+ * no key is sent — the SDK does NOT invent one, because a key generated per
963
+ * call is different every time and protects nothing.
918
964
  *
919
965
  * @example
920
966
  * const product = await garu.products.create({
@@ -930,11 +976,10 @@ var Products = class {
930
976
  */
931
977
  async create(params) {
932
978
  const { idempotencyKey, ...body } = params;
933
- const key = idempotencyKey ?? generateIdempotencyKey();
934
979
  return this.http.call(
935
980
  (signal) => this.http.client.POST("/api/v1/products", {
936
981
  body,
937
- headers: { "X-Idempotency-Key": key },
982
+ headers: idempotencyHeaders(idempotencyKey),
938
983
  signal
939
984
  }).then((r) => r)
940
985
  );
@@ -970,10 +1015,15 @@ var ScheduledCharges = class {
970
1015
  }
971
1016
  http;
972
1017
  /**
973
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
974
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
975
- * UUIDv4. Safe to retry: the same key returns the originally-created
976
- * series for 24h instead of double-booking recurring billing.
1018
+ * Create a new scheduled charge.
1019
+ *
1020
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1021
+ * original series for 24h. Derive it from something stable in your own
1022
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1023
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1024
+ * call is different every time and protects nothing.
1025
+ * Worth the effort here: without a key, a retry double-books recurring
1026
+ * billing for the same customer.
977
1027
  *
978
1028
  * @example
979
1029
  * const charge = await garu.scheduledCharges.create({
@@ -1000,12 +1050,11 @@ var ScheduledCharges = class {
1000
1050
  * });
1001
1051
  */
1002
1052
  async create(params) {
1003
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
1004
1053
  const { idempotencyKey: _omit, ...body } = params;
1005
1054
  return this.http.call(
1006
1055
  (signal) => this.http.client.POST("/api/v1/scheduled-charges", {
1007
1056
  body,
1008
- headers: { "X-Idempotency-Key": idempotencyKey },
1057
+ headers: idempotencyHeaders(params.idempotencyKey),
1009
1058
  signal
1010
1059
  }).then((r) => r)
1011
1060
  );
@@ -1394,11 +1443,10 @@ var WebhookEvents = class {
1394
1443
  * original — distinguishable both by the `resend_` prefix and by reading
1395
1444
  * the response payload's `manualResendOf` field.
1396
1445
  *
1397
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1398
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1399
- * `/resend` calls against it, so retrying this call from your own code
1400
- * after a network failure can create more than one clone — pair it with
1401
- * your own retry-suppression if that matters for your integration.
1446
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1447
+ * Either way the gateway does not currently deduplicate `/resend` calls
1448
+ * against it, so retrying after a network failure can create more than one
1449
+ * clone pair it with your own retry-suppression if that matters.
1402
1450
  *
1403
1451
  * Returns the *clone* event (new uuid), not the original. The original is
1404
1452
  * unchanged on the server.
@@ -1410,11 +1458,10 @@ var WebhookEvents = class {
1410
1458
  * clone.manualResendOf === event.uuid; // true — points back at the source
1411
1459
  */
1412
1460
  async resend(uuid, params = {}) {
1413
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
1414
1461
  return this.http.call(
1415
1462
  (signal) => this.http.client.POST(`/api/v1/webhook-events/${uuid}/resend`, {
1416
1463
  body: {},
1417
- headers: { "X-Idempotency-Key": idempotencyKey },
1464
+ headers: idempotencyHeaders(params.idempotencyKey),
1418
1465
  signal
1419
1466
  }).then((r) => r)
1420
1467
  );
@@ -1521,6 +1568,7 @@ exports.Garu = Garu;
1521
1568
  exports.GaruAPIError = GaruAPIError;
1522
1569
  exports.GaruAuthenticationError = GaruAuthenticationError;
1523
1570
  exports.GaruConnectionError = GaruConnectionError;
1571
+ exports.GaruDuplicateChargeError = GaruDuplicateChargeError;
1524
1572
  exports.GaruError = GaruError;
1525
1573
  exports.GaruNotFoundError = GaruNotFoundError;
1526
1574
  exports.GaruPermissionError = GaruPermissionError;
package/dist/index.d.cts CHANGED
@@ -1100,9 +1100,11 @@ declare class Charges {
1100
1100
  /**
1101
1101
  * Create a charge (PIX, boleto, or credit card).
1102
1102
  *
1103
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1104
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1105
- * returns the original charge for 24h.
1103
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1104
+ * original charge for 24h. Derive it from something stable in your own
1105
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1106
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1107
+ * call is different every time and protects nothing.
1106
1108
  *
1107
1109
  * @example
1108
1110
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -1133,6 +1135,20 @@ declare class Charges {
1133
1135
  * }
1134
1136
  * });
1135
1137
  * // charge.amount is the base price; charge.chargedTotal is what was charged.
1138
+ *
1139
+ * @example
1140
+ * // Handling a duplicate. A 409 means an identical charge is already being
1141
+ * // processed, or already went through — NOT that this one failed. Send the
1142
+ * // same request again after the wait and you get the ORIGINAL charge back.
1143
+ * // The SDK will not retry it for you.
1144
+ * try {
1145
+ * await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
1146
+ * } catch (err) {
1147
+ * if (err instanceof GaruDuplicateChargeError) {
1148
+ * await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
1149
+ * // retry the same call
1150
+ * }
1151
+ * }
1136
1152
  */
1137
1153
  create(params: CreateChargeParams): Promise<Charge>;
1138
1154
  /**
@@ -1159,10 +1175,10 @@ declare class Charges {
1159
1175
  * settles.
1160
1176
  *
1161
1177
  * For Pix/boleto (which open a refund request instead of an automated
1162
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
1163
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
1164
- * card, which reverses automatically and has no manual request to
1165
- * duplicate.
1178
+ * reversal), pass `idempotencyKey` to make a retry return the original
1179
+ * request rather than opening a second one; omit it and no key is sent.
1180
+ * Ignored for card, which reverses automatically and has no manual request
1181
+ * to duplicate.
1166
1182
  *
1167
1183
  * @example
1168
1184
  * await garu.charges.refund('6f1c9b2e-...'); // full
@@ -1196,10 +1212,16 @@ declare class InstallmentPlans {
1196
1212
  private readonly http;
1197
1213
  constructor(http: HttpClient);
1198
1214
  /**
1199
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
1200
- * you don't pass `idempotencyKey`), which matters more here than anywhere
1201
- * else in the API: this call registers a REAL boleto at the bank, so a
1202
- * blind retry can put two payable barcodes in one buyer's hands.
1215
+ * Sell a product as a carnê.
1216
+ *
1217
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1218
+ * original plan for 24h. Derive it from something stable in your own
1219
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1220
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1221
+ * call is different every time and protects nothing.
1222
+ * This matters more here than anywhere else in the API: the call registers a
1223
+ * REAL boleto at the bank, so a retry without a key can put two payable
1224
+ * barcodes in one buyer's hands.
1203
1225
  *
1204
1226
  * @example
1205
1227
  * const carne = await garu.installmentPlans.create({
@@ -1310,10 +1332,9 @@ declare class InstallmentPlans {
1310
1332
  * team. Transfer the money to the buyer yourself, then close it with
1311
1333
  * `garu.refundRequests.confirm`.
1312
1334
  *
1313
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1314
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
1315
- * a second pending request for the same carnê, so this is mainly
1316
- * defense-in-depth for the request-in-flight window.
1335
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
1336
+ * key is sent. The backend already dedupes a second pending request for the
1337
+ * same carnê, so this is defense-in-depth rather than the main guard.
1317
1338
  *
1318
1339
  * @example
1319
1340
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -1404,9 +1425,11 @@ declare class Customers {
1404
1425
  /**
1405
1426
  * Register a customer for the current seller.
1406
1427
  *
1407
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1408
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1409
- * returns the originally-created/matched customer for 24h.
1428
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1429
+ * original customer for 24h. Derive it from something stable in your own
1430
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1431
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1432
+ * call is different every time and protects nothing.
1410
1433
  *
1411
1434
  * @example
1412
1435
  * const customer = await garu.customers.create({
@@ -1576,10 +1599,11 @@ declare class Products {
1576
1599
  * product (HTTP 201). Only `name` is required; everything else falls back
1577
1600
  * to seller/server defaults.
1578
1601
  *
1579
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
1580
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
1581
- * retry on transient failures safe: a retried POST returns the original
1582
- * product instead of creating a duplicate.
1602
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1603
+ * original product for 24h. Derive it from something stable in your own
1604
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1605
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1606
+ * call is different every time and protects nothing.
1583
1607
  *
1584
1608
  * @example
1585
1609
  * const product = await garu.products.create({
@@ -1629,10 +1653,15 @@ declare class ScheduledCharges {
1629
1653
  private readonly http;
1630
1654
  constructor(http: HttpClient);
1631
1655
  /**
1632
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
1633
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
1634
- * UUIDv4. Safe to retry: the same key returns the originally-created
1635
- * series for 24h instead of double-booking recurring billing.
1656
+ * Create a new scheduled charge.
1657
+ *
1658
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1659
+ * original series for 24h. Derive it from something stable in your own
1660
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1661
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1662
+ * call is different every time and protects nothing.
1663
+ * Worth the effort here: without a key, a retry double-books recurring
1664
+ * billing for the same customer.
1636
1665
  *
1637
1666
  * @example
1638
1667
  * const charge = await garu.scheduledCharges.create({
@@ -1899,11 +1928,10 @@ declare class WebhookEvents {
1899
1928
  * original — distinguishable both by the `resend_` prefix and by reading
1900
1929
  * the response payload's `manualResendOf` field.
1901
1930
  *
1902
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1903
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1904
- * `/resend` calls against it, so retrying this call from your own code
1905
- * after a network failure can create more than one clone — pair it with
1906
- * your own retry-suppression if that matters for your integration.
1931
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1932
+ * Either way the gateway does not currently deduplicate `/resend` calls
1933
+ * against it, so retrying after a network failure can create more than one
1934
+ * clone pair it with your own retry-suppression if that matters.
1907
1935
  *
1908
1936
  * Returns the *clone* event (new uuid), not the original. The original is
1909
1937
  * unchanged on the server.
@@ -1982,7 +2010,7 @@ declare class Garu {
1982
2010
  * without parsing messages. Non-2xx API responses are mapped to the most specific
1983
2011
  * subclass of `GaruAPIError` by {@link mapApiError}.
1984
2012
  */
1985
- type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
2013
+ type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'charge_in_progress' | 'charge_already_processed' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
1986
2014
  declare class GaruError extends Error {
1987
2015
  readonly code: GaruErrorCode;
1988
2016
  constructor(code: GaruErrorCode, message: string);
@@ -2016,8 +2044,40 @@ declare class GaruRateLimitError extends GaruAPIError {
2016
2044
  readonly retryAfterSec: number | null;
2017
2045
  constructor(message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number | null);
2018
2046
  }
2047
+ /**
2048
+ * A charge identical to this one — same buyer, product, rail, amount and
2049
+ * instalment count — is already being processed, or already went through inside
2050
+ * the gateway's duplicate window.
2051
+ *
2052
+ * **This is not a failure.** The buyer's money is either on its way or already
2053
+ * taken. Wait `retryAfterSec` and send the same request again: the retry is
2054
+ * answered with the ORIGINAL charge rather than creating a second one.
2055
+ *
2056
+ * The SDK does not retry this for you. Re-POSTing to a money endpoint on your
2057
+ * behalf is exactly the kind of hidden behaviour 5.0.0 removed, and if your own
2058
+ * client also retries, the two stack.
2059
+ *
2060
+ * The real fix is upstream: pass `idempotencyKey` derived from something stable
2061
+ * in your domain, so a retry reproduces it. This error is the backstop for when
2062
+ * that has not happened.
2063
+ *
2064
+ * @example
2065
+ * try {
2066
+ * await garu.charges.create({ productId, paymentMethod: 'creditCard', customer });
2067
+ * } catch (err) {
2068
+ * if (err instanceof GaruDuplicateChargeError) {
2069
+ * await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
2070
+ * // Sending it again returns the original charge.
2071
+ * }
2072
+ * }
2073
+ */
2074
+ declare class GaruDuplicateChargeError extends GaruAPIError {
2075
+ /** How long to wait before sending the same request again. */
2076
+ readonly retryAfterSec: number;
2077
+ constructor(code: 'charge_in_progress' | 'charge_already_processed', message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number);
2078
+ }
2019
2079
  declare class GaruServerError extends GaruAPIError {
2020
2080
  constructor(message: string, status: number, requestId: string | null, body: unknown);
2021
2081
  }
2022
2082
 
2023
- export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
2083
+ export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruDuplicateChargeError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
package/dist/index.d.ts CHANGED
@@ -1100,9 +1100,11 @@ declare class Charges {
1100
1100
  /**
1101
1101
  * Create a charge (PIX, boleto, or credit card).
1102
1102
  *
1103
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1104
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1105
- * returns the original charge for 24h.
1103
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1104
+ * original charge for 24h. Derive it from something stable in your own
1105
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1106
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1107
+ * call is different every time and protects nothing.
1106
1108
  *
1107
1109
  * @example
1108
1110
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -1133,6 +1135,20 @@ declare class Charges {
1133
1135
  * }
1134
1136
  * });
1135
1137
  * // charge.amount is the base price; charge.chargedTotal is what was charged.
1138
+ *
1139
+ * @example
1140
+ * // Handling a duplicate. A 409 means an identical charge is already being
1141
+ * // processed, or already went through — NOT that this one failed. Send the
1142
+ * // same request again after the wait and you get the ORIGINAL charge back.
1143
+ * // The SDK will not retry it for you.
1144
+ * try {
1145
+ * await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
1146
+ * } catch (err) {
1147
+ * if (err instanceof GaruDuplicateChargeError) {
1148
+ * await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
1149
+ * // retry the same call
1150
+ * }
1151
+ * }
1136
1152
  */
1137
1153
  create(params: CreateChargeParams): Promise<Charge>;
1138
1154
  /**
@@ -1159,10 +1175,10 @@ declare class Charges {
1159
1175
  * settles.
1160
1176
  *
1161
1177
  * For Pix/boleto (which open a refund request instead of an automated
1162
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
1163
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
1164
- * card, which reverses automatically and has no manual request to
1165
- * duplicate.
1178
+ * reversal), pass `idempotencyKey` to make a retry return the original
1179
+ * request rather than opening a second one; omit it and no key is sent.
1180
+ * Ignored for card, which reverses automatically and has no manual request
1181
+ * to duplicate.
1166
1182
  *
1167
1183
  * @example
1168
1184
  * await garu.charges.refund('6f1c9b2e-...'); // full
@@ -1196,10 +1212,16 @@ declare class InstallmentPlans {
1196
1212
  private readonly http;
1197
1213
  constructor(http: HttpClient);
1198
1214
  /**
1199
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
1200
- * you don't pass `idempotencyKey`), which matters more here than anywhere
1201
- * else in the API: this call registers a REAL boleto at the bank, so a
1202
- * blind retry can put two payable barcodes in one buyer's hands.
1215
+ * Sell a product as a carnê.
1216
+ *
1217
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1218
+ * original plan for 24h. Derive it from something stable in your own
1219
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1220
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1221
+ * call is different every time and protects nothing.
1222
+ * This matters more here than anywhere else in the API: the call registers a
1223
+ * REAL boleto at the bank, so a retry without a key can put two payable
1224
+ * barcodes in one buyer's hands.
1203
1225
  *
1204
1226
  * @example
1205
1227
  * const carne = await garu.installmentPlans.create({
@@ -1310,10 +1332,9 @@ declare class InstallmentPlans {
1310
1332
  * team. Transfer the money to the buyer yourself, then close it with
1311
1333
  * `garu.refundRequests.confirm`.
1312
1334
  *
1313
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1314
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
1315
- * a second pending request for the same carnê, so this is mainly
1316
- * defense-in-depth for the request-in-flight window.
1335
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
1336
+ * key is sent. The backend already dedupes a second pending request for the
1337
+ * same carnê, so this is defense-in-depth rather than the main guard.
1317
1338
  *
1318
1339
  * @example
1319
1340
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -1404,9 +1425,11 @@ declare class Customers {
1404
1425
  /**
1405
1426
  * Register a customer for the current seller.
1406
1427
  *
1407
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
1408
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
1409
- * returns the originally-created/matched customer for 24h.
1428
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1429
+ * original customer for 24h. Derive it from something stable in your own
1430
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1431
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1432
+ * call is different every time and protects nothing.
1410
1433
  *
1411
1434
  * @example
1412
1435
  * const customer = await garu.customers.create({
@@ -1576,10 +1599,11 @@ declare class Products {
1576
1599
  * product (HTTP 201). Only `name` is required; everything else falls back
1577
1600
  * to seller/server defaults.
1578
1601
  *
1579
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
1580
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
1581
- * retry on transient failures safe: a retried POST returns the original
1582
- * product instead of creating a duplicate.
1602
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1603
+ * original product for 24h. Derive it from something stable in your own
1604
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1605
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1606
+ * call is different every time and protects nothing.
1583
1607
  *
1584
1608
  * @example
1585
1609
  * const product = await garu.products.create({
@@ -1629,10 +1653,15 @@ declare class ScheduledCharges {
1629
1653
  private readonly http;
1630
1654
  constructor(http: HttpClient);
1631
1655
  /**
1632
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
1633
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
1634
- * UUIDv4. Safe to retry: the same key returns the originally-created
1635
- * series for 24h instead of double-booking recurring billing.
1656
+ * Create a new scheduled charge.
1657
+ *
1658
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1659
+ * original series for 24h. Derive it from something stable in your own
1660
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1661
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1662
+ * call is different every time and protects nothing.
1663
+ * Worth the effort here: without a key, a retry double-books recurring
1664
+ * billing for the same customer.
1636
1665
  *
1637
1666
  * @example
1638
1667
  * const charge = await garu.scheduledCharges.create({
@@ -1899,11 +1928,10 @@ declare class WebhookEvents {
1899
1928
  * original — distinguishable both by the `resend_` prefix and by reading
1900
1929
  * the response payload's `manualResendOf` field.
1901
1930
  *
1902
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1903
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1904
- * `/resend` calls against it, so retrying this call from your own code
1905
- * after a network failure can create more than one clone — pair it with
1906
- * your own retry-suppression if that matters for your integration.
1931
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1932
+ * Either way the gateway does not currently deduplicate `/resend` calls
1933
+ * against it, so retrying after a network failure can create more than one
1934
+ * clone pair it with your own retry-suppression if that matters.
1907
1935
  *
1908
1936
  * Returns the *clone* event (new uuid), not the original. The original is
1909
1937
  * unchanged on the server.
@@ -1982,7 +2010,7 @@ declare class Garu {
1982
2010
  * without parsing messages. Non-2xx API responses are mapped to the most specific
1983
2011
  * subclass of `GaruAPIError` by {@link mapApiError}.
1984
2012
  */
1985
- type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
2013
+ type GaruErrorCode = 'authentication_error' | 'permission_error' | 'not_found' | 'validation_error' | 'rate_limited' | 'charge_in_progress' | 'charge_already_processed' | 'server_error' | 'api_error' | 'connection_error' | 'signature_verification_failed';
1986
2014
  declare class GaruError extends Error {
1987
2015
  readonly code: GaruErrorCode;
1988
2016
  constructor(code: GaruErrorCode, message: string);
@@ -2016,8 +2044,40 @@ declare class GaruRateLimitError extends GaruAPIError {
2016
2044
  readonly retryAfterSec: number | null;
2017
2045
  constructor(message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number | null);
2018
2046
  }
2047
+ /**
2048
+ * A charge identical to this one — same buyer, product, rail, amount and
2049
+ * instalment count — is already being processed, or already went through inside
2050
+ * the gateway's duplicate window.
2051
+ *
2052
+ * **This is not a failure.** The buyer's money is either on its way or already
2053
+ * taken. Wait `retryAfterSec` and send the same request again: the retry is
2054
+ * answered with the ORIGINAL charge rather than creating a second one.
2055
+ *
2056
+ * The SDK does not retry this for you. Re-POSTing to a money endpoint on your
2057
+ * behalf is exactly the kind of hidden behaviour 5.0.0 removed, and if your own
2058
+ * client also retries, the two stack.
2059
+ *
2060
+ * The real fix is upstream: pass `idempotencyKey` derived from something stable
2061
+ * in your domain, so a retry reproduces it. This error is the backstop for when
2062
+ * that has not happened.
2063
+ *
2064
+ * @example
2065
+ * try {
2066
+ * await garu.charges.create({ productId, paymentMethod: 'creditCard', customer });
2067
+ * } catch (err) {
2068
+ * if (err instanceof GaruDuplicateChargeError) {
2069
+ * await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
2070
+ * // Sending it again returns the original charge.
2071
+ * }
2072
+ * }
2073
+ */
2074
+ declare class GaruDuplicateChargeError extends GaruAPIError {
2075
+ /** How long to wait before sending the same request again. */
2076
+ readonly retryAfterSec: number;
2077
+ constructor(code: 'charge_in_progress' | 'charge_already_processed', message: string, status: number, requestId: string | null, body: unknown, retryAfterSec: number);
2078
+ }
2019
2079
  declare class GaruServerError extends GaruAPIError {
2020
2080
  constructor(message: string, status: number, requestId: string | null, body: unknown);
2021
2081
  }
2022
2082
 
2023
- export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
2083
+ export { type CancelAtPeriodEndScheduledChargeParams, type CancelChargeResult, type CancelInstallmentPlanParams, type CancelRecurrenceScheduledChargeParams, type CardInput, type ChangePaymentMethodScheduledChargeParams, type Charge, type ChargeList, type ChargeNowOutcome, type ChargeNowReason, type ChargeNowResult, type ChargePaymentMethod, type ChargeStatus, type CreateChargeParams, type CreateCustomerParams, type CreateInstallmentPlanParams, type CreateProductParams, type CreateScheduledChargeParams, type Customer, type CustomerList, type CustomerRecord, type FailurePayload, Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruDuplicateChargeError, GaruError, type GaruErrorCode, type GaruFailureCode, GaruNotFoundError, type GaruOptions, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, type Installment, type InstallmentPlan, type InstallmentPlanList, type InstallmentPlanStatus, type InstallmentStatus, type ListChargesParams, type ListCustomersParams, type ListInstallmentPlansParams, type ListProductsParams, type ListRefundRequestsParams, type ListScheduledChargeAttemptsParams, type ListScheduledChargesParams, type ListWebhookEventsParams, type MarkPaidScheduledChargeParams, type MetaFeatures, type MetaResponse, type PaginatedList, type PauseScheduledChargeParams, type PaymentMethodExpiredPayload, type PaymentMethodExpiringPayload, type PostponeInstallmentParams, type PostponeScheduledChargeParams, type Product, type ProductList, type ProductPortalConfig, type RecurrenceConfig, type RecurrenceInterval, type RefundChargeParams, type RefundRequest, type RefundRequestList, type RefundRequestStatus, type ReissueInstallmentResult, type RequestPlanRefundParams, type ResendWebhookEventParams, type ResolveRefundRequestParams, type ScheduledChargeActor, type ScheduledChargeAttempt, type ScheduledChargeAttemptList, type ScheduledChargeAttemptSource, type ScheduledChargeAttemptStatus, type ScheduledChargeDetail, type ScheduledChargeEvent, type ScheduledChargeEventType, type ScheduledChargeLinkedTransaction, type ScheduledChargeList, type ScheduledChargeRecord, type ScheduledChargeStatus, type ScheduledChargeType, type ScheduledPaymentMethod, type SetBillingEmailOverrideParams, type SetProductPortalConfigParams, type UpdateCustomerParams, type UpdateProductParams, type VerifiedWebhook, type VerifyWebhookParams, type WebhookEvent, type WebhookEventEndpoint, type WebhookEventList, type WebhookEventStatus, webhooks };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import createClient from 'openapi-fetch';
2
- import { createHmac, timingSafeEqual, randomUUID } from 'crypto';
2
+ import { createHmac, timingSafeEqual } from 'crypto';
3
3
 
4
4
  // src/http.ts
5
5
 
@@ -70,6 +70,15 @@ var GaruRateLimitError = class extends GaruAPIError {
70
70
  this.retryAfterSec = retryAfterSec;
71
71
  }
72
72
  };
73
+ var GaruDuplicateChargeError = class extends GaruAPIError {
74
+ /** How long to wait before sending the same request again. */
75
+ retryAfterSec;
76
+ constructor(code, message, status, requestId, body, retryAfterSec) {
77
+ super(code, message, status, requestId, body);
78
+ this.name = "GaruDuplicateChargeError";
79
+ this.retryAfterSec = retryAfterSec;
80
+ }
81
+ };
73
82
  var GaruServerError = class extends GaruAPIError {
74
83
  constructor(message, status, requestId, body) {
75
84
  super("server_error", message, status, requestId, body);
@@ -87,6 +96,13 @@ function mapApiError(status, body, requestId, retryAfterSec) {
87
96
  if (status === 429) {
88
97
  return new GaruRateLimitError(message, status, requestId, body, retryAfterSec);
89
98
  }
99
+ if (status === 409) {
100
+ const code = readDuplicateChargeCode(body);
101
+ if (code) {
102
+ const wait = retryAfterSec ?? readRetryAfterFromBody(body) ?? DEFAULT_DUPLICATE_RETRY_SEC;
103
+ return new GaruDuplicateChargeError(code, message, status, requestId, body, wait);
104
+ }
105
+ }
90
106
  if (status >= 500) return new GaruServerError(message, status, requestId, body);
91
107
  return new GaruAPIError("api_error", message, status, requestId, body);
92
108
  }
@@ -99,6 +115,15 @@ function extractMessage(body) {
99
115
  }
100
116
  return null;
101
117
  }
118
+ var DEFAULT_DUPLICATE_RETRY_SEC = 5;
119
+ function readDuplicateChargeCode(body) {
120
+ const code = body?.error;
121
+ return code === "charge_in_progress" || code === "charge_already_processed" ? code : null;
122
+ }
123
+ function readRetryAfterFromBody(body) {
124
+ const value = body?.retryAfter;
125
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
126
+ }
102
127
 
103
128
  // src/http.ts
104
129
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
@@ -182,8 +207,8 @@ function backoffDelay(attempt, retryAfterSec) {
182
207
  function sleep(ms) {
183
208
  return new Promise((resolve) => setTimeout(resolve, ms));
184
209
  }
185
- function generateIdempotencyKey() {
186
- return randomUUID();
210
+ function idempotencyHeaders(key) {
211
+ return key ? { "X-Idempotency-Key": key } : {};
187
212
  }
188
213
 
189
214
  // src/resources/charges.ts
@@ -195,9 +220,11 @@ var Charges = class {
195
220
  /**
196
221
  * Create a charge (PIX, boleto, or credit card).
197
222
  *
198
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
199
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
200
- * returns the original charge for 24h.
223
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
224
+ * original charge for 24h. Derive it from something stable in your own
225
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
226
+ * no key is sent — the SDK does NOT invent one, because a key generated per
227
+ * call is different every time and protects nothing.
201
228
  *
202
229
  * @example
203
230
  * // PIX — render charge.pix.code as a QR in your own checkout
@@ -228,9 +255,22 @@ var Charges = class {
228
255
  * }
229
256
  * });
230
257
  * // charge.amount is the base price; charge.chargedTotal is what was charged.
258
+ *
259
+ * @example
260
+ * // Handling a duplicate. A 409 means an identical charge is already being
261
+ * // processed, or already went through — NOT that this one failed. Send the
262
+ * // same request again after the wait and you get the ORIGINAL charge back.
263
+ * // The SDK will not retry it for you.
264
+ * try {
265
+ * await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, card });
266
+ * } catch (err) {
267
+ * if (err instanceof GaruDuplicateChargeError) {
268
+ * await new Promise((r) => setTimeout(r, err.retryAfterSec * 1000));
269
+ * // retry the same call
270
+ * }
271
+ * }
231
272
  */
232
273
  async create(params) {
233
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
234
274
  const body = {
235
275
  productId: params.productId,
236
276
  paymentMethod: params.paymentMethod,
@@ -239,7 +279,7 @@ var Charges = class {
239
279
  if (params.card) body.card = params.card;
240
280
  if (params.checkoutSessionToken) body.checkoutSessionToken = params.checkoutSessionToken;
241
281
  if (params.additionalInfo !== void 0) body.additionalInfo = params.additionalInfo;
242
- return this.post("/api/v1/charges", body, { "X-Idempotency-Key": idempotencyKey });
282
+ return this.post("/api/v1/charges", body, idempotencyHeaders(params.idempotencyKey));
243
283
  }
244
284
  /**
245
285
  * Retrieve a charge by uuid.
@@ -280,23 +320,24 @@ var Charges = class {
280
320
  * settles.
281
321
  *
282
322
  * For Pix/boleto (which open a refund request instead of an automated
283
- * reversal), attaches an `X-Idempotency-Key` header automatically if you
284
- * don't pass `idempotencyKey`, the SDK generates a UUIDv4. Ignored for
285
- * card, which reverses automatically and has no manual request to
286
- * duplicate.
323
+ * reversal), pass `idempotencyKey` to make a retry return the original
324
+ * request rather than opening a second one; omit it and no key is sent.
325
+ * Ignored for card, which reverses automatically and has no manual request
326
+ * to duplicate.
287
327
  *
288
328
  * @example
289
329
  * await garu.charges.refund('6f1c9b2e-...'); // full
290
330
  * await garu.charges.refund('6f1c9b2e-...', { amount: 10.0 }); // R$10,00
291
331
  */
292
332
  async refund(uuid, params = {}) {
293
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
294
333
  const body = {};
295
334
  if (params.amount !== void 0) body.amount = params.amount;
296
335
  if (params.reason !== void 0) body.reason = params.reason;
297
- return this.post(`/api/v1/charges/${encodeURIComponent(uuid)}/refund`, body, {
298
- "X-Idempotency-Key": idempotencyKey
299
- });
336
+ return this.post(
337
+ `/api/v1/charges/${encodeURIComponent(uuid)}/refund`,
338
+ body,
339
+ idempotencyHeaders(params.idempotencyKey)
340
+ );
300
341
  }
301
342
  /**
302
343
  * Cancel an unpaid charge.
@@ -337,10 +378,16 @@ var InstallmentPlans = class {
337
378
  }
338
379
  http;
339
380
  /**
340
- * Sell a product as a carnê. Auto-attaches `X-Idempotency-Key` (UUIDv4 if
341
- * you don't pass `idempotencyKey`), which matters more here than anywhere
342
- * else in the API: this call registers a REAL boleto at the bank, so a
343
- * blind retry can put two payable barcodes in one buyer's hands.
381
+ * Sell a product as a carnê.
382
+ *
383
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
384
+ * original plan for 24h. Derive it from something stable in your own
385
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
386
+ * no key is sent — the SDK does NOT invent one, because a key generated per
387
+ * call is different every time and protects nothing.
388
+ * This matters more here than anywhere else in the API: the call registers a
389
+ * REAL boleto at the bank, so a retry without a key can put two payable
390
+ * barcodes in one buyer's hands.
344
391
  *
345
392
  * @example
346
393
  * const carne = await garu.installmentPlans.create({
@@ -366,12 +413,11 @@ var InstallmentPlans = class {
366
413
  * });
367
414
  */
368
415
  async create(params) {
369
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
370
416
  const { idempotencyKey: _omit, ...body } = params;
371
417
  return this.http.call(
372
418
  (signal) => this.http.client.POST("/api/v1/installment-plans", {
373
419
  body,
374
- headers: { "X-Idempotency-Key": idempotencyKey },
420
+ headers: idempotencyHeaders(params.idempotencyKey),
375
421
  signal
376
422
  }).then((r) => r)
377
423
  );
@@ -514,10 +560,9 @@ var InstallmentPlans = class {
514
560
  * team. Transfer the money to the buyer yourself, then close it with
515
561
  * `garu.refundRequests.confirm`.
516
562
  *
517
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
518
- * `idempotencyKey`, the SDK generates a UUIDv4. The backend already dedupes
519
- * a second pending request for the same carnê, so this is mainly
520
- * defense-in-depth for the request-in-flight window.
563
+ * Pass `idempotencyKey` to cover the request-in-flight window; omit it and no
564
+ * key is sent. The backend already dedupes a second pending request for the
565
+ * same carnê, so this is defense-in-depth rather than the main guard.
521
566
  *
522
567
  * @example
523
568
  * const request = await garu.installmentPlans.requestRefund(uuid, {
@@ -527,12 +572,11 @@ var InstallmentPlans = class {
527
572
  * request.amount; // defaults to everything the carnê collected
528
573
  */
529
574
  async requestRefund(uuid, params = {}) {
530
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
531
575
  const { idempotencyKey: _omit, ...body } = params;
532
576
  return this.http.call(
533
577
  (signal) => this.http.client.POST(`/api/v1/installment-plans/${uuid}/refund-requests`, {
534
578
  body,
535
- headers: { "X-Idempotency-Key": idempotencyKey },
579
+ headers: idempotencyHeaders(params.idempotencyKey),
536
580
  signal
537
581
  }).then((r) => r)
538
582
  );
@@ -640,9 +684,11 @@ var Customers = class {
640
684
  /**
641
685
  * Register a customer for the current seller.
642
686
  *
643
- * Attaches an `X-Idempotency-Key` header automatically if you don't pass
644
- * `idempotencyKey`, the SDK generates a UUIDv4. Safe to retry: the same key
645
- * returns the originally-created/matched customer for 24h.
687
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
688
+ * original customer for 24h. Derive it from something stable in your own
689
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
690
+ * no key is sent — the SDK does NOT invent one, because a key generated per
691
+ * call is different every time and protects nothing.
646
692
  *
647
693
  * @example
648
694
  * const customer = await garu.customers.create({
@@ -655,12 +701,11 @@ var Customers = class {
655
701
  * customer.uuid;
656
702
  */
657
703
  async create(params) {
658
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
659
704
  const { idempotencyKey: _omit, ...body } = params;
660
705
  return this.http.call(
661
706
  (signal) => this.http.client.POST("/api/v1/customers", {
662
707
  body,
663
- headers: { "X-Idempotency-Key": idempotencyKey },
708
+ headers: idempotencyHeaders(params.idempotencyKey),
664
709
  signal
665
710
  }).then((r) => r)
666
711
  );
@@ -905,10 +950,11 @@ var Products = class {
905
950
  * product (HTTP 201). Only `name` is required; everything else falls back
906
951
  * to seller/server defaults.
907
952
  *
908
- * Automatically attaches an `X-Idempotency-Key` header if you don't pass
909
- * `idempotencyKey`, the SDK generates a UUIDv4. This makes the built-in
910
- * retry on transient failures safe: a retried POST returns the original
911
- * product instead of creating a duplicate.
953
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
954
+ * original product for 24h. Derive it from something stable in your own
955
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
956
+ * no key is sent — the SDK does NOT invent one, because a key generated per
957
+ * call is different every time and protects nothing.
912
958
  *
913
959
  * @example
914
960
  * const product = await garu.products.create({
@@ -924,11 +970,10 @@ var Products = class {
924
970
  */
925
971
  async create(params) {
926
972
  const { idempotencyKey, ...body } = params;
927
- const key = idempotencyKey ?? generateIdempotencyKey();
928
973
  return this.http.call(
929
974
  (signal) => this.http.client.POST("/api/v1/products", {
930
975
  body,
931
- headers: { "X-Idempotency-Key": key },
976
+ headers: idempotencyHeaders(idempotencyKey),
932
977
  signal
933
978
  }).then((r) => r)
934
979
  );
@@ -964,10 +1009,15 @@ var ScheduledCharges = class {
964
1009
  }
965
1010
  http;
966
1011
  /**
967
- * Create a new scheduled charge. Attaches an `X-Idempotency-Key` header
968
- * automatically — if you don't pass `idempotencyKey`, the SDK generates a
969
- * UUIDv4. Safe to retry: the same key returns the originally-created
970
- * series for 24h instead of double-booking recurring billing.
1012
+ * Create a new scheduled charge.
1013
+ *
1014
+ * Pass `idempotencyKey` to make this safe to retry: the same key returns the
1015
+ * original series for 24h. Derive it from something stable in your own
1016
+ * domain (an order id, a booking id) so a retry reproduces it. Omit it and
1017
+ * no key is sent — the SDK does NOT invent one, because a key generated per
1018
+ * call is different every time and protects nothing.
1019
+ * Worth the effort here: without a key, a retry double-books recurring
1020
+ * billing for the same customer.
971
1021
  *
972
1022
  * @example
973
1023
  * const charge = await garu.scheduledCharges.create({
@@ -994,12 +1044,11 @@ var ScheduledCharges = class {
994
1044
  * });
995
1045
  */
996
1046
  async create(params) {
997
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
998
1047
  const { idempotencyKey: _omit, ...body } = params;
999
1048
  return this.http.call(
1000
1049
  (signal) => this.http.client.POST("/api/v1/scheduled-charges", {
1001
1050
  body,
1002
- headers: { "X-Idempotency-Key": idempotencyKey },
1051
+ headers: idempotencyHeaders(params.idempotencyKey),
1003
1052
  signal
1004
1053
  }).then((r) => r)
1005
1054
  );
@@ -1388,11 +1437,10 @@ var WebhookEvents = class {
1388
1437
  * original — distinguishable both by the `resend_` prefix and by reading
1389
1438
  * the response payload's `manualResendOf` field.
1390
1439
  *
1391
- * The SDK also attaches an `X-Idempotency-Key` header (UUIDv4 unless you
1392
- * pass `idempotencyKey`); the gateway does not currently deduplicate
1393
- * `/resend` calls against it, so retrying this call from your own code
1394
- * after a network failure can create more than one clone — pair it with
1395
- * your own retry-suppression if that matters for your integration.
1440
+ * `idempotencyKey` is forwarded when you pass one and omitted when you don't.
1441
+ * Either way the gateway does not currently deduplicate `/resend` calls
1442
+ * against it, so retrying after a network failure can create more than one
1443
+ * clone pair it with your own retry-suppression if that matters.
1396
1444
  *
1397
1445
  * Returns the *clone* event (new uuid), not the original. The original is
1398
1446
  * unchanged on the server.
@@ -1404,11 +1452,10 @@ var WebhookEvents = class {
1404
1452
  * clone.manualResendOf === event.uuid; // true — points back at the source
1405
1453
  */
1406
1454
  async resend(uuid, params = {}) {
1407
- const idempotencyKey = params.idempotencyKey ?? generateIdempotencyKey();
1408
1455
  return this.http.call(
1409
1456
  (signal) => this.http.client.POST(`/api/v1/webhook-events/${uuid}/resend`, {
1410
1457
  body: {},
1411
- headers: { "X-Idempotency-Key": idempotencyKey },
1458
+ headers: idempotencyHeaders(params.idempotencyKey),
1412
1459
  signal
1413
1460
  }).then((r) => r)
1414
1461
  );
@@ -1511,4 +1558,4 @@ var Garu = class {
1511
1558
  }
1512
1559
  };
1513
1560
 
1514
- export { Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruError, GaruNotFoundError, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, webhooks };
1561
+ export { Garu, GaruAPIError, GaruAuthenticationError, GaruConnectionError, GaruDuplicateChargeError, GaruError, GaruNotFoundError, GaruPermissionError, GaruRateLimitError, GaruServerError, GaruSignatureVerificationError, GaruValidationError, webhooks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garuhq/node",
3
- "version": "4.1.0",
3
+ "version": "5.1.0",
4
4
  "description": "Official Node.js / TypeScript SDK for the Garu payment gateway.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://garu.com.br",