Skip to content
chiltepin

Generated from: “Write the release notes for 2.4 from these merged PRs: #412 idempotent refunds, #418 webhook retries with backoff, #421 EU data residency for invoices, #425 CLI `orders export --since`, #430 fix double-charge on network retry, #433 drop Node 18.”

Release notes — 2.4

AI-generated example · 2026-09-14. The source passes chiltepin check. System details and measurements are illustrative; review them before adapting this document.

DOCUMENTRelease · 2026-09-10

Release notes — 2.4

Idempotent refunds, webhook retries, EU invoice residency, a new export command, and the end of Node 18.

Version 2.4 makes two operations safe to retry: refunds and webhooks. It also stores EU customers' invoices in the EU, adds orders export --since to the CLI, and fixes a double charge that a network retry could cause. Node 18 support ends with this release. Upgrade steps are at the end.

What changed

SECTION 01 · Changelog
2.4.02026-09-10minor
addedIdempotent refunds: POST /v1/refunds accepts an Idempotency-Key header and returns the original refund on a repeat (#412)
addedWebhook retries with exponential backoff: 8 attempts over 24 hours, with jitter, and a dead-letter view in the dashboard (#418)
addedEU data residency for invoices: invoice PDFs and rows for EU-billed accounts live in eu-central-1 (#421)
addedCLI: orders export --since <date> streams orders as NDJSON to stdout (#425)
fixedA network retry on POST /v1/charges could charge twice when the first response was lost; the second attempt now returns the first charge (#430)
removedNode 18 support in the SDK and the CLI; the minimum is Node 20 (#433)

Node 18 is no longer supported

SECTION 02 · Note

Breaking for Node 18 users

Warning
The SDK and the CLI need Node 20 or later. Node 18 left upstream maintenance in April 2025, and 2.4 uses the built-in fetch and WebSocket without polyfills. Installing 2.4 on Node 18 fails at install time with an engines error, not at runtime.

Refunds you can retry

SECTION 03 · API endpoint

Create a refund

POST/v1/refundsBearer secret key

Refund all or part of a charge. Send the same Idempotency-Key on a retry and the API returns the first refund instead of creating a second.

Parameters
NameInTypeDescription
Idempotency-Key requiredheaderstringUnique per refund attempt; kept for 24 hours
Request body
FieldTypeDescription
charge_id requiredstringThe charge to refund
amountintegerMinor units; omit for a full refund
reasonstringrequested_by_customer, duplicate, or fraudulent
Responses
StatusDescription
201Refund created
200Same key seen before; the original refund is returned
409Same key with a different body
402Charge already fully refunded
Example request
{ "charge_id": "ch_9K2f", "amount": 1500, "reason": "requested_by_customer" }
Example response
{ "id": "re_4Hd1", "charge_id": "ch_9K2f", "amount": 1500, "status": "succeeded" }

The key is scoped to your account and kept for 24 hours. A retry after 24 hours creates a new refund, so a client that retries across days must check the charge's amount_refunded first. The same header now protects POST /v1/charges, and the double-charge fix in #430 is that header applied by the SDK on every network retry.

Export orders from the CLI

SECTION 04 · Code
terminalshell
$ orders export --since 2026-09-01 > september.ndjsonexported 18,204 orders (2026-09-01 to 2026-09-10) in 41 s$ head -1 september.ndjson{"id":"ord_7Qm2","created_at":"2026-09-01T00:02:11Z","status":"shipped","total":8490,"currency":"EUR"}$ orders export --since 2026-09-01 --until 2026-09-02 --status refunded | wc -l113

Upgrade steps

SECTION 05 · Steps
  1. Move to Node 20 or later

    Check CI images and the Dockerfile as well as your laptop.

    bash
    node --version && npm install @example/sdk@2.4 @example/cli@2.4
  2. Make webhook handlers idempotent on event id

    2.4 retries a webhook for up to 24 hours. A handler can receive the same event up to 8 times. Store the event id and skip a repeat.

    Handlers that already return 200 within 10 seconds need no change beyond the dedupe.

  3. Send an Idempotency-Key on every refund

    The SDK sets the header when you pass idempotencyKey. A call without it works, but each retry creates a new refund.

    typescript
    await client.refunds.create({ chargeId: 'ch_9K2f', amount: 1500 }, { idempotencyKey: 'ref-9K2f-1' });
  4. Confirm invoice residency for EU accounts

    Accounts with an EU billing address moved on 8 September. The invoice URL now starts with eu.files.example.com. No API change; update any allowlist that pins the old host.

View the Markdown
```meta
title: Release notes — 2.4
subtitle: Idempotent refunds, webhook retries, EU invoice residency, a new export command, and the end of Node 18.
tag: Release · 2026-09-10
```

Version 2.4 makes two operations safe to retry: refunds and webhooks. It also stores EU customers' invoices in the EU, adds `orders export --since` to the CLI, and fixes a double charge that a network retry could cause. Node 18 support ends with this release. Upgrade steps are at the end.

## What changed

```changelog
id: rel-2-4
releases:
  - version: 2.4.0
    date: 2026-09-10
    tag: minor
    items:
      - { type: added, text: "Idempotent refunds: POST /v1/refunds accepts an Idempotency-Key header and returns the original refund on a repeat (#412)" }
      - { type: added, text: "Webhook retries with exponential backoff: 8 attempts over 24 hours, with jitter, and a dead-letter view in the dashboard (#418)" }
      - { type: added, text: "EU data residency for invoices: invoice PDFs and rows for EU-billed accounts live in eu-central-1 (#421)" }
      - { type: added, text: "CLI: orders export --since <date> streams orders as NDJSON to stdout (#425)" }
      - { type: fixed, text: "A network retry on POST /v1/charges could charge twice when the first response was lost; the second attempt now returns the first charge (#430)" }
      - { type: removed, text: "Node 18 support in the SDK and the CLI; the minimum is Node 20 (#433)" }
```

## Node 18 is no longer supported

```callout
tone: warn
title: Breaking for Node 18 users
body: "The SDK and the CLI need Node 20 or later. Node 18 left upstream maintenance in April 2025, and 2.4 uses the built-in fetch and WebSocket without polyfills. Installing 2.4 on Node 18 fails at install time with an engines error, not at runtime."
```

## Refunds you can retry

```endpoint
id: rel-refunds
method: POST
path: /v1/refunds
title: Create a refund
description: "Refund all or part of a charge. Send the same Idempotency-Key on a retry and the API returns the first refund instead of creating a second."
auth: Bearer secret key
params:
  - { name: Idempotency-Key, in: header, type: string, required: true, desc: "Unique per refund attempt; kept for 24 hours" }
body:
  - { name: charge_id, type: string, required: true, desc: "The charge to refund" }
  - { name: amount, type: integer, desc: "Minor units; omit for a full refund" }
  - { name: reason, type: string, desc: "requested_by_customer, duplicate, or fraudulent" }
responses:
  - { status: 201, desc: "Refund created" }
  - { status: 200, desc: "Same key seen before; the original refund is returned" }
  - { status: 409, desc: "Same key with a different body" }
  - { status: 402, desc: "Charge already fully refunded" }
request: |
  { "charge_id": "ch_9K2f", "amount": 1500, "reason": "requested_by_customer" }
response: |
  { "id": "re_4Hd1", "charge_id": "ch_9K2f", "amount": 1500, "status": "succeeded" }
```

The key is scoped to your account and kept for 24 hours. A retry after 24 hours creates a new refund, so a client that retries across days must check the charge's `amount_refunded` first. The same header now protects `POST /v1/charges`, and the double-charge fix in #430 is that header applied by the SDK on every network retry.

## Export orders from the CLI

```code
kind: terminal
session: |
  $ orders export --since 2026-09-01 > september.ndjson
  exported 18,204 orders (2026-09-01 to 2026-09-10) in 41 s
  $ head -1 september.ndjson
  {"id":"ord_7Qm2","created_at":"2026-09-01T00:02:11Z","status":"shipped","total":8490,"currency":"EUR"}
  $ orders export --since 2026-09-01 --until 2026-09-02 --status refunded | wc -l
  113
caption: "--since takes a date or an RFC 3339 timestamp. Output is one order per line, oldest first, and the command resumes from the last printed id on Ctrl-C and rerun."
```

## Upgrade steps

```steps
id: rel-upgrade
items:
  - title: Move to Node 20 or later
    body: Check CI images and the Dockerfile as well as your laptop.
    code: node --version && npm install @example/sdk@2.4 @example/cli@2.4
    lang: bash
  - title: Make webhook handlers idempotent on event id
    body: 2.4 retries a webhook for up to 24 hours. A handler can receive the same event up to 8 times. Store the event id and skip a repeat.
    note: Handlers that already return 200 within 10 seconds need no change beyond the dedupe.
  - title: Send an Idempotency-Key on every refund
    body: The SDK sets the header when you pass idempotencyKey. A call without it works, but each retry creates a new refund.
    code: "await client.refunds.create({ chargeId: 'ch_9K2f', amount: 1500 }, { idempotencyKey: 'ref-9K2f-1' });"
    lang: typescript
  - title: Confirm invoice residency for EU accounts
    body: Accounts with an EU billing address moved on 8 September. The invoice URL now starts with eu.files.example.com. No API change; update any allowlist that pins the old host.
```