Changelog
View as MarkdownThe 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:
| Tag | Meaning |
|---|---|
| Breaking | Could break a correctly-written integration, as defined in 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: theurlof a checkout link and a payment link, a charge'sreceipt_url, the hosted pay link on invoices and subscriptions, and the links in the emails we send. Links issued earlier onhttps://app.paygeyt.comkeep 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, allowapp.worthypayments.comas 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 onhttps://webhooks.paygeyt.comkeeps 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) defaultsbaseUrltohttps://api.worthypayments.com.https://api.paygeyt.comserves 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_sessionto 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.jsandhttps://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.jsloaded fromapp.worthypayments.com/sdk/v1/defaults tohttps://api.worthypayments.com/v1andhttps://element.worthypayments.com, so a page that loads the SDK from worthypayments.com stays on that domain. Loaded fromapp.paygeyt.com/sdk/v1/, bundled from npm, or hosted on your own site, it keeps the paygeyt.com hosts it always used.apiBaseandelementOriginstill 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.createdfor a chargeback puts a paid order On hold,dispute.wonrestores the status it had, anddispute.lostmarks it Failed, as Stripe's WooCommerce plugin does. A pre-dispute alert and the other dispute events still only add an order note, anddispute.closedno longer adds one (it repeats thedispute.won/dispute.lostit 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-endpointswhen a mode's signing secret is empty, with thecharge.*,dispute.*,pre_dispute_alert.*andsetup_intent.*events the plugin uses, and saves its signing secret; an existing endpoint for the same URL gets its secret rolled instead. Needswebhooks:readandwebhooks:writeon 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;
0turns 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
metadatakeys as payments made in the Element:order_idandorder_number, plussubscription_idon a renewal. They carriedwc_order_id,wc_subscription_idandwc_site; read the new keys incharge.*webhooks andGET /v1/charges. Charges made before the update keep their old keys, so code that reads an order id from a charge should readorder_idand fall back towc_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 beforeonSuccessoronErrorruns. On by default, with no option, as Stripe draws its own 3-D Secure UI. See The 3-D Secure overlay. - Changed. Payment Element:
onEscapeis 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; youronAddressChange/onMethodChangeanswer the new total. Nothing is created until the buyer authorizes:createIntentthen 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 beforecreateIntentruns (shipping_unavailable). See mountExpressCheckout. - Fixed.
worthypayments.jsApple 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.jsGoogle 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: withrecurringset, 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.
GETandPOST /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 withthree_ds.request, send{ "risk_step_up": { "enabled": false } }so your rules are the only trigger. Needsfraud: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 earlyupdate({ clientSecret })left the element confirming the previous intent, and an earlylocale,appearanceordefaultValuesdid not show. See Payment Element. - Fixed. Payment Element:
element.update({ defaultValues })followed byelement.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 wayupdate({ clientSecret })followed bysubmit()confirmed the previous intent, and asubmit()sent as the element finished loading was dropped. See Payment Element. - Fixed. Payment Element:
defaultValues.billingDetails.address.line2reaches the payment, from the card form and as the Apple Pay / Google Pay default. It was dropped, and theBillingDefaultValuestype did not accept it. It rides withline1, 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 andonError'smessagegive the buyer's sentence in the element'slocale, as for a decline. They gave an English diagnostic ("3DS challenge cancelled.", "3DS authentication failed (…).");codeandtypeare unchanged. A decline withdecline_code: three_ds_failednow 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 asiframe { 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 (ECI06) already was:three_ds.liability_shiftistrueonGET /v1/charges/{id}, thecharge.*webhooks'detail.threeDsLiabilityShiftistrue, 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'sworthypayments_save_cardis nownever, and the newworthypayments_setup_future_usageisoff_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 sendspayment_dataworthypayments_setup_intent. The plugin completes such an order onsetup_intent.succeededtoo: add thesetup_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_intentorsetup_intent), andGET /wc/store/v1/cartcarriesextensions.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_3dssaved 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_addressandpayment_methodare required on every call) with an unchanged cart, orPOST /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 (ECI06). The guide said Mastercard's did not.liability_shifton the 3DS session, which is the network's own answer, is unchanged. See 3-D Secure. - Fixed (docs). Wallets: a Google Pay
PAN_ONLYcard carries no cryptogram, does not satisfy SCA and can be stepped up like a card, and a Google Pay payment shifts liability only on ECI05(Visa) or02(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).
descriptiondoes not reach the buyer's card statement, and a charge'sstatement_descriptoris 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.
metadataon payment intents: your own key/value pairs onPOST /v1/payment_intents, with thePOST /v1/chargesrules and error messages. It is answered withreference(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 (soGET /v1/charges/{id}and thecharge.*webhooks'detail.metadatacarry it), and carried bycheckout.session.completed. A GDPR customer deletion clears it. - Added.
PATCH /v1/payment_intents/{id}takesreference,customer,receipt_emailandmetadata, 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 a400 failed_precondition).metadatais merged as on Stripe: a key sent as""is removed, keys not sent are kept, at most 50 keys after the merge.referenceandreceipt_emailare cleared with""ornull.customeris checked like on create, cannot be removed, and cannot change once a payment attempt on the intent started (400 failed_precondition,param: customer).amountis no longer required when the request changes a detail; a request with neither is refused onamountas before. The WooCommerce plugin now sends the order'sorder_idandorder_numberas the intent's metadata. See Payment Element. - Fixed.
PATCH /v1/payment_intents/{id}changes payment intents only, as documented. Api_…id built from another checkout session (a payment link's, an invoice's, a subscription's first payment, a store cart) is now a404resource_missingand 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
EVERVAULTrail only.GET /v1/wallet-configno longer hascollectJsTokenizationKey, and itsproviderisEVERVAULTorNONE. Inworthypayments.js,WalletProvideris"EVERVAULT" | "NONE", andmountWalletButton()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 newPOST /v1/device_sessionsfrom the buyer's browser with your publishable key and resolves ads_…id; send it as the new optionaldevice_sessiononPOST /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 a400withparam: "device_session"(resource_missingordevice_session_expired) that does not consume theIdempotency-Key. A charge withoutdevice_sessionbehaves 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_sessiononPOST /v1/payment_intents/{client_secret}/confirmandPOST /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 a400withparam: "device_session"and nothing is charged or verified. - Added.
GET /v1/charges/{id}?expand[]=attemptslists every processor the charge was tried on, in order: the processor, how the attempt ended (approved,declined,error,timed_outorpending, withreconciled: truewhen a lost answer was confirmed approved at the gateway), itsdecline_codeandresponse_code, whether it was a cascade step, when it was sent and how long it took.finalmarks the attempt that produced the result. A fraud-blocked charge answers[]. Withoutexpand[]the charge is unchanged, and anexpand[]value we do not have is ignored. Not onGET /v1/charges, and never with a publishable key. See Charges. - Fixed.
created[gte],created[gt],created[lte]andcreated[lt]onGET /v1/chargesandGET /v1/customersnow filter the list, as Pagination describes. Until now these bounds were ignored: the list came back unfiltered, newest first, as if nocreatedhad been sent. Only a barecreated=<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 a400parameter_invalid_integernaming it, and a bound sent twice, orcreated=sent together withcreated[...], is a400parameter_invalid. A list call withoutcreated[...]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
403sandbox_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 resolvednull, because the browser refused the fingerprint library the SDK loads, so a charge you build server-side carried nodevice.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_codeisprocessing_errorwith "The transaction could not be processed.", where it wasavs_rejectedwith "The billing address could not be verified." A refusal by your gateway's address rule is stillavs_rejected. This shows on a setup intent'slast_setup_error. - Changed.
POST /v1/chargeson 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 answersrequires_actionwithnext_action.three_ds.session_id, and you resubmit it withthree_ds.session_idunder the sameIdempotency-Key. A card with no such copy (saved before 2026-10-18, or imported from your gateway vault) is declined402 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.requestis now accepted on such a charge; it is still a400on 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_preconditionrefusal 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: arecurringoption onmountPaymentElementfor 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 answercard_save: skipped/card_not_saveablefor 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 withPOST /v1/products/{id}/tax_ratesandDELETE /v1/products/{id}/tax_rates/{tax_rate}, andPOST /v1/tax/calculatefor up to 100 lines. Rates are percent strings with at most two decimals ("7.25"). New scopestax:readandtax:write(server-only), not on new keys by default. - Added. Fraud controls for your own tools (secret or restricted keys):
GET,POSTandDELETE /v1/customers/{id}/three_dsread, set and clear a customer's Require 3DS flag;GET /v1/fraud/ruleslists your fraud rules andGET /v1/payment_workflow/routesyour payment routes, read-only. New scopesfraud:readandfraud: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.requeston a payment intent; such a card used to be charged without 3-D Secure. The card is authenticated where it is: the answer isrequires_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 declinedsoft_decline_3dswith nothing charged, the buyer enters the card once more, and it is kept for the next time. OnPOST /v1/charges, a buyer-present saved-card charge follows the same rules (see theinitiated_byentry above); a merchant-initiated one still refusesthree_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) onPOST /v1/payment_intents: the card the intent is paid with is saved to the intent'scustomerwhatever the buyer chooses, as the card's initial stored-credential payment. The Payment Element draws no "save this card" box for such an intent; amode: 'payment'mount takessetupFutureUsagefor the same.customeris required with it (400parameter_missing,param: customer). Every read of the intent answerssetup_future_usage(nullwhen 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}andPOST /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 whenthree_ds.requestasks for it. The Payment Element mounted with a setup intent'sclient_secret(seti_…) is a card form with a "Save card" button, andonSuccessreceivessetupIntentIdandpayment_method. New webhook eventssetup_intent.succeededandsetup_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 optionalcancellation_reason(abandoned|duplicate|requested_by_customer); a canceled intent takes no card (400setup_intent_unexpected_state).GET /v1/setup_intents(secret key,customers:read) lists them newest first, cursor-paged, optionally for onecustomer. Every setup intent now answerscancellation_reason(nullunless canceled) and may readstatus: 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_methodwithlast_setup_errorcard_declined/decline_codefraudulent, counted toward the intent's five attempts. - Added.
PATCH /v1/subscriptions/{id}takesdefault_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;createIntentresolves theseti_…secret your server created. A deferred mount now refuses a secret of the other kind (invalid_client_secret): a setup intent's onmode: 'payment', or a payment intent's onmode: 'setup'. - Changed. A merchant-initiated charge on a saved card (a subscription renewal, an invoice
charge, or
POST /v1/chargeson 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 itsoft_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/chargesis 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.jsmounts the Payment Element before a payment intent exists:mountPaymentElement(target, { mode: 'payment', amount, currency, createIntent }), Stripe'selements({ mode: 'payment', amount, currency }).createIntentcreates the intent on your server when the buyer pays (Pay,element.submit()or an approved wallet sheet) and resolves itsclient_secret; the element then confirms it as before.amountis 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_failedononError;client_secret_with_mode,invalid_mode,invalid_amount,invalid_currency,missing_create_intentat mount, andupdate_not_supportedfromupdate(). AclientSecretmount is unchanged. See Mount before the order exists. - Added.
three_ds.requestonPOST /v1/charges,POST /v1/payment_intentsandPOST /v1/3ds/sessions:any,challengeorchallenge_mandatedasks 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.jstypes it onCreateChargeParams. See 3-D Secure. - Breaking. A charge resubmitted with a
three_ds.session_idwhose session did not authenticate is refused instead of being charged without authentication:402three_ds_failedorthree_ds_card_not_enrolled(stored under theIdempotency-Key),409three_ds_session_incompletewhile it is still pending, and503three_ds_result_unavailablewhen the result cannot be read (both released for a retry). An unknown key insidethree_ds(orsession_id: null) is a400. - Added.
failure_reasononGET /v1/3ds/sessions/{id}: a normalised reason such ascard_not_enrolledorauthentication_failed. - Added. A charge's
metadatais stored:GET /v1/charges/{id}, the charge list and the charge webhooks (detail.metadata, includingcharge.canceledandcharge.expired) return what you sent onPOST /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.
metadataonPOST /v1/chargesis validated: up to 50 keys, each 1-40 characters without square brackets, each value a string of up to 500 characters. Anything else is a400parameter_invalidwithparam: 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_byonPOST /v1/chargeswith a saved payment method:customerwhen 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_tokenor ACH, orcustomerwithrecurring: true, is a400parameter_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 propertyaccentColor. See Embedded SDK. - Added.
worthypayments.js: thewalletButtonsoption 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 thecolorPrimaryyou name (a.ButtonbackgroundColorrule 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 nocolorPrimarylooks as before. - Added.
worthypayments.js: thepayButtonoption.{ label }replaces "Pay" on the idle button (1-40 characters), and{ display: 'never' }draws no button so your own callselement.submit(). See Payment Element. - Changed.
worthypayments.js: afontFamilynaming 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
referenceit 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.cominsidedash.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 withrecurring: true; only"customer"withrecurring: trueis refused. The guide said anyinitiated_bywithrecurring: truewas a400. See Charges. - Fixed (docs).
POST /v1/customersdoes not readexternal_id, and no other call sets it, soGET /v1/customers/external/{externalId}/statefinds no customer you created. UseGET /v1/customers/{id}/state. The guides said to passexternal_idon create. See Customers. - Fixed (docs). An email that already has a customer makes
POST /v1/customersanswer409withcode: "already_exists", notresource_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 bodyidempotency_keydoes. See Customers.
2026-10-17#
- Breaking.
POST /v1/customer_sessions(customer) andGET /v1/customers/{id}/stateandGET /v1/customers/external/{externalId}/state(id) answer the customer id ascus_<uuid>, asGET /v1/customersdoes; they answered the bare uuid. Requests accept either form, as before. Still the bare uuid, unchanged:customeron charges (includingGET /v1/customers/{id}/transactions),customer_idon pre-dispute alerts, and webhookcustomerId. - 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-Afternames 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_intentscheckscustomer. A customer this key's merchant and mode do not hold is a400invalid_request_error,code: resource_missing,param: customer, replayed on the sameIdempotency-Key. ACUS-…number is accepted and answered ascus_…. While the customer lookup is unavailable the create answers503, which is not stored on the key. - Fixed. A browser confirm with a saved card accepts the
pm_…id thatGET /v1/customers/{id}/payment_methodsreturns. It answered401customer_session_invalid. - Changed. A payment intent's
customeris always answered ascus_<uuid>on create, retrieve, update and confirm; some intents answered the bare uuid. Acustomerthat is not a string is a400parameter_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}/stateaccepts thecus_…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-Sessionheader, 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
402card_error,code: card_declined, with adecline_codesuch asincorrect_cvcoramount_too_large.POST /v1/chargesanswered500and a payment intent confirm503upstream_error. See Payment Workflow rules. - Fixed. A second confirm sent while the first charge is still running answers
status: processinginstead of503. - Fixed. A fraud block on a 3-D Secure resubmit is the same
fraudulentdecline as on the first submit. It was a503. - Changed. The
customernamed 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 reachesonErrorastype: "card_error",code: "card_declined", withdeclineCodeand the card form's sentence in the element'slocale. A walletonSuccessreceives the same shape as a card payment. - Changed.
worthypayments.js: a card-form decline inonErrorhastype: "card_error", as documented; it wasapi_error.codeis stillcard_declined, and other frame failures stayapi_error. - Added.
worthypayments.js: when the intent's amount changed after the wallet buttons were drawn, the wallet charges nothing and reportstype: "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 reportssucceededorprocessing. A wallet buyer's email, name, phone and address, and billingphoneandaddress.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
400fromPOST /v1/chargesnames it incharge;GET /v1/charges/{id}readsfailure_code: fraudulentandoutcome.type: blocked;charge.failedfires withdecline.declineOrigin: "PLATFORM". See Fraud blocks. - Fixed (docs). The Payment Element guides describe
onLoadError, the full decline table, and matching webhooks ondetail.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 is341111111111111. - Fixed (docs). A
PATCHon a closed intent is a400failed_precondition, not412. A 3-D Secure resubmit with another amount is refused with400three_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/chargespassesbilling_address,shipping_address, the customer's name and phone anddevice.kount_session_idto the payment, so AVS and fraud screening see them.GET /v1/charges/{id}returns the storedbilling_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_idfrom another payment is refused as an unknown session, and nothing is charged.
2026-10-13#
- Changed.
POST /v1/checkcan deny withBLOCKED_BYPASS_LINKwhen 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.
recurringonPOST /v1/chargesmarks a saved-card charge as a subscription renewal. It is accepted only with a saved payment method; otherwise it is a400parameter_invalid. Saved-card charges are merchant-initiated and exempt from your 3-D Secure policy. A customer flagged "Require 3-D Secure" declines withsoft_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
401customer_session_invalid. - Fixed. A saved card that needs 3-D Secure but cannot run it is declined with
402soft_decline_3ds. It answeredrequires_actionwith an emptyidand session. - Added.
card_saveon a payment intent confirm, on the retrieve of a succeeded intent and inonSuccess, with thepm_…payment_methodwhen the card was saved. See Did the card get saved?. - Added.
capture_beforeandcancellation_reasonon 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, andcharge.expiredandcharge.canceledfire. 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 was503upstream_error.worthypayments.jsnow callsonSuccess({ status: "processing" })instead ofonErrorwithpayment_unresolved. Checkstatusbefore showing a success page, and fulfil fromcharge.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 withmerchant_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
400refund_declined; it was201withstatus: failed. A refused capture is a402, and a refused void a400void_declined.descriptionno 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'sonTokenmay return a promise. - Fixed. A payment the gateway itself refuses is an immediate
402card_declinedwithdecline_codeduplicate_transaction,invalid_merchant,invalid_transactionorprocessing_error. It was a502with the charge left pending. Payment Element declines carrydeclineCode. - Added.
outcomeon every charge read:network_status,type,seller_message,risk_level. - Changed.
GET /v1/disputes/statscounts dispute cases only. Pre-dispute alerts and PayArc RDR resolutions, reversals and credits are left out.win_rateisnulluntil a case is won or lost. Fixed (docs): rates are whole percents. - Fixed.
created[lte],created[lt]andcreated=list filters include rows from within the boundary second.
2026-10-09#
- Fixed. Retrying
POST /v1/chargeswith the sameIdempotency-Keyafter an unknown outcome resolves the earlier attempt instead of charging again. While it is unresolved the retry answers409idempotency_in_progress. An unknown outcome readsstatus: pending; it wasunknown. - 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}/transactionslists charges only, with realamount_refunded,disputedandlivemode. Fixed (docs): the refund errors are documented. See Refund a customer's past charges.
2026-10-07#
- Added.
card.walletis 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_last4andcard.wallet.card_brand, the buyer's own card inside an Apple Pay / Google Pay wallet. Send them aswallet.card_last4/wallet.card_brandonPOST /v1/charges;worthypayments.jsdoes this for you. Webhooks carrywalletCardLast4/walletCardBrand. - Added.
walletType,walletCardLast4andwalletCardBrandare on a later manual capture'scharge.succeededand on refunds of a wallet sale. - Fixed (docs). A pre-dispute alert's
matchEvidencecard digits are the issuer's and can differ from the sale's card (wallets, virtual cards). Reconcile ontransactionId, 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, andwalletTypeon its webhooks.
2026-10-02#
- Added. Dispute
statusRELATED_CASEandparent_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) andwp: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.
