# Changelog

The public API is dated. The current version is **`2026-01-01`**.

There is only one behavioural version today. Sending `API-Version: 2026-01-01`
records intent for the first time a second version ships; it does not select a
different contract yet. See [Versioning](./20-versioning.md). Every change below
therefore applies to every request, whatever `API-Version` it sends.

## What is listed here

Every change that can alter what an existing, correctly-written integration observes is
listed on this page: a response field, HTTP status, error `code` or `type` that changes; a
request that was accepted and is now refused, or the reverse; a webhook payload; a
`worthypayments.js` callback shape or default. Each entry is dated the day it went live in
production (UTC) and tagged:

| Tag | Meaning |
|---|---|
| **Breaking** | Could break a correctly-written integration, as defined in [What counts as a breaking change](./20-versioning.md#what-counts-as-a-breaking-change). |
| **Changed** | Behaves differently, but an integration that follows these guides keeps working. |
| **Added** | A new field, value, event, parameter or callback property. Additive; ignore what you do not use. |
| **Fixed** | The API now does what these guides already said. **Fixed (docs)** means the guide was wrong and now describes what the API does. |

## 2026-10-22

- **Changed.** Absolute links the platform hands out now use `https://app.worthypayments.com`:
  the `url` of a checkout link and a payment link, a charge's `receipt_url`, the hosted pay link on
  invoices and subscriptions, and the links in the emails we send. Links issued earlier on
  `https://app.paygeyt.com` keep working; both hosts serve the same pages. If your code checks the
  host of these URLs, or a Content-Security-Policy on your site lists it, allow
  `app.worthypayments.com` as well.
- **Changed.** The inbound webhook URLs the dashboard shows you to paste into a provider portal
  (PayArc, Kount) now use `https://webhooks.worthypayments.com`. A URL you registered earlier on
  `https://webhooks.paygeyt.com` keeps working; both hosts deliver to the same place, so nothing
  needs to change in a portal that already has one.
- **Changed.** The Node SDK (`@worthypayments/node`) defaults `baseUrl` to
  `https://api.worthypayments.com`. `https://api.paygeyt.com` serves the same API with the same
  keys; pass `{ baseUrl: "https://api.paygeyt.com" }` to stay on it.
- **Fixed (docs).** [Charges](./03-charges.md#client-ip-fraud-signal-not-a-body-field),
  [Integration hard rules](./27-integration-hard-rules.md#1-client-ip) and three other guides
  still warned that a card-testing burst could auto-block your server's IP on a server-side
  charge, so renewals needed an allowlist. That stopped being true on 2026-10-19: a charge sent
  with a secret key records your server's address apart from the buyer's and never scores or
  blocklists it. Nothing to allowlist; send a
  [`device_session`](./24-device-data-collection.md#device-session-the-buyers-ip-and-browser)
  to score a server-built charge on the buyer's IP.

## 2026-10-21

- **Added.** The platform answers on worthypayments.com: `https://api.worthypayments.com/v1`,
  `https://app.worthypayments.com/sdk/v1/worthypayments.umd.js` and
  `https://element.worthypayments.com`, and these guides now show those hosts. The paygeyt.com
  hosts are the same API, keys, data and SDK file and keep working, so an existing integration
  needs no change. See [Two domains](./README.md#two-domains).
- **Added.** `worthypayments.js` loaded from `app.worthypayments.com/sdk/v1/` defaults to
  `https://api.worthypayments.com/v1` and `https://element.worthypayments.com`, so a page that
  loads the SDK from worthypayments.com stays on that domain. Loaded from
  `app.paygeyt.com/sdk/v1/`, bundled from npm, or hosted on your own site, it keeps the
  paygeyt.com hosts it always used. `apiBase` and `elementOrigin` still override both. See
  [Embedded SDK](./08-embedded-sdk.md).
- **Added.** WooCommerce plugin 2.2.0: an order whose buyer never came back after paying (the
  page closed after 3-D Secure, a lost redirect) is settled even without a webhook. The plugin
  reads the order's intent itself 10 minutes after the checkout handed it out, then after 30
  minutes, 2 hours and 12 hours. Before WooCommerce's **Hold stock** cleanup cancels an unpaid
  order, the plugin reads its intent once more, and an order whose payment succeeded or is
  still settling is never cancelled. See [How a payment works](./09-woocommerce.md#how-a-payment-works).
- **Changed.** WooCommerce plugin 2.2.0: `dispute.created` for a chargeback puts a paid order
  **On hold**, `dispute.won` restores the status it had, and `dispute.lost` marks it
  **Failed**, as Stripe's WooCommerce plugin does. A pre-dispute alert and the other dispute
  events still only add an order note, and `dispute.closed` no longer adds one (it repeats the
  `dispute.won` / `dispute.lost` it comes with). These status changes send the customer no
  WooCommerce order e-mail. See [Disputes](./09-woocommerce.md#disputes).
- **Added.** WooCommerce plugin 2.2.0: the order screen shows the card the order was paid with
  (brand and last four), its mode, and a link to the transaction in your dashboard; the
  transaction id next to **Payment via** links there too. See
  [The order screen](./09-woocommerce.md#the-order-screen).
- **Added.** WooCommerce plugin 2.3.0: saving the settings checks every key with
  `GET /v1/ping`. It reports a refused key, a key of the other mode, each missing secret-key
  scope, and a publishable key whose allowed domains do not list the site. While the method is
  on in test mode, every admin screen shows a test-mode notice. See
  [Configure](./09-woocommerce.md#configure).
- **Added.** WooCommerce plugin 2.3.0: **Set up the webhook for me** (on by default) registers
  the store's endpoint with `POST /v1/webhook-endpoints` when a mode's signing secret is empty,
  with the `charge.*`, `dispute.*`, `pre_dispute_alert.*` and `setup_intent.*` events the
  plugin uses, and saves its signing secret; an existing endpoint for the same URL gets its
  secret rolled instead. Needs `webhooks:read` and `webhooks:write` on the secret key. See
  [Webhooks](./09-woocommerce.md#webhooks).
- **Added.** WooCommerce plugin 2.3.0: card-testing protection. A buyer, counted by IP
  address, WooCommerce session and billing e-mail, may make 10 payment attempts an hour (the
  **Payment attempts per hour** setting; `0` turns it off); past that, checkout refuses the
  attempt before it reaches our API. See [Card testing](./09-woocommerce.md#card-testing).
- **Added.** WooCommerce plugin 2.4.0: a renewal declined `soft_decline_3ds` (the bank wants the
  customer to confirm the payment, and a renewal never runs 3-D Secure) e-mails the customer
  **Renewal payment needs confirmation**, with a link to the renewal's payment page, as Stripe's
  plugin does; WooCommerce Subscriptions' failed-renewal and retry e-mails and WooCommerce's
  failed-order e-mail are not sent for it. See
  [WooCommerce Subscriptions](./09-woocommerce.md#woocommerce-subscriptions).
- **Added.** WooCommerce plugin 2.4.0: My account → Subscriptions and the admin's subscription
  screens name the card renewals charge (*Via Visa ending in 4242*).
- **Breaking.** WooCommerce plugin 2.4.0: saved-card payments and renewals carry the same
  `metadata` keys as payments made in the Element: `order_id` and `order_number`, plus
  `subscription_id` on a renewal. They carried `wc_order_id`, `wc_subscription_id` and
  `wc_site`; read the new keys in `charge.*` webhooks and `GET /v1/charges`. Charges made
  before the update keep their old keys, so code that reads an order id from a charge should
  read `order_id` and fall back to `wc_order_id`.
- **Changed.** WooCommerce plugin 2.4.0, classic checkout and the pay-for-order page: placing
  an order with a saved card waits up to 2 seconds for the buyer's device signals, as the
  Checkout block already did, so the charge carries a current device session.
- **Added.** Payment Element: while 3-D Secure runs for a confirm from the card form, the
  element covers the page with a dimmed layer, a spinner and "Verifying your card…" in the
  element's `locale`. The bank's challenge sits above it; clicks on the page are blocked and
  focus is held, so the page's own submit button cannot be pressed twice. It comes down
  before `onSuccess` or `onError` runs. On by default, with no option, as Stripe draws its own
  3-D Secure UI. See [The 3-D Secure overlay](./08b-payment-element.md#the-3-d-secure-overlay).
- **Changed.** Payment Element: `onEscape` is not called while 3-D Secure runs, and Escape
  pressed on the page then does not reach the page's own key handlers. A dialog that closed on
  Escape tore the element down in the middle of the authentication.
- **Changed.** Payment Element: a confirm waits at most 2.5 s for the device fingerprint and
  the Kount session, and the element computes the fingerprint while the buyer types. A
  buyer's first Pay could wait up to 10 s for the fingerprint library and then for the
  fingerprint itself with no limit. A confirm made before the element's fingerprint library had
  loaded also no longer goes out without a fingerprint.
- **Fixed.** Checkout links (hosted checkout): the Apple Pay sheet asks the buyer for their
  name, email and billing address. It asked for none of them, so when the checkout had no
  customer email yet (none typed on the page), the buyer confirmed in the sheet and the
  payment was then refused with nothing charged, and Apple Pay payments reached you with no
  customer name and no billing address for AVS. Google Pay is unchanged: it already asked for
  the email and the full billing address.
- **Added.** `worthypayments.js`: `mountExpressCheckout()`, Apple Pay and Google Pay buttons
  with no card form, for a product page, a cart or above your checkout (Stripe's Express
  Checkout Element). The buyer picks the shipping address and method inside the sheet; your
  `onAddressChange` / `onMethodChange` answer the new total. Nothing is created until the
  buyer authorizes: `createIntent` then receives the buyer's email, name, phone, billing and
  shipping address and the chosen method, so a guest can pay. A total that differs from the
  one the sheet showed last charges nothing (`amount_changed`), and an address you cannot ship
  to is refused before `createIntent` runs (`shipping_unavailable`). See
  [mountExpressCheckout](./08-embedded-sdk.md#mountexpresscheckouttarget-opts).
- **Fixed.** `worthypayments.js` Apple Pay buttons (Payment Element): tapping the button
  opens the Apple Pay sheet. The SDK asked for the buyer's details in a shape Evervault's
  Apple Pay no longer accepts, and the sheet failed before it opened.
- **Fixed.** `worthypayments.js` Google Pay buttons (Payment Element): the sheet asks for the
  full billing address and phone, and the payment carries the buyer's name and address for
  AVS, as the guides said. Only the email came back.
- **Changed.** `worthypayments.js`: with `recurring` set, Google Pay offers only the cards it
  hands back as the card itself (`PAN_ONLY`), which are saved and can renew. A card on the
  buyer's Android device alone (a device token, `card_not_saveable`) is no longer offered on a
  subscription. See
  [Subscriptions paid by wallet](./08b-payment-element.md#subscriptions-paid-by-wallet).
- **Changed.** WooCommerce plugin 2.5.0: **Express checkout** shows the Apple Pay and Google
  Pay buttons themselves on product, cart and checkout pages and in the Cart and Checkout
  blocks, instead of a button that opened a payment sheet. Guests can pay, and the buyer picks
  the shipping address and method in the wallet sheet, each priced by WooCommerce; the order is
  placed with the sheet's e-mail, name, phone, billing and shipping address only when the buyer
  authorizes. With it on, the checkout's own payment form shows no second set of wallet
  buttons. See [Express checkout](./09-woocommerce.md#express-checkout).
- **Added.** WooCommerce plugin 2.5.0: a subscription can be paid with Google Pay through
  Express checkout; its card is saved and renewals charge it. Apple Pay is not offered for a
  subscription (its token cannot be saved yet). See
  [Express checkout](./09-woocommerce.md#express-checkout).
- **Added.** WooCommerce plugin 2.5.0: the plugin serves Apple's domain verification file at
  `/.well-known/apple-developer-merchantid-domain-association`, and saving the settings while
  Apple Pay is on checks that the site serves it. See
  [Apple Pay on your domain](./09-woocommerce.md#apple-pay-on-your-domain).
- **Added.** WooCommerce plugin 2.5.0: deleting the plugin removes its settings, caches and
  scheduled jobs, never an order, payment record or saved card; a translation template ships
  in `languages/`. The plugin now requires WordPress 6.2 (WooCommerce 8.0's own minimum). See
  [Install](./09-woocommerce.md#install).

## 2026-10-20

- **Added.** `GET` and `POST /v1/fraud/three_ds_policy`: read, turn off, or move the
  risk-score 3DS step-up (by default a charge scoring 70–95 is stepped up). If you decide
  3DS yourself with `three_ds.request`, send `{ "risk_step_up": { "enabled": false } }` so
  your rules are the only trigger. Needs `fraud:read` / `fraud:write`. Nothing changes until
  you call it. See [Fraud checks](./23-fraud-checks.md#the-risk-score-3ds-step-up).
- **Fixed.** Payment Element: an `element.update()` called before the element has finished
  loading is no longer lost. An early `update({ clientSecret })` left the element confirming
  the previous intent, and an early `locale`, `appearance` or `defaultValues` did not show.
  See [Payment Element](./08b-payment-element.md#2-mount-the-element-browser).
- **Fixed.** Payment Element: `element.update({ defaultValues })` followed by
  `element.submit()` in the same tick now confirms the billing details the update carried.
  The submit used the values from before the update, so a page that hands over its own
  billing form at submit confirmed with no billing address, and the issuer had nothing to
  check for AVS. In the same way `update({ clientSecret })` followed by `submit()` confirmed
  the previous intent, and a `submit()` sent as the element finished loading was dropped.
  See [Payment Element](./08b-payment-element.md#three-ready-made-forms).
- **Fixed.** Payment Element: `defaultValues.billingDetails.address.line2` reaches the
  payment, from the card form and as the Apple Pay / Google Pay default. It was dropped, and
  the `BillingDefaultValues` type did not accept it. It rides with `line1`, and is not sent
  when the buyer types a different street.
- **Changed.** Payment Element: when 3-D Secure does not authenticate the card
  (`three_ds_failed`) or the buyer closes the challenge (`three_ds_cancelled`), the form and
  `onError`'s `message` give the buyer's sentence in the element's `locale`, as for a
  decline. They gave an English diagnostic ("3DS challenge cancelled.", "3DS authentication
  failed (…)."); `code` and `type` are unchanged. A decline with `decline_code:
  three_ds_failed` now reads the same sentence instead of "Your card was declined. Try another
  card." The form's "Please complete the payment details." is in Spanish on a Spanish form.
  See [Payment Element](./08b-payment-element.md#errors-worth-recognising).
- **Fixed.** Payment Element: a 3-D Secure authentication that completes without a challenge
  no longer grows the element to 620 px for a moment. The page below it jumped by about
  400 px and back.
- **Fixed.** Payment Element: the iframe's height is set with `!important`, so a page
  stylesheet such as `iframe { height: auto !important; }` no longer draws the form at the
  browser's default 150 px, clipped.
- **Fixed.** A charge with an attempted 3-D Secure authentication on Mastercard (ECI `01`) is
  now recorded as shifting liability, as an attempt on Visa, Amex and Discover (ECI `06`)
  already was: `three_ds.liability_shift` is `true` on `GET /v1/charges/{id}`, the `charge.*`
  webhooks' `detail.threeDsLiabilityShift` is `true`, and the dashboard counts the charge as
  liability-shifted. It was recorded as not shifted. Charges recorded before this change are
  corrected; no webhook is sent for them. See [3-D Secure](./07-3ds.md#liability-shift).
- **Changed.** While your account is not yet live, the test-mode transactions we sync back from
  NMI come from your own NMI sandbox account, the one whose key you added under Settings >
  Integrations. They used to come from the sandbox account the platform shared between
  accounts. Without your own sandbox key nothing is synced back in test mode. Test transactions
  synced earlier from the shared account are not changed or removed. Live mode is unchanged.
  See [Testing](./15-testing.md#your-own-nmi-sandbox-key).
- **Changed.** WooCommerce plugin 2.1.0: an order that starts or renews a subscription always
  saves its card. Its payment intent is created with `setup_future_usage: "off_session"` and the
  order's customer (so a buyer who creates their account at checkout is covered), the Element
  draws no **Save this card** box and no Apple Pay / Google Pay buttons for such a cart, and the
  checkout tells the buyer the card is kept for the renewals. For such an order the Store API's
  `worthypayments_save_card` is now `never`, and the new `worthypayments_setup_future_usage` is
  `off_session`. See [WooCommerce Subscriptions](./09-woocommerce.md#woocommerce-subscriptions).
- **Added.** WooCommerce plugin 2.1.0: a $0 subscription sign-up (a free trial) saves its card
  with no payment, through a [setup intent](./08b-payment-element.md#saving-a-card-without-a-payment-setup-intents),
  on the classic checkout, the Checkout block and the Store API. The Store API answers
  `worthypayments_setup_intent` (`seti_…`) and its client secret instead of a payment intent;
  the second call sends `payment_data` `worthypayments_setup_intent`. The plugin completes such
  an order on `setup_intent.succeeded` too: add the `setup_intent.*` events to your store's
  webhook endpoint. See [Headless checkout](./09-woocommerce.md#headless-checkout-store-api).
- **Added.** WooCommerce plugin 2.1.0: **My account → Payment methods → Add payment method**
  saves a new card with no payment, and a subscription's **Change payment** page takes a new
  card as well as a saved one. See [Saved cards](./09-woocommerce.md#saved-cards).
- **Added.** WooCommerce plugin 2.1.0: every pending Store API answer carries
  `worthypayments_intent_type` (`payment_intent` or `setup_intent`), and
  `GET /wc/store/v1/cart` carries `extensions.worthypayments.subscription` (the cart starts a
  subscription). See [Headless checkout](./09-woocommerce.md#headless-checkout-store-api).
- **Changed.** WooCommerce plugin 2.1.0, classic checkout and Checkout block: Place order is
  answered *The payment form is still loading* (and nothing is placed) until the card form has
  loaded; while a payment confirms, the rest of the form is frozen and a busy line shows; a card
  form that loads late clears its "could not load" notice; and a declined `soft_decline_3ds`
  saved card switches the Checkout block to the new-card form.
- **Fixed (docs).** WooCommerce's Store API second call must repeat the whole first body
  (`billing_address` and `payment_method` are required on every call) with an unchanged cart,
  or `POST /wc/store/v1/checkout/{order_id}` finishes one particular order. The guide said to
  post the payment data alone. See [Headless checkout](./09-woocommerce.md#headless-checkout-store-api).

## 2026-10-19

- **Fixed (docs).** An attempted 3-D Secure authentication on Mastercard (ECI `01`) normally
  shifts liability, as an attempt does on Visa, Amex and Discover (ECI `06`). The guide said
  Mastercard's did not. `liability_shift` on the 3DS session, which is the network's own
  answer, is unchanged. See [3-D Secure](./07-3ds.md#what-the-eci-says).
- **Fixed (docs).** Wallets: a Google Pay `PAN_ONLY` card carries no cryptogram, does not
  satisfy SCA and can be stepped up like a card, and a Google Pay payment shifts liability
  only on ECI `05` (Visa) or `02` (Mastercard). The guides said every wallet satisfied SCA and
  carried its own shift. See [Charges](./03-charges.md) and [3-D Secure](./07-3ds.md).
- **Fixed (docs).** The capture windows (7 days; 4 days 18 hours for a Visa merchant-initiated
  charge) are ours and fit inside the card networks' own limits; the guides called them the
  networks' windows. An authorization canceled at the end of its window is not voided at the
  gateway, so the buyer's hold lasts until their bank drops it; the guide said the bank had
  already released it. See [Authorize and capture](./17-auth-capture.md).
- **Fixed (docs).** `description` does not reach the buyer's card statement, and a charge's
  `statement_descriptor` is the descriptor we stored, not necessarily the text the bank
  prints. The guide said both did. See [Charges](./03-charges.md).
- **Fixed (docs).** Subscription renewals are merchant-initiated and charged without 3-D
  Secure; the guide said every renewal ran it. See [Subscriptions](./18b-subscriptions.md).
  In [WooCommerce](./09-woocommerce.md), Subscriptions retries a declined renewal only when
  its **Retry Failed Payments** setting is on, which is off by default.
- **Added.** `metadata` on payment intents: your own key/value pairs on
  `POST /v1/payment_intents`, with the `POST /v1/charges` rules and error messages. It is
  answered with `reference` (on create, on update, and on retrieve to secret and restricted
  keys; never to a publishable key), copied onto the charge the intent makes when the buyer
  pays (so `GET /v1/charges/{id}` and the `charge.*` webhooks' `detail.metadata` carry it),
  and carried by `checkout.session.completed`. A GDPR customer deletion clears it.
- **Added.** `PATCH /v1/payment_intents/{id}` takes `reference`, `customer`, `receipt_email`
  and `metadata`, as well as the amount, while the intent is still open (the same states as
  a re-price; a paid, in-flight or expired intent is still a `400 failed_precondition`).
  `metadata` is merged as on Stripe: a key sent as `""` is removed, keys not sent are kept,
  at most 50 keys after the merge. `reference` and `receipt_email` are cleared with `""` or
  `null`. `customer` is checked like on create, cannot be removed, and cannot change once a
  payment attempt on the intent started (`400 failed_precondition`, `param: customer`).
  `amount` is no longer required when the request changes a detail; a request with neither
  is refused on `amount` as before. The WooCommerce plugin now sends the order's `order_id`
  and `order_number` as the intent's metadata. See
  [Payment Element](./08b-payment-element.md#metadata-and-the-other-details).
- **Fixed.** `PATCH /v1/payment_intents/{id}` changes payment intents only, as documented.
  A `pi_…` id built from another checkout session (a payment link's, an invoice's, a
  subscription's first payment, a store cart) is now a `404` `resource_missing` and nothing
  changes. It used to re-price that open session, so its buyer could be charged an amount
  other than the link or invoice they were sent.
- **Changed.** Apple Pay and Google Pay run on the `EVERVAULT` rail only.
  `GET /v1/wallet-config` no longer has `collectJsTokenizationKey`, and its `provider` is
  `EVERVAULT` or `NONE`. In `worthypayments.js`, `WalletProvider` is `"EVERVAULT" | "NONE"`,
  and `mountWalletButton()` no longer loads the gateway's Collect.js script. A wallet that
  works today keeps working. See [Payment Element](./08b-payment-element.md).
- **Added.** Device sessions give a charge your server sends the buyer's own IP address and
  browser. `wp.getDeviceSession()` in worthypayments.js calls the new
  `POST /v1/device_sessions` from the buyer's browser with your publishable key and resolves a
  `ds_…` id; send it as the new optional `device_session` on `POST /v1/charges`. A session
  lasts 30 minutes, belongs to the account and mode that created it, and may go on more than
  one charge in that time. An unknown or expired one is a `400` with
  `param: "device_session"` (`resource_missing` or `device_session_expired`) that does not
  consume the `Idempotency-Key`. A charge without `device_session` behaves as before, except
  for what it is scored on: with a secret or restricted key, the IP we see is your server's,
  and it is now recorded as your server's address and no longer counted as a buyer IP by
  per-IP velocity, the IP blocklist or the cascade, so one server address can no longer get
  every buyer of your store blocked. Responses do not change. The WooCommerce plugin sends the
  session with a saved-card payment. See
  [Device data collection](./24-device-data-collection.md#device-session-the-buyers-ip-and-browser).
- **Added.** `device_session` on `POST /v1/payment_intents/{client_secret}/confirm` and
  `POST /v1/setup_intents/{client_secret}/confirm`, for a confirm your server sends. The same
  rule as on charges now applies to both: confirmed with a secret or restricted key, your
  server's IP is no longer scored as the buyer's. A confirm from the buyer's browser, the
  Payment Element's included, is unchanged. Responses do not change; an unknown or expired
  session is a `400` with `param: "device_session"` and nothing is charged or verified.
- **Added.** `GET /v1/charges/{id}?expand[]=attempts` lists every processor the charge was
  tried on, in order: the processor, how the attempt ended (`approved`, `declined`, `error`,
  `timed_out` or `pending`, with `reconciled: true` when a lost answer was confirmed
  approved at the gateway), its `decline_code` and `response_code`, whether it was a cascade
  step, when it was sent and how long it took. `final` marks the attempt that produced the
  result. A fraud-blocked charge answers `[]`. Without `expand[]` the charge is unchanged,
  and an `expand[]` value we do not have is ignored. Not on `GET /v1/charges`, and never
  with a publishable key. See
  [Charges](./03-charges.md#which-processors-it-was-tried-on-expandattempts).
- **Fixed.** `created[gte]`, `created[gt]`, `created[lte]` and `created[lt]` on
  `GET /v1/charges` and `GET /v1/customers` now filter the list, as
  [Pagination](./13-pagination.md#filtering-by-creation-time) describes. Until now these bounds
  were ignored: the list came back unfiltered, newest first, as if no `created` had been
  sent. Only a bare `created=<seconds>` worked. If your integration sends these bounds and
  relied on getting every row back, it now gets only the rows created in that window. The
  bounds are now also checked: one in milliseconds, or not a whole number of seconds, is a
  `400` `parameter_invalid_integer` naming it, and a bound sent twice, or `created=` sent
  together with `created[...]`, is a `400` `parameter_invalid`. A list call without
  `created[...]` behaves as before.

## 2026-10-18

- **Breaking.** Test mode needs your own NMI sandbox key. A test-mode call that reaches the
  gateway (a charge, a payment intent or setup intent confirm, saving a card, a capture, void
  or refund) on an account without one is refused with `403` `sandbox_key_required`. It used
  to run on a sandbox account the platform shared between every account, whose saved-card
  storage had filled up, so test card saves were failing for everyone. Add your key in the
  dashboard under Settings > Integrations (NMI card, test mode); no NMI sandbox account yet:
  create one at [guide.nmi.com](https://guide.nmi.com/). Test cards saved before this change
  sit in the shared account and can no longer be charged: remove them and save them again.
  Live mode is unchanged. See [Testing](./15-testing.md#your-own-nmi-sandbox-key).
- **Fixed.** `wp.getFingerprint()` now returns the browser fingerprint on your own pages, as
  [Device data collection](./24-device-data-collection.md#browser-fingerprint) describes. On
  any page outside our domain it resolved `null`, because the browser refused the fingerprint
  library the SDK loads, so a charge you build server-side carried no `device.fingerprint`.
  Your code does not change. The Payment Element was not affected.
- **Fixed.** A refusal by the card gateway that is not about the billing address (a duplicate,
  an invalid expiry, a vault the gateway would not write) no longer reads as an address
  failure: its `decline_code` is `processing_error` with "The transaction could not be
  processed.", where it was `avs_rejected` with "The billing address could not be verified."
  A refusal by your gateway's address rule is still `avs_rejected`. This shows on a setup
  intent's `last_setup_error`.
- **Changed.** `POST /v1/charges` on a saved card with `"initiated_by": "customer"` (the buyer
  paying now on your checkout) now applies your 3-D Secure rules, as the Payment Element does:
  the route's **Attempt 3DS**, the 3DS risk band, a customer marked **Require 3DS** and a fraud
  rule asking for 3DS. The card is authenticated on the encrypted copy kept when it was saved:
  the charge answers `requires_action` with `next_action.three_ds.session_id`, and you resubmit
  it with `three_ds.session_id` under the same `Idempotency-Key`. A card with no such copy
  (saved before 2026-10-18, or imported from your gateway vault) is declined `402
  card_declined`, `decline_code: soft_decline_3ds`, with nothing charged: ask the buyer to enter
  the card again. These charges used to go through without 3DS. `three_ds.request` is now
  accepted on such a charge; it is still a `400` on a merchant-initiated saved-card charge,
  which stays without 3DS. See [3DS on a saved card](./07-3ds.md#3ds-on-a-saved-card).
- **Fixed.** A hosted-checkout or Payment Element payment left on "processing" by an attempt
  that died before reaching the gateway now resolves as not charged after 15 minutes, so the
  buyer can pay again; it used to stay processing for up to 24 hours. An attempt that was paid
  meanwhile resolves as paid.
- **Fixed.** A decline-recovery retry that stalled without taking any money no longer blocks its
  recovery: it is retried at the next scheduled time, or the recovery ends at its last attempt
  (`decline_recovery.failed`). A recovery whose original payment has since been paid stops
  without charging again.
- **Fixed.** A capture or void that stalled and finds the payment already moved on (a later
  void, an expiry, a void that completed meanwhile) books nothing and answers the same
  `failed_precondition` refusal as any charge that cannot be captured or voided, instead of
  overwriting the newer state.
- **Fixed.** A charge whose gateway answer arrived after our background reconciliation had
  already settled it is no longer booked a second time: the charge answers with the settled
  result, and an approval the reconciliation had closed as not found takes the payment back.
- **Fixed.** A charge on a saved card is sent with the gateway account that holds the card. A
  saved card exists in one processor account only, and a charge routed to another account used
  to fail; now the charge moves to the card's own account, and a fallback route is tried only
  on that same account. Responses are unchanged.
- **Changed.** Cards imported from your existing NMI customer vault keep the stored-credential
  series your own NMI integration began, so their renewals reference the original transaction.
  Removing an imported card here never deletes it at NMI; your own integration may still use it.
- **Added.** `worthypayments.js`: a `recurring` option on `mountPaymentElement` for a payment
  that starts a subscription. Apple Pay opens as a recurring payment request (your billing
  agreement, cadence, optional trial and a management link), and a wallet payment asks to save
  its payment method, as a ticked "save my card" does. A Google Pay card returned as the plain
  card is saved and can renew; Apple Pay and tokenised Google Pay still answer
  `card_save: skipped` / `card_not_saveable` for now. Amounts are in minor units. See
  [Payment Element](./08b-payment-element.md).
- **Added.** Tax rates for your own store (secret or restricted keys): `GET`/`POST
  /v1/tax_rates`, `GET`/`POST`/`DELETE /v1/tax_rates/{id}` (delete is a soft delete), attach
  and detach a rate on a product with `POST /v1/products/{id}/tax_rates` and
  `DELETE /v1/products/{id}/tax_rates/{tax_rate}`, and `POST /v1/tax/calculate` for up to 100
  lines. Rates are percent strings with at most two decimals (`"7.25"`). New scopes `tax:read`
  and `tax:write` (server-only), not on new keys by default.
- **Added.** Fraud controls for your own tools (secret or restricted keys): `GET`, `POST` and
  `DELETE /v1/customers/{id}/three_ds` read, set and clear a customer's Require 3DS flag;
  `GET /v1/fraud/rules` lists your fraud rules and `GET /v1/payment_workflow/routes` your payment
  routes, read-only. New scopes `fraud:read` and `fraud:write` (server-only). A platform rule
  still outranks a flag or a request. See [Fraud checks](./23-fraud-checks.md).
- **Changed.** A 3-D Secure session left pending for more than 24 hours can no longer be
  completed; start a new one. Deleting a customer now also clears the buyer details kept on
  their pending 3DS sessions. See [3-D Secure](./07-3ds.md).
- **Changed.** 3-D Secure now applies to a saved card the buyer pays with in the Payment
  Element or on the hosted checkout page. Your own rules apply to it (a route with Require
  3DS on, your 3DS policy, a fraud rule asking for a challenge), and so does
  `three_ds.request` on a payment intent; such a card used to be charged without 3-D Secure.
  The card is authenticated where it is: the answer is `requires_action`, and the buyer
  completes the bank's check without re-entering the card. A card saved before this date, or
  imported, has no copy to authenticate: that attempt is declined `soft_decline_3ds` with
  nothing charged, the buyer enters the card once more, and it is kept for the next time.
  On `POST /v1/charges`, a buyer-present saved-card charge follows the same rules (see the
  `initiated_by` entry above); a merchant-initiated one still refuses `three_ds.request`
  (`400`), and the error now says to collect the card again. See
  [3DS on a saved card](./07-3ds.md#3ds-on-a-saved-card).
- **Added.** `setup_future_usage` (`off_session` | `on_session`) on
  `POST /v1/payment_intents`: the card the intent is paid with is saved to the intent's
  `customer` whatever the buyer chooses, as the card's initial stored-credential payment. The
  Payment Element draws no "save this card" box for such an intent; a `mode: 'payment'` mount
  takes `setupFutureUsage` for the same. `customer` is required with it (400
  `parameter_missing`, `param: customer`). Every read of the intent answers
  `setup_future_usage` (`null` when unset). See
  [Always saving the card](./08b-payment-element.md#always-saving-the-card-subscriptions).
- **Added.** Setup intents save and verify a card with no payment: `POST /v1/setup_intents`
  (secret key, `customers:write`), `GET /v1/setup_intents/{client_secret}` and
  `POST /v1/setup_intents/{client_secret}/confirm`. The confirm runs a $0 verification that
  saves the card to the customer as its initial stored-credential transaction, with 3-D Secure
  when `three_ds.request` asks for it. The Payment Element mounted with a setup intent's
  `client_secret` (`seti_…`) is a card form with a "Save card" button, and `onSuccess`
  receives `setupIntentId` and `payment_method`. New webhook events `setup_intent.succeeded`
  and `setup_intent.setup_failed`. See
  [Saving a card without a payment](./08b-payment-element.md#saving-a-card-without-a-payment-setup-intents).
- **Added.** `POST /v1/setup_intents/{id}/cancel` (secret key, `customers:write`) cancels an
  open setup intent, with an optional `cancellation_reason` (`abandoned` | `duplicate` |
  `requested_by_customer`); a canceled intent takes no card (400
  `setup_intent_unexpected_state`). `GET /v1/setup_intents` (secret key, `customers:read`)
  lists them newest first, cursor-paged, optionally for one `customer`. Every setup intent
  now answers `cancellation_reason` (`null` unless canceled) and may read `status: canceled`.
- **Added.** A setup intent's confirm meets the same fraud screening as a payment (your
  blocked customers and cards, card-testing detection, your velocity rules) before the card
  is verified. A refusal is a declined attempt: 200 `requires_payment_method` with
  `last_setup_error` `card_declined` / `decline_code` `fraudulent`, counted toward the
  intent's five attempts.
- **Added.** `PATCH /v1/subscriptions/{id}` takes `default_payment_method` (`pm_…`): the card
  later renewals charge, switched with no charge now, typically the one a setup intent just
  saved. It must be an active saved card of the subscription's customer (else 404). See
  [Changing a subscription's card](./08b-payment-element.md#changing-a-subscriptions-card).
- **Added.** `worthypayments.js`: `mountPaymentElement({ mode: 'setup', createIntent })`
  mounts the element before its setup intent exists; `createIntent` resolves the `seti_…`
  secret your server created. A deferred mount now refuses a secret of the other kind
  (`invalid_client_secret`): a setup intent's on `mode: 'payment'`, or a payment intent's on
  `mode: 'setup'`.
- **Changed.** A merchant-initiated charge on a saved card (a subscription renewal, an invoice
  charge, or `POST /v1/charges` on a saved card) is never sent to 3-D Secure: there is no buyer
  to complete a challenge, so it is charged without one. A customer flagged Require 3-D Secure
  or a fraud rule asking for a challenge used to decline it `soft_decline_3ds`. A fraud rule
  that blocks still blocks it. See [3-D Secure](./07-3ds.md).
- **Changed.** Automatic decline recovery now retries only charges we schedule ourselves with
  no buyer present: renewals of our subscriptions and invoice charges. A declined
  `POST /v1/charges` is no longer retried by us, so your own billing system's retry rules are
  the only retry and a renewal cannot be charged twice. A saved card the buyer chooses at the
  hosted checkout or in the Payment Element is not retried later either: the buyer sees the
  decline and can pay another way. Responses are unchanged. See [Charges](./03-charges.md).
- **Added.** `worthypayments.js` mounts the Payment Element before a payment intent exists:
  `mountPaymentElement(target, { mode: 'payment', amount, currency, createIntent })`, Stripe's
  `elements({ mode: 'payment', amount, currency })`. `createIntent` creates the intent on your
  server when the buyer pays (Pay, `element.submit()` or an approved wallet sheet) and resolves
  its `client_secret`; the element then confirms it as before. `amount` is the wallet sheet
  total; the intent's amount is the one charged. `element.update({ amount, currency })` redraws
  the wallet buttons on such a mount. New error codes: `intent_creation_failed` on `onError`;
  `client_secret_with_mode`, `invalid_mode`, `invalid_amount`, `invalid_currency`,
  `missing_create_intent` at mount, and `update_not_supported` from `update()`. A
  `clientSecret` mount is unchanged. See
  [Mount before the order exists](./08b-payment-element.md#mount-before-the-order-exists).
- **Added.** `three_ds.request` on `POST /v1/charges`, `POST /v1/payment_intents` and
  `POST /v1/3ds/sessions`: `any`, `challenge` or `challenge_mandated` asks for 3-D Secure on
  that payment even when no rule does (`automatic`, the default, leaves it to your rules). A
  rule always outranks a request. Only on a card entered for this payment. `worthypayments.js`
  types it on `CreateChargeParams`. See [3-D Secure](./07-3ds.md).
- **Breaking.** A charge resubmitted with a `three_ds.session_id` whose session did not
  authenticate is refused instead of being charged without authentication: `402`
  `three_ds_failed` or `three_ds_card_not_enrolled` (stored under the `Idempotency-Key`), `409`
  `three_ds_session_incomplete` while it is still pending, and `503`
  `three_ds_result_unavailable` when the result cannot be read (both released for a retry).
  An unknown key inside `three_ds` (or `session_id: null`) is a `400`.
- **Added.** `failure_reason` on `GET /v1/3ds/sessions/{id}`: a normalised reason such as
  `card_not_enrolled` or `authentication_failed`.
- **Added.** A charge's `metadata` is stored: `GET /v1/charges/{id}`, the charge list and the
  charge webhooks (`detail.metadata`, including `charge.canceled` and `charge.expired`) return
  what you sent on `POST /v1/charges`; it used to come back only on the create response. It is
  cleared if the customer is deleted (GDPR), everywhere it is copied.
- **Breaking.** `metadata` on `POST /v1/charges` is validated: up to 50 keys, each 1-40
  characters without square brackets, each value a string of up to 500 characters. Anything
  else is a `400` `parameter_invalid` with `param: metadata`; such a body was accepted and
  ignored before.
- **Changed.** A charge on a saved card (API, Payment Element, subscription renewal, invoice)
  now sends the card network the transaction id of the payment the card was saved from,
  when one was recorded, and a decline-recovery retry goes out as a merchant-initiated
  charge on the stored card. Responses are unchanged.
- **Added.** `initiated_by` on `POST /v1/charges` with a saved payment method: `customer`
  when the cardholder is paying now with a card they saved, `merchant` (the default) for a
  merchant-initiated charge. Sending it with a fresh card, a wallet, `wp_token` or ACH, or
  `customer` with `recurring: true`, is a `400` `parameter_invalid`.
- **Changed.** Card networks now receive the credential-on-file indicators they expect: the
  charge that saves a card is flagged as the stored credential's first use, a saved card used
  by the buyer at checkout is customer-initiated, and the first charge of a subscription
  (hosted checkout, or a store or checkout-link sale of a subscription product) opens the
  recurring series. Responses are unchanged.
- **Added.** `worthypayments.js`: appearance selectors for placeholders
  (`.Input::placeholder`), the save-this-card checkbox (`.Checkbox`, `.CheckboxInput`,
  `.CheckboxLabel`), the card brand mark (`.CardBrand`), a saved card under the pointer
  (`.SavedItem:hover`) and the Pay button's states (`.Button--disabled`, `.Button--loading`,
  `.Button--paid`), plus the rule property `accentColor`. See
  [Embedded SDK](./08-embedded-sdk.md#rule-selectors).
- **Added.** `worthypayments.js`: the `walletButtons` option sets the Apple Pay and Google
  Pay buttons' theme and type presets and their height (40-55 px). See
  [Wallet buttons](./08-embedded-sdk.md#wallet-buttons).
- **Fixed.** `worthypayments.js`: an Apple Pay button takes the theme it is given (it stayed
  black), and the "Or pay with card" line under the wallet buttons uses your page's font.
- **Changed.** `worthypayments.js`: the Pay button inside the Payment Element is filled with
  the `colorPrimary` you name (a `.Button` `backgroundColor` rule wins over it). It kept a
  fixed green gradient whatever the appearance said. A theme's own primary does not repaint
  it, and an element with no `colorPrimary` looks as before.
- **Added.** `worthypayments.js`: the `payButton` option. `{ label }` replaces "Pay" on the
  idle button (1-40 characters), and `{ display: 'never' }` draws no button so your own calls
  `element.submit()`. See [Payment Element](./08b-payment-element.md).
- **Changed.** `worthypayments.js`: a `fontFamily` naming Inter, Roboto, Open Sans, Lato,
  Montserrat or Poppins renders in that font, because the frame now serves them itself. It
  fell back to the system font. Other families still fall back down your stack.
- **Fixed.** A payment intent answers the `reference` it was created with: on create and
  update, and on retrieve to a secret or restricted key. A publishable key never receives
  it, because the reference is your own order id. It was documented as echoed back and never
  was.
- **Fixed.** A Payment Element on a page that another of your sites frames (for example
  `shop.example.com` inside `dash.example.com`) renders the card form when every host in the
  chain is on the key's allowed domains. Only the page directly above the form was allowed,
  so the browser refused the frame. Firefox reports one level of framing only. See
  [Payment Element](./08b-payment-element.md).
- **Added.** A secret or restricted key can be limited to your server IP addresses and can
  require signed requests (HMAC), both set in the dashboard when the key is created. The
  signing secret is shown once. See [Authentication](./02-authentication.md).
- **Fixed (docs).** `initiated_by: "merchant"` is accepted together with `recurring: true`;
  only `"customer"` with `recurring: true` is refused. The guide said any `initiated_by` with
  `recurring: true` was a `400`. See [Charges](./03-charges.md#charge-a-saved-card).
- **Fixed (docs).** `POST /v1/customers` does not read `external_id`, and no other call sets
  it, so `GET /v1/customers/external/{externalId}/state` finds no customer you created. Use
  `GET /v1/customers/{id}/state`. The guides said to pass `external_id` on create. See
  [Customers](./18-customers.md).
- **Fixed (docs).** An email that already has a customer makes `POST /v1/customers` answer
  `409` with `code: "already_exists"`, not `resource_already_exists`, and the reference no
  longer describes the call as a find-or-create that returns the existing customer: only a
  retry with the same body `idempotency_key` does. See [Customers](./18-customers.md).

## 2026-10-17

- **Breaking.** `POST /v1/customer_sessions` (`customer`) and `GET /v1/customers/{id}/state`
  and `GET /v1/customers/external/{externalId}/state` (`id`) answer the customer id as
  `cus_<uuid>`, as `GET /v1/customers` does; they answered the bare uuid. Requests accept
  either form, as before. Still the bare uuid, unchanged: `customer` on charges (including
  `GET /v1/customers/{id}/transactions`), `customer_id` on pre-dispute alerts, and webhook
  `customerId`.
- **Fixed (docs).** The id table in [Versioning](./20-versioning.md) gives the customer's id
  as `cus_<uuid>` and lists where the bare uuid still appears, and the
  [Events](./19-events.md) example shows the bare uuid.
- **Fixed (docs).** There is no per-minute refund budget: only the per-key request ceilings
  apply, and `Retry-After` names the window that was hit. The guides said 5 refunds a minute
  on a live key.
- **Fixed (docs).** Card-testing auto-block of an IP is an account setting per mode, not per
  key, and server-side saved-card renewals are not exempt from it. See
  [Integration hard rules](./27-integration-hard-rules.md).
- **Fixed (docs).** An attempted 3-D Secure authentication shifts liability on Visa, Amex and
  Discover (ECI 06), not on Mastercard (ECI 01). See [3-D Secure](./07-3ds.md).

## 2026-10-16

- **Changed.** `POST /v1/payment_intents` checks `customer`. A customer this key's merchant
  and mode do not hold is a `400` `invalid_request_error`, `code: resource_missing`,
  `param: customer`, replayed on the same `Idempotency-Key`. A `CUS-…` number is accepted
  and answered as `cus_…`. While the customer lookup is unavailable the create answers
  `503`, which is not stored on the key.
- **Fixed.** A browser confirm with a saved card accepts the `pm_…` id that
  `GET /v1/customers/{id}/payment_methods` returns. It answered `401`
  `customer_session_invalid`.
- **Changed.** A payment intent's `customer` is always answered as `cus_<uuid>` on create,
  retrieve, update and confirm; some intents answered the bare uuid. A `customer` that is
  not a string is a `400` `parameter_invalid`.
- **Fixed.** An intent created with `customer: "cus_…"` now saves the buyer's card to that
  customer; the save used to fail silently. `GET /v1/customers/{id}/state` accepts the
  `cus_…` id.
- **Fixed.** Hosted checkout pages use the appearance set in Settings → Checkout (logo,
  colors, font, corner radius, favicon) and link the terms and privacy pages set in
  Settings → Company.

## 2026-10-15

- **Fixed.** The API's CORS policy allows the `X-Customer-Session` header, so browsers load
  a returning buyer's saved cards in the Payment Element and can pay with one. Browsers
  dropped both requests before.
- **Fixed.** A refusal by a Payment Workflow rule (CVV, AVS or postal-code match, amount
  limits, currency, anonymous IP) is a `402` `card_error`, `code: card_declined`, with a
  `decline_code` such as `incorrect_cvc` or `amount_too_large`. `POST /v1/charges` answered
  `500` and a payment intent confirm `503` `upstream_error`. See
  [Payment Workflow rules](./10-errors.md#payment-workflow-rules).
- **Fixed.** A second confirm sent while the first charge is still running answers
  `status: processing` instead of `503`.
- **Fixed.** A fraud block on a 3-D Secure resubmit is the same `fraudulent` decline as on
  the first submit. It was a `503`.
- **Changed.** The `customer` named on a payment intent owns the payment and the saved card,
  whatever email the buyer types. The email used to decide.
- **Changed.** `worthypayments.js`: an Apple Pay / Google Pay decline reaches `onError` as
  `type: "card_error"`, `code: "card_declined"`, with `declineCode` and the card form's
  sentence in the element's `locale`. A wallet `onSuccess` receives the same shape as a
  card payment.
- **Changed.** `worthypayments.js`: a card-form decline in `onError` has
  `type: "card_error"`, as documented; it was `api_error`. `code` is still
  `card_declined`, and other frame failures stay `api_error`.
- **Added.** `worthypayments.js`: when the intent's amount changed after the wallet buttons
  were drawn, the wallet charges nothing and reports
  `type: "invalid_request_error"`, `code: "amount_changed"`, then redraws the buttons with
  the new total.
- **Changed.** `worthypayments.js`: a confirm waits at least 55 s (it was 15 s). If that
  runs out, the SDK retrieves the intent, which never charges, and reports `succeeded` or
  `processing`. A wallet buyer's email, name, phone and address, and billing `phone` and
  `address.line2`, now reach the payment. Error messages no longer contain the client
  secret.
- **Added.** A charge refused by the scored fraud screen is recorded as a failed charge. The
  `400` from `POST /v1/charges` names it in `charge`; `GET /v1/charges/{id}` reads
  `failure_code: fraudulent` and `outcome.type: blocked`; `charge.failed` fires with
  `decline.declineOrigin: "PLATFORM"`. See [Fraud blocks](./10-errors.md#fraud-blocks-fraud_blocked).
- **Fixed (docs).** The Payment Element guides describe `onLoadError`, the full decline
  table, and matching webhooks on `detail.orderReference`. `update()` keeps the same client
  secret.
- **Fixed (docs).** Sandbox test declines use an amount below 1.00 (the sandbox refuses
  `4000000000000002`), and the sandbox Amex number is `341111111111111`.
- **Fixed (docs).** A `PATCH` on a closed intent is a `400` `failed_precondition`, not
  `412`. A 3-D Secure resubmit with another amount is refused with `400`
  `three_ds_amount_mismatch`. A key's allowed domains can be edited after it is created.

## 2026-10-14

- **Breaking.** A publishable key (`pk_…`) can no longer charge a saved card, capture or
  void: `403 permission_denied`. Use a secret key. A publishable key is public, and a void
  can become a full refund.
- **Fixed.** `POST /v1/charges` passes `billing_address`, `shipping_address`, the customer's
  name and phone and `device.kount_session_id` to the payment, so AVS and fraud screening
  see them. `GET /v1/charges/{id}` returns the stored `billing_details`. The charge is
  attached to the customer with the given email.
- **Fixed.** A payment intent confirmed with a saved card records the card's brand, BIN and
  last four on the charge.
- **Fixed.** A 3-D Secure resubmit is bound to the attempt that stepped up. A
  `three_ds.session_id` from another payment is refused as an unknown session, and nothing
  is charged.

## 2026-10-13

- **Changed.** `POST /v1/check` can deny with `BLOCKED_BYPASS_LINK` when a device or IP
  links to a blocked customer. On charges, that link check obeys your Cascade switches. See
  [Linked-account blocks](./23-fraud-checks.md#linked-account-blocks).

## 2026-10-11

- **Added.** `recurring` on `POST /v1/charges` marks a saved-card charge as a subscription
  renewal. It is accepted only with a saved payment method; otherwise it is a `400`
  `parameter_invalid`. Saved-card charges are merchant-initiated and exempt from your 3-D
  Secure policy. A customer flagged "Require 3-D Secure" declines with
  `soft_decline_3ds`.
- **Fixed.** A customer session lists the customer's saved cards, and confirming with one
  charges it. The list was empty and confirm answered `401` `customer_session_invalid`.
- **Fixed.** A saved card that needs 3-D Secure but cannot run it is declined with `402`
  `soft_decline_3ds`. It answered `requires_action` with an empty `id` and session.
- **Added.** `card_save` on a payment intent confirm, on the retrieve of a succeeded intent
  and in `onSuccess`, with the `pm_…` `payment_method` when the card was saved. See
  [Did the card get saved?](./08b-payment-element.md#did-the-card-get-saved).
- **Added.** `capture_before` and `cancellation_reason` on charges.
- **Changed.** An authorization nobody captured is canceled when the card network's window
  ends (4 days 18 hours for a Visa merchant-initiated charge, 7 days otherwise). The charge
  reads `canceled`, and `charge.expired` and `charge.canceled` fire. See
  [Auth & capture](./17-auth-capture.md).
- **Changed.** A delayed auto-capture runs at your delay or 6 hours before the authorization
  expires, whichever comes first. A failed capture is retried with growing waits, up to an
  hour apart.
- **Fixed.** `capture_method: "manual"` is honoured on a route whose Capture block is
  Instant. Those charges were captured at once.
- **Fixed.** The buyer's IP address is read through our proxies. Fraud IP rules, your key's
  IP allowlist and per-IP throttling saw a proxy address on every API, Payment Element and
  hosted checkout request.
- **Fixed.** Payment Workflow routes are first-match. A catch-all "All other conditions →
  Decline" route no longer declines a charge that an earlier route accepted.

## 2026-10-10

- **Breaking.** When the charge outcome is not yet known, a payment intent confirm answers
  `status: processing`; it was `503` `upstream_error`. `worthypayments.js` now calls
  `onSuccess({ status: "processing" })` instead of `onError` with `payment_unresolved`.
  Check `status` before showing a success page, and fulfil from `charge.succeeded`.
  Retrieving the intent resolves the outcome without charging.
- **Fixed.** Hosted checkout shows a completed payment as completed and opens 3-D Secure
  when asked. It could report a completed payment as failed.
- **Changed.** Webhook endpoints that keep failing are throttled to one request a minute
  after 10 failures in a row. They are auto-disabled only after 24 hours of continuous
  failure, then probed hourly, and the account owner is emailed. Events are queued, not
  dropped. See [Webhooks](./16-webhooks.md#when-an-endpoint-keeps-failing).
- **Fixed.** On payment intents and hosted checkout, a fraud pre-screen block declines with
  `fraudulent`, and an account that cannot take payments declines with
  `merchant_unavailable`. Both were service errors. Hosted checkout charges the total the
  Pay button shows after shipping and tax.
- **Fixed.** A void on a captured charge whose batch has not settled is a real void: the
  charge reads `canceled`, not refunded. A settled charge is still refunded in full.
- **Fixed.** A refund the gateway refuses is a `400` `refund_declined`; it was `201` with
  `status: failed`. A refused capture is a `402`, and a refused void a `400`
  `void_declined`. `description` no longer triggers a duplicate-order refusal.
- **Changed.** There is no per-minute cap on charges, captures, confirms or refunds. It was
  5 a minute on a live key and 50 on a test key. See [Rate limits](./14-rate-limits.md).
- **Changed.** `worthypayments.js`: the Apple Pay / Google Pay sheet stays open until the
  charge answers. `mountWallet`'s `onToken` may return a promise.
- **Fixed.** A payment the gateway itself refuses is an immediate `402` `card_declined` with
  `decline_code` `duplicate_transaction`, `invalid_merchant`, `invalid_transaction` or
  `processing_error`. It was a `502` with the charge left pending. Payment Element
  declines carry `declineCode`.
- **Added.** `outcome` on every charge read: `network_status`, `type`, `seller_message`,
  `risk_level`.
- **Changed.** `GET /v1/disputes/stats` counts dispute cases only. Pre-dispute alerts and
  PayArc RDR resolutions, reversals and credits are left out. `win_rate` is `null` until a
  case is won or lost. **Fixed (docs):** rates are whole percents.
- **Fixed.** `created[lte]`, `created[lt]` and `created=` list filters include rows from
  within the boundary second.

## 2026-10-09

- **Fixed.** Retrying `POST /v1/charges` with the same `Idempotency-Key` after an unknown
  outcome resolves the earlier attempt instead of charging again. While it is unresolved
  the retry answers `409` `idempotency_in_progress`. An unknown outcome reads
  `status: pending`; it was `unknown`.
- **Fixed.** After a decline, a new card on the same payment intent or checkout is charged.
  It used to receive the first card's decline again.

## 2026-10-08

- **Fixed.** `GET /v1/customers/{id}/transactions` lists charges only, with real
  `amount_refunded`, `disputed` and `livemode`. **Fixed (docs):** the refund errors are
  documented. See [Refund a customer's past charges](./06b-refund-past-charges.md).

## 2026-10-07

- **Added.** `card.wallet` is set on approved live sales synced from your NMI gateway once
  NMI names the wallet, usually a few minutes after the sale. No webhook is sent when it
  arrives.

## 2026-10-06

- **Added.** `card.wallet.card_last4` and `card.wallet.card_brand`, the buyer's own card
  inside an Apple Pay / Google Pay wallet. Send them as `wallet.card_last4` /
  `wallet.card_brand` on `POST /v1/charges`; `worthypayments.js` does this for you.
  Webhooks carry `walletCardLast4` / `walletCardBrand`.
- **Added.** `walletType`, `walletCardLast4` and `walletCardBrand` are on a later manual
  capture's `charge.succeeded` and on refunds of a wallet sale.
- **Fixed (docs).** A pre-dispute alert's `matchEvidence` card digits are the issuer's and
  can differ from the sale's card (wallets, virtual cards). Reconcile on `transactionId`,
  the ARN or the auth code.

## 2026-10-05

- **Added.** `card.wallet` (`{ "type": "apple_pay" }` or `{ "type": "google_pay" }`) on a
  wallet charge we processed, and `walletType` on its webhooks.

## 2026-10-02

- **Added.** Dispute `status` `RELATED_CASE` and `parent_dispute_id`, for a PayArc related
  case filed under its head dispute.

## 2026-01-01

Initial public contract for the Worthy Payments API:

- Charges, refunds, customers, payment methods, and payment intents
- Checkout links, products, and subscriptions
- Webhooks, 3-D Secure, and fraud checks
- Browser Payment Element (`worthypayments.js`) and `wp:` tokenization

Pin this date with the `API-Version` request header. Responses echo the resolved
version and a `Request-Id`.

The interactive reference is at [/developers/api-reference](/developers/api-reference).
