Changelog

View as Markdown

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. 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:

TagMeaning
BreakingCould break a correctly-written integration, as defined in What counts as a breaking change.
ChangedBehaves differently, but an integration that follows these guides keeps working.
AddedA new field, value, event, parameter or callback property. Additive; ignore what you do not use.
FixedThe 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, Integration hard rules 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 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • Added. WooCommerce plugin 2.1.0: a $0 subscription sign-up (a free trial) saves its card with no payment, through a setup intent, 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.
  • 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.
  • 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.
  • 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.

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.
  • 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 and 3-D Secure.
  • 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.
  • 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.
  • Fixed (docs). Subscription renewals are merchant-initiated and charged without 3-D Secure; the guide said every renewal ran it. See Subscriptions. In WooCommerce, 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.
  • 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.
  • 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.
  • 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.
  • Fixed. created[gte], created[gt], created[lte] and created[lt] on GET /v1/charges and GET /v1/customers now filter the list, as Pagination 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. 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.
  • Fixed. wp.getFingerprint() now returns the browser fingerprint on your own pages, as Device data collection 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

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 gives the customer's id as cus_<uuid> and lists where the bare uuid still appears, and the Events 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.
  • 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.

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.
  • 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.
  • 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.

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?.
  • 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.
  • 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.
  • 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.
  • 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.

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.