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-11-02#
- Changed. Webhook
detail.settlementBatchIdis omitted instead of"0"on refunded, enriched and capture events that carry stored transaction detail.GET /v1/charges/{id}also omitssettlement.batch_idinstead of"0"; when the batch was the only settlement value, the wholesettlementobject is absent. Real batch ids are unchanged. The guides already say empty fields are omitted. - Fixed.
GET /v1/customers/{id}/three_dsreturns an HTTP 500 or 503 API error (type: api_error,code: upstream_error) when enforcement state cannot be read, instead of reportingrequire_3ds: false.POSTandDELETEon the same endpoint can return that error after the flag change has already been applied, if the read-back fails. Retrying the same operation is idempotent; a 5xx does not mean the write was rolled back.
2026-10-30#
- Added.
appearance.rulesaccepts four more selectors for the saved-cards-across-stores (Link) steps of the Payment Element:.CodeInput(the 6-digit code field),.Actionand.Action:hover(the Change, Not you?, Send a new code and Use a different card text buttons) and.Badge(the Default mark). Nothing changes until you name them. See Payment Element. - Changed. In the Payment Element, the email a shopper types for their saved cards also fills the element's billing email field while that field is empty, so they type it once. An address they type into the billing field themselves is kept. Saved cards now show their network's mark and name ("Mastercard") the way a typed card does.
- Fixed. The Payment Element's field labels follow
colorText. They were drawn near-black whatever the theme, so onnightor any darkcolorTextthey were unreadable. A saved card's brand name had the same problem. With the default theme the labels are now the default text colour (#30313d), a shade lighter than before. - Fixed. An Apple Pay or Google Pay payment made with a device token (a cryptogram) in the
Payment Element or the express element is charged. It used to be refused inside the platform
before it reached the processor and was left without an outcome. A wallet charge, here and on
/v1/charges, now also records the card network ascard_brand. Nothing changes in what you send. See Payment Element. - Fixed. A card charge made in the Payment Element, the express element or the hosted
checkout, and a charge on a saved payment method (
pm_…), now records the card's expiry: the webhookdetail'scardExpMonth/cardExpYearwere0before. A card charge also keeps itscard.bin,card.last4andcard.brandwhere they used to be left empty. Nothing changes in what you send. See Webhooks. - Fixed. A Payment Element payment sealed in
wp:mode records its card'scard.bin,card.last4,card.brandand expiry, which were empty: worthypayments.js now sends the sealed card'scard.bin,card.last4and expiry besidewp_tokenon the confirm. If you confirm awp:token yourself, send them too. See Payment Element. - Added. Error code
wp_token_card_mismatch(400, do not retry): awp:charge whose sealed card does not start with thebinor end with thelast4sent beside it is refused, on the confirm and onPOST /v1/charges. Both are optional; a charge that sends neither is not checked. OnPOST /v1/chargesawp:card'scard.brandis now derived fromcard.bin. See Tokenization error codes.
2026-10-29#
- Added. The Payment Element can offer a shopper's saved cards from other WorthyPayments
checkouts (Link). It is off until you turn it on in Dashboard > Settings > Checkout >
Payment methods, per mode. When it is on, the element asks for the shopper's email, looks it
up by itself (no button; an address you pass in
defaultValues.billingDetails.emailas soon as the element shows), emails them a 6-digit code, and lists the cards they can pay you with. Your code, the element's options and its callbacks do not change. Before the shopper pays, your page sees none of the email, the code or the cards. A completed payment reports the email they paid with inbilling_details.email, as any card payment does. Turning it on means we email your shoppers a code at their real address. See Payment Element.
2026-10-28#
- Added. A webhook's
detailcarriesfromSavedPaymentMethod: truewhen the charge was made on a saved payment method (apm_…): a subscription renewal, a retry of a declined renewal, a charge on a stored card. Such a charge was not approved in a wallet for that payment, even whenwalletTypenames the wallet the method was saved from. It is left out otherwise, including on the payment that saved the method. See Webhooks. - Changed. A charge on a saved Apple Pay payment method, such as a subscription renewal, is
reported as an Apple Pay charge:
card.walletis{ "type": "apple_pay" }with thecard_last4/card_brandof the buyer's own card, and the webhook'swalletTypeisapple_pay. Before, it read as a card with no wallet.card.last4andcard.binstill belong to the token. Nothing changes in what you send. See Charges. - Changed. An Apple Pay payment on a
recurringPayment Element or express element is saved for renewals. Its confirm now answerscard_save: savedwith apm_…(before:skipped/card_not_saveable); charge each renewal on thatpm_…withrecurring: true, a merchant-initiated charge that refers to this first payment. The wallet credential carries a newrecurring: truewhen the sheet was opened as a recurring payment request:mountWalletButtonsets it from the wallet's answer and the Payment Element forwards it. Only an Apple Pay payment with it, on a confirm that saves, is saved. A one-off Apple Pay sheet and tokenised Google Pay answer as before. See Payment Element. - Added. Every payment intent object carries
last_payment_errorandcancellation_reason.GET /v1/payment_intents/{client_secret}answers the refusal of the intent's latest payment attempt (type: card_error,code: card_declined,message,decline_code,network_status) while the intent isrequires_payment_methodorcanceled; before, only the declined confirm said why, so your server could not read back a decline the buyer saw in the Element. Both arenullotherwise. See Payment Element. - Changed. A payment intent takes at most five payment attempts that end without a
payment. The confirm that is the fifth answers
200canceledwithcancellation_reason: failed_attemptsand itslast_payment_error; a later confirm is400 payment_intent_attempts_exceeded, and a confirm of an intent you canceled (or that expired) is400 payment_intent_unexpected_state. Nothing is charged on either. The same cap holds on a hosted checkout or payment link session. A buyer who needs more tries needs a new intent.worthypayments.jsresolves that fifth confirm asrequires_payment_methodwith itslast_payment_error, so the Payment Element shows the decline (it read the newcanceledasprocessingbefore). - Added.
POST /v1/payment_intents/{id}/cancel(payment_intents:create) cancels an open payment intent, withcancellation_reasonrequested_by_customer(default),duplicate,fraudulentorabandoned. A processing or succeeded intent is400 payment_intent_unexpected_state. - Changed.
POST /v1/setup_intentsrefuses an eleventh setup intent for the same customer within 24 hours:429 setup_intent_limit_exceeded(param: customer). Reuse an open intent rather than creating one per page view. - Fixed.
POST /v1/customersrepeated with theidempotency_keyof a customer that was deleted since answered the deleted customer as if it were live. It now answers409 customer_deleted(param: idempotency_key). - Added.
POST /v1/customers/searchwith{ "email": "…" }looks a customer up by email with the email in the body, out of load balancer access logs.GET /v1/customers?email=keeps working. See Customers. - Added.
GET /v1/pinganswerssettleable_currencies, the currencies a charge or payment intent is accepted in (today["usd"]). Check your store's currency against it when you save your keys. - Fixed. A webhook endpoint's
statuswas alwaysenabled. It is nowdisabledfor an endpoint that receives no deliveries, withdisabled_reason:delivery_failures(the platform switched it off after a sustained run of failed deliveries) ordisabled_by_merchant;nullwhile enabled. Roll-secret answers the real status too. - Changed. Creating a webhook endpoint or rolling its secret without an
Idempotency-Keyis a new request every time. Before, the key was derived from the url or the endpoint id, so a second roll answered the first roll's secret instead of rolling again, and an endpoint deleted and created again came back as the deleted one with its old secret. AnIdempotency-Keyyou send answers for 24 hours. - Changed. A webhook endpoint
urlthat nameslocalhostor a loopback address is refused in test mode and live mode alike:400 url_invalid. An endpoint whose name resolves to a loopback address is not delivered to. Aurlthat is nothttpswas already refused; it now answers400 url_invalidtoo. - Fixed. The
400 idempotency_errorand409 idempotency_in_progressenvelopes now carryrequest_id, as every other error does. - Added.
GET /v1/charges/{id}answersamount_refunded_pending: the part ofamount_refundedwhose refund the gateway has not confirmed yet (a refund request answered409 idempotency_in_progress). A201refund is confirmed and never fails later. See Refunds. - Added.
POST /v1/webhook-endpoints/{id}(webhooks:write) changes an endpoint'surland/orenabled_events(the list is replaced). The answer is the endpoint asGET /v1/webhook-endpoints/{id}returns it; the signing secret is neither changed nor returned. Another merchant's, the other mode's or a deleted endpoint is404 resource_missing. See Webhooks. - Added. Every event from
GET /v1/eventsandGET /v1/events/{id}carriesreplay:truewhen it was written during a backfill or historical replay (it records the past, and no webhook was sent for it),falseotherwise. Always present. - Fixed.
GET /v1/refunds?charge=lists the charge's refunds made in your gateway's own portal (synced from the gateway) as refund objects of that charge, beside those made through the API. Every refund object gainssource(platformorgateway) andcreated(Unix seconds). Before, a portal refund counted in the charge'samount_refundedbut was listed under no charge. - Fixed.
GET /v1/eventsandGET /v1/events/{id}answer an event with the same fields masked as its webhook: card security codes, card-vault tokens (ev:…), a card number found in free text and the other card-data fields read[REDACTED]. They answered an event that had not been delivered to an endpoint unmasked. Identifiers (transaction, gateway, dispute, alert, order ids, ARN) are unchanged. - Fixed. A buyer whose customer you deleted can pay again. Their email still belonged to
the deleted customer, so hosted checkout, and a payment intent created without a
customer, failed with an error whenever the buyer's email was that customer's. The payment now goes ahead and the buyer becomes a new customer. See Customers. - Changed. A deleted customer no longer holds its email.
POST /v1/customerswith that email creates a new customer with a newid, where it answered409 already_exists, andGET /v1/customers?email=finds the new customer.POST /v1/customers/:idcan likewise change a customer'semailto it. An email that belongs to a customer that is not deleted is still a409. The deleted customer stays deleted, with itsidand history. - Fixed. Paying an invoice on its
hosted_invoice_urlpage charges what the invoice says is due. If your account had a default, global or location tax rate, that rate was added again on top of the invoice total, which already includes the invoice's own tax, and an untaxed invoice was taxed too. The invoice was still marked paid at its own amount, so the extra showed only on the charge: itsamountwas above the invoice'samount_due. A payment page opened before this change and not yet paid is corrected as well. See Invoices. - Fixed. A subscription's opening payment taken through its payment session is taxed
exactly as its renewals are. This covers an incomplete subscription
(
payment_behavior=default_incomplete, or no card on file) confirmed withlatest_invoice.payment_intent.client_secret, and a subscription invited by email from the dashboard. The tax comes from the customer's stored address, and there is none when the subscription was created without tax collection. It used to be taxed like a one-off checkout, so a rate that depends on the customer's address could be missed, and a dashboard subscription with tax collection off was still charged your default or global rate. See Subscriptions. - Changed. A promo code is refused on an invoice's payment page and on a subscription's
opening payment session (
coupon not applicable). The hosted page never offered the field there, but the code could still be sent, and the buyer then paid less than the invoice or the first period while the invoice stayed part-paid. A subscription's own coupon, set when it is created, still applies. - Added. An incomplete subscription (
payment_behavior=default_incomplete, or no card on file) whose customer completes the payment now lists that first period as a PAID invoice onGET /v1/invoicesand sendsinvoice.paid, as a subscription whose card is charged at create already did. It carries the period's tax and the subscription's coupon, and its payment is the checkout's charge. See Invoices. - Fixed. A subscription
couponis counted as redeemed when the first period is paid, whichever way it is paid. For an incomplete subscription it used to be counted only at the first renewal, so a coupon with a redemption limit could be used past it in the meantime, and a coupon meant for one period only discounted the first renewal as well. A subscription that is never paid does not use up a redemption. - Fixed (docs). Subscriptions and Invoices
said opening create mints no first invoice. Since 12 October 2026 a card charged at create
writes a PAID invoice for the first period, and a
SEND_INVOICEsubscription that starts at create gets its first period's OPEN invoice. - Fixed. A subscription taxed at an inclusive rate (the price already contains the tax,
such as VAT included) is charged its price. Its renewals, a card charged at create,
SEND_INVOICErenewal invoices, the opening payment session and the preview added the included tax on top again: a 120.00 plan with 20% VAT included was charged 140.00. Only exclusive rates add tax now, as a one-off checkout already did. A subscription invoice's tax shows only the tax added on top; tax included in the price stays in the charge's tax record. See Subscriptions. - Fixed. An invoice that is still owed has one payment session for that amount. A payment
link copied from the dashboard opened a new session each time, beside the one behind
hosted_invoice_url, and each could be paid in full: a customer who paid two of them was charged twice while the invoice counted one payment. Every link now opens the same session until it is paid or expires; after a partial payment the next link is for what is left. See Invoices. - Added. The
hosted_invoice_urlpage and its PDF state the tax included in a subscription's price ("Includes tax") when the rate is inclusive, such as VAT included. The total does not change: a 120.00 plan with 20% VAT included shows a total of 120.00 that includes 20.00 of tax. The invoice's own tax amount still counts only tax added on top. - Fixed. Voiding an invoice closes its payment page's checkout, and cancelling an incomplete subscription closes its opening payment session, so neither takes a payment any more. They used to stay payable until they expired, and a payment made on them was recorded nowhere. This also holds when the subscription is cancelled because its checkout link expired or because payment recovery ran out, and for a checkout that was mid-payment at the time and was then declined. No webhook is sent for a closed checkout.
- Fixed. After a partial payment, or a credit note applied to part of an invoice, the Pay
button on the invoice's
hosted_invoice_urlpage opens a checkout for what is still owed, and the earlier checkout for the full amount is closed. It used to stay payable for the full amount, so a customer could pay the whole invoice again. A Pay link in an invoice email sent before that payment now opens a closed checkout; thehosted_invoice_urlpage always has the current one. See Invoices. - Added. The charge webhooks'
data.object.detailcarries the shipping address the charge was made with:shippingName,shippingPhone,shippingLine1,shippingCity,shippingState,shippingPostalandshippingCountry, each omitted when you sent no value. They ride every event that carries the charge'sdetail:charge.authorized,charge.succeeded(automatic or manual capture),charge.failed,transaction.enrichedand acharge.refundedfor a refund issued here, with theirtransaction.*twins. A sale or refund synced from your gateway carries none, andcharge.canceled/charge.expiredcarry no buyer details at all. Before, a webhook-only integration had no way to see where a charge's goods went. See Webhooks. - Added.
POST /v1/payment_intentsandPATCH /v1/payment_intents/{id}takeshipping_address(the flat shapePOST /v1/chargestakes),line_items(up to 100:name,amountin minor units,quantity1–10000,sku,category) andbilling_details(first_name,last_name). The charge the intent makes sends the shipping address and the lines to fraud scoring, as a server-side charge already could, and the intent keeps them as dispute evidence. None of them changes the amount, tax or a shipping charge;PATCHreplaces each one sent.shipping_addressis answered on create, update and retrieve to secret and restricted keys (nullwhen none). Confirm takesbilling.first_name/billing.last_name, which, like the intent'sbilling_details, are used for the charge's billing name and the customer record instead of splittingbilling.name. Before, an intent's charge carried the intent's singledescriptionline as its basket, no shipping address, and a name split on its last space. See Payment Element. - Added.
noteonPOST /v1/refunds: your own words for why you refunded, up to 500 characters, stored on the refund and shown in your dashboard. Every refund object now has anotefield: the create response,GET /v1/refunds/{id}andGET /v1/refundsreturn the stored note,nullwhen there is none. A note over 500 characters, or one that is not a string, is400 parameter_invalid,param: "note". See Refunds. - Fixed.
GET /v1/refunds/{id}andGET /v1/refundsreturn the refund'sreason, as the Refunds guide showed; they readreason: nullfor every refund. It is the value the create takes (requested_by_customer,duplicate,fraudulent), andnullfor a refund made without one, in your gateway's own portal, or in your dashboard as Product Issue, Service Issue or Other, which none of the three describes. See Refunds. - Added.
line_itemsonPOST /v1/charges: up to 100{ name, amount, quantity?, sku?, category? }items,amountthe unit price in cents, sent to fraud scoring as the order's basket. They never change what is charged. They are stored on the charge and returned asline_items, in the same shape, on the create response,GET /v1/charges/{id}and the charge a capture or void answers with:[]when there are none.GET /v1/chargesdoes not return them. An invalid item is400 parameter_invalidnaming it, e.g.param: "line_items[2].amount". See Charges. - Added.
shippingonGET /v1/charges/{id},GET /v1/chargesandGET /v1/customers/{id}/transactions: the shipping address the charge was made with, as stored,{ name, phone, line1, line2, city, state, postal_code, country }, withline2folded intoline1(soline2isnull) and"[redacted]"for a customer you erased, asbilling_detailsreads.nullwhen there is none. Before, only the create response echoed it. The create response now carriesshipping: nullwhen you sent noshipping_address, where the key used to be absent. See Charges. - Changed. WooCommerce plugin 2.13.0 loads the SDK from
app.worthypayments.com/sdk/v1/, callsapi.worthypayments.com/v1and frameselement.worthypayments.com: the same API, keys and data as paygeyt.com. A site that sets a Content-Security-Policy allows those three hosts, or stays on paygeyt.com withWORTHYPAYMENTS_SDK_URLandWORTHYPAYMENTS_API_BASEinwp-config.php. The order screen's dashboard link opensapp.worthypayments.com. Updates still come fromapp.paygeyt.com. See Configure. - Changed. WooCommerce plugin 2.12.0: a webhook acts only on an order of the mode whose
secret signed it, is deduped on the signed event id, and a dispute is read again
(
GET /v1/disputes/{id}) before its order changes; missed webhooks are recovered fromGET /v1/eventshourly and by a daily reconciliation, so the secret key also needsdisputes:readandevents:read(both default-on for a new secret key). Refunds made in the dashboard or the gateway's portal are recorded one refund object at a time; a captured charge voided before it settled puts its order On hold; an unanswered refund is resent under its key, never re-minted. Updates install only a release signed with a key the plugin pins. A lost dispute records no WooCommerce refund any more: lost for the whole payment, its order is marked Failed (as WooCommerce's own Stripe gateway does); for part of it, the order keeps its status; a win puts it back. The plugin never charges that order again. See Webhooks, Refunds, Disputes and Updates. - Changed. WooCommerce plugin 2.11.0: every payment sends the order's products as
line_items(name, unit price, quantity, SKU, category); a payment intent also sends theshipping_addressand the buyer's first and last name asbilling_details, and an open intent's update removes a part the order no longer has; a refund's typed reason is sent as itsnote, and a refund made in your dashboard reaches the order with its note as the WooCommerce refund's reason. An API that does not know these fields yet gets the payment again without them. See What a payment says about the order and Refunds. - Changed. WooCommerce plugin 2.10.0: saved-card and renewal charges send the buyer's
customer.first_name/last_nameand phone, and ashipping_addresswhen the order ships; every payment'smetadataaddssite_url, andsubscription_idon the order a subscription starts with; customers are created with their account's billing phone. The order screen shows the gateway's answer (auth code, AVS, CVV, 3-D Secure liability, card expiry, acquirer, capture deadline). See Configure and The order screen. - Fixed. WooCommerce plugin 2.10.0: an Apple Pay / Google Pay order no longer records the
device token's last four digits as the buyer's card when the wallet did not state the card;
a refund made in your dashboard reaches the order by the sale it refunds
(
parentTransactionId); a Visa RDR case is recorded as lost when it opens; a pre-dispute alert's note gives its deadline (slaDueAt); a key with the wrong prefix is no longer saved over the working one; the block checkout shows the load error instead of an empty payment box when the payment form cannot load. Two requests charging one order at the same moment can no longer both send a charge (the first keeps the order until its answer is recorded); a payment after a voided or expired authorization pays the order instead of being flagged as a second charge; a declined or voided other charge no longer replaces the paid order's charge. - Added. WooCommerce plugin 2.9.0 updates through WordPress's own screens: a new version shows on Plugins and Dashboard → Updates with its changelog, installs in one click or by WordPress's auto-updates, and is installed only when its SHA-256 checksum matches the one published with it. A renewal cut off by an update is finished on the next request, with the same idempotency key, so it is never charged twice. Versions 2.8.0 and earlier are updated to 2.9.0 once by hand. See Updates.
- Changed. WooCommerce plugin 2.8.0: payments carry one more
metadatakey,wc_site(a short hash of the store's address), on the payment intent and on saved-card and renewal charges, so a charge is proven to be this store's order. A checkout submitted again for a cart the same session paid in the last 5 minutes is cancelled instead of charged. A renewal retry is not sent to a card that was declinedsoft_decline_3dsfor that renewal. A live saved card deleted on a copy of the store is not deleted at WorthyPayments. See Double charges. - Fixed. WooCommerce plugin 2.8.0: money taken for an order that was already paid is noted on the order and listed in the double-charge watch instead of silently replacing the order's charge; a webhook that finds its order busy is retried instead of dropped; two requests can no longer both hold an order's lock.
- Added. WooCommerce plugin 2.7.0: WooCommerce → WorthyPayments activity records every money step the plugin takes or learns about (charges sent with their idempotency key, payments taken, declines, captures, voids, refunds, disputes, the charges it stopped, webhooks, subscription moves), with a summary per mode and period, a double-charge watch, filters and a CSV export, and an activity box on each order and subscription. See Payment activity.
- Fixed. WooCommerce plugin 2.6.0:
wp worthypayments migrate-subscriptionsfinishes on a store with more subscriptions than--batch; it used to read the first page again and again and never end. A run that is stopped part way no longer leaves a subscription half moved: each one moves (and reverts) in its own database transaction. See Moving subscriptions from your NMI plugin. - Changed. WooCommerce plugin 2.6.0: the subscription migration works at any store size.
The mapping file is loaded into a working table and subscriptions are read by id, so memory
stays flat; the API checks are paced to
--rate(10 a second by default) and a429is waited out; every page prints the--from-idto resume from; one command runs at a time.revert-subscriptionsreads what each subscription held before from the subscription itself:--logis no longer read and is required only to record a real revert, and subscriptions moved by plugin 2.5 (which carry no such record) are put back by hand. An unpaid renewal still on NMI moves with its subscription, so its retry is charged through WorthyPayments; a revert gives its unpaid renewals back to NMI with the NMI token. A subscription that cannot be put back is reportederrorinstead of stopping the revert.--batchdefaults to 200. - Fixed. WooCommerce plugin 2.6.0: on a store whose orders use WooCommerce's own tables (HPOS), a renewal order no longer starts with the subscription's card, customer and migration details copied onto it; it now starts as it already did on posts storage.
- Added. WooCommerce plugin 2.6.0:
migrate-subscriptions --report=<file>(one CSV row per subscription),--from-id,--rate; totals show the recurring amount that moves and the renewals due within 24 hours;wp worthypayments forget-mappingdrops the working table. - Fixed. WooCommerce plugin 2.5.1: a subscription renewal is charged once. A second run of the same renewal that had read the order before the first run paid it no longer sends a second charge, and a duplicate WooCommerce Subscriptions scheduled payment action (one that runs while the subscription's next payment is still ahead and still scheduled) no longer creates a second renewal order and charges the same period again; the subscription gets a note instead. Process renewal on the subscription screen, retries of failed renewals and the only payment action run early by hand are unchanged. A saved-card payment whose 3-D Secure challenge the buyer finishes after the order was cancelled, or paid another way, is no longer charged. See WooCommerce Subscriptions.
- Fixed. WooCommerce plugin 2.5.1: a lost dispute no longer marks the order Failed. On a subscription renewal, Failed made WooCommerce Subscriptions put the subscription on hold, e-mail the buyer to pay and, with failed-payment retries on, charge the card again for the period the cardholder had just taken back. The lost amount is now recorded as a refund that moves no money (a full loss leaves the order Refunded). See Disputes.
- Changed. WooCommerce plugin 2.5.1: moving an authorized-only order to Processing or Completed always captures it, and cancelling it always voids it; a capture that fails puts the order back On hold. The Capture setting, which turned this on, is gone: with it on Automatic (the default), an authorized order could be fulfilled without its money being taken. See Capture.
- Fixed. WooCommerce plugin 2.5.1: a refund refused by the API (an open dispute, more than is left) can be tried again for the same amount once its cause is gone; it used to replay the refusal for 24 hours. The "Renewal payment needs confirmation" e-mail is sent once per renewal, not again with every retry.
- Added. WooCommerce plugin 2.5.1: an admin notice while the method is enabled but hidden at checkout because the active mode's keys are not both set; on save, an error when no webhook signing secret is saved or no endpoint sends to the site (with automatic set-up off).
- Fixed. A payment is retried on another processor only when its route asks for it: a Payment Workflow route with fallback processors, or a Failover routing rule with cascade on (only on the processors it lists). A payment that matched no such route used to be retried, after a do-not-honor decline, on every other processor of the account. No live payment was retried that way. See Which processors it was tried on.
2026-10-27#
- Changed. Ethoca and Verifi CDRN pre-dispute alerts received through Kount are now answered
to the card network before their deadline, not only the alerts the platform refunded: 4 hours
before an Ethoca deadline (24 h) and 6 hours before a Verifi one (72 h), 90 minutes before it
while a rule's refund waits for a Visa RDR or a wallet answer. The answer follows the evidence:
previously refunded when the linked sale was already refunded in full, declined when your rule
or an operator decided not to refund, "not refunded, unmatched" when the alert could not be tied
to one of your sales (never "transaction not found"), and "not refunded" otherwise. Not
answered: alerts held for a manual link review while the review is open, alerts already
answered with the dashboard's Kount accept or decline, and alerts Kount closed (these read
SKIPPED). An answer you make directly in Kount 360 is not visible to the platform. OnGET /v1/pre_dispute_alerts,network_statustherefore readsPENDINGand thenSENTon these alerts too, and aNEWalert can readSENT;FAILEDif the answer could not be sent before the deadline. Each change movesupdated_at, as a refund's report always did. No webhook is sent for a network answer. See Webhooks. - Changed. An alert whose linked sale was already refunded in full when the alert arrived,
or when a refund the platform tried was refused because the sale had been refunded another way,
now moves to
REFUNDEDstraight away:pre_dispute_alert.updatedis sent withoutcome: REFUNDEDandrefundStatus: SETTLED, andauto_rule.reasonstill says why (sale-already-refunded,sale-refunded-outside-platform). It used to stayNEWuntil someone closed it. - Added.
auto_rule.reason: network-already-answered: the card network has already been told this alert was not refunded (by the platform, or by your dashboard Kount decline), so no rule refunds it, and a refund from the dashboard is refused (a refund after that answer cannot be reported and would not stop the chargeback). An alert whose earlier answer was a refund that then failed can still be refunded. While an alert's answer is being sent, a refund or dismiss from the dashboard is refused for a few minutes; try again. The dashboard's Kount accept or decline is refused once the alert already has an answer. - Added.
auto_rule.reason: sla-window-closing: with less than 30 minutes of the alert's response window left, a rule no longer starts a refund, since it could complete after the deadline and not stop the chargeback; the alert is answered as not refunded instead. - Fixed. A refund the platform completed through its automatic lane is reported to the card
network and announced with
pre_dispute_alert.updated(refundStatus: SETTLED) every time. When the payment processor's own confirmation arrived before the refund was recorded, both could be skipped. - Changed. A pre-dispute alert we could not tie to a sale when it arrived is matched again,
by the rule a new alert meets, while its response window is still open: when the sale's details
arrive later (its auth code, card BIN, ARN or settlement) and on a regular sweep. When it links
you receive
pre_dispute_alert.updatedwithoutcome: MATCHED, exactly as for a match on arrival, and your auto-refund rules judge the alert then, so a rule may refund it before its deadline. See Webhooks. - Changed. An alert whose response deadline has passed is no longer linked automatically when
its sale learns its ARN later. Before, such an alert could still receive a late
pre_dispute_alert.updatedwithoutcome: MATCHED; now it stays unlinked unless an operator links it. An RDR alert (Visa has already refunded it, so it has no deadline) is still linked when its sale learns a new ARN — at that moment only, as before — so your auto-refund rules never refund a sale Visa already refunded; you receivepre_dispute_alert.updatedwithoutcome: MATCHEDfor it as before. When that ARN arrives through one of our historical replays, the RDR is linked too but nothing is sent to you. - Changed. When an alert is linked because its sale learned its ARN later, your dispute escalation rules no longer run on the alert's dispute at that moment, as for an alert matched on arrival. They used to.
- Changed. An alert that was imported from a provider export, or replayed, is not matched again automatically after it is stored; an operator can still link it.
2026-10-26#
- Changed. Payment Workflow cascade no longer retries
200(declined by processor) on the next processor; only do not honor (201,05) is retried.200is the gateway's catch-all and can stand for an issuer answer the card networks forbid retrying, and the gateway does not return the issuer's own code with the decline. A retry after a decline is also sent only while the card has fewer than 10 declines in the last 24 hours and fewer than 15 in the last 30 days on your account, this charge's own declines included. The card is recognised by its card fingerprint or its BIN and last four; a charge with neither is not retried after a decline. A charge that is not retried ends with its first decline, as before. See Charges. - Added. A pre-dispute auto-rule's wallet hold can cover fraud alerts only:
walletHoldScope: "fraud"besideskipWalletSales: true. A customer dispute or an alert that names no category is then decided by the rule, andauto_rule.hold_waivedsays why (non-fraud-alert,unknown-alert-category), as it does for a waived 3-D Secure hold. Without the key, or with"all", every alert on a wallet sale is held as before; any other value refuses the rule (malformed-rule). See Webhooks. - Added.
three_ds.cryptogram_validationcan benot_recorded: the charge carries 3-D Secure, but no issuer check of the cryptogram was recorded, soliabilityrests on the ECI alone. It used to be omitted in that case, which read the same as a charge with no 3-D Secure. Expect it on every Mastercard charge, on sales recorded before 23 October 2026, and on a new charge until the gateway's result is read (the create response included).liabilityandliability_reasondo not change. - Changed. A charge that threads
three_ds.session_idis refused with400 invalid_request_error,code: three_ds_card_mismatch,param: three_ds.session_idwhen its card states a different last four (card.last4) or expiry (exp_month,exp_year) from the card the cardholder authenticated. Nothing is charged. A detail that either the session or the charge does not state is not compared. A charge on the card that was authenticated is unaffected. - Changed. A Google Pay
PAN_ONLYpayment (the buyer's own card, no cryptogram) made through the Payment Element,mountCheckoutor hosted checkout is now recorded as Google Pay: the charge readscard.wallet: { "type": "google_pay" }and its webhooks carrywalletType: "google_pay", where both used to be empty. It is still charged, scored and stepped up to 3-D Secure as a card, andcard.last4/card.binare the buyer's own card. A wallet-alert hold set on your auto-refund rules now applies to these payments too, as it already did when your gateway ran them. Load the SDK from/sdk/v1/to get it. - Added.
card.wallet_typeonPOST /v1/chargesand on a payment intent confirm:"google_pay"marks a card as a Google PayPAN_ONLYcredential. Any other value is400 parameter_invalid,param: "card.wallet_type". - Fixed (docs). A
walletblock without a cryptogram is refused; the guides said a Google Pay one was charged as a card. Send that card ascardwithcard.wallet_type. The Payment Element guide no longer callsPAN_ONLYthe usual case on iOS (our Google Pay button runs in Chromium browsers only), and says how to accept device tokens only:wallets: 'never'andmountWalletButtonwithallowedAuthMethods: ['CRYPTOGRAM_3DS']; the Element has no such option.
2026-10-25#
- Changed. Hosted checkout pages and payment links take their look (logo, icon, brand and accent color, font, corner radius) from Settings → Branding, the same in test and live mode. The accent color is the Pay button and links; the brand color is the order summary's background. A session's own design, such as a payment link's colors, still wins field by field, and an unset field keeps the default. Settings → Checkout is now Settings → Payment methods and holds only the wallets. Nothing changes in your integration.
- Fixed.
worthypayments.jsdropped a Google Pay card's expiry month and year. The wallet sends them as numbers and the SDK read them as text, so thewalletcredentialonTokenreceived carried emptyexp_monthandexp_year, and a charge that 3-D Secure had to authenticate was declined ("3-D Secure authentication could not be started"). The expiry is now kept whether it arrives as a number or as text. Load the SDK from/sdk/v1/to get the fix; nothing changes in your integration.
2026-10-24#
- Fixed.
charge.succeeded,charge.capturedandtransaction.succeededfor a charge your gateway processed directly (made outside WorthyPayments and synced from the gateway) omittedipandinstrument.bineven when the gateway reported them. They now carry both, as the example payload shows; a value the gateway did not report is still omitted. Webhooks already delivered are not sent again. - Added. Pre-dispute alerts say what they are about.
GET /v1/pre_dispute_alerts(and/{id}) addreason_code, the provider's own code (Kount's alert type such asETHOCA_DISPUTE, or the CDRN issuer reason code such as13.1), andcategory, that code read asfraud,non-fraudorunknown.reasonstaysFRAUDULENTon every Kount-fed alert as before. Thepre_dispute_alert.created/.updatedwebhooks carry the same two fields indata.object.alert(reasonCode,category), and theAUTO_REFUNDEDupdate now also carriesautoRule, the decision that refunded the alert, asGETstates it. See Webhooks. - Fixed. Kount Ethoca alerts imported from a Kount export had no
reason_code, so they readcategory: unknown. They now carry Kount's alert type (ETHOCA_FRAUD,ETHOCA_DISPUTE) like the alerts Kount sends us directly; existing alerts were updated without changing theirupdated_at. The flatreasonCodeof a Kount alert'sMATCHEDupdate saidFRAUDULENT; it is now the provider's code, the same value asdata.object.alert.reasonCode. An alert decision recorded before this date keeps the category it was judged on, so itsauto_rule.alert_categorycan differ from the alert'scategorynow. - Fixed. A pre-dispute alert whose CDRN code is one of Visa's three-level conditions
(
12.6.1duplicate processing,12.6.2paid by other means) readcategory: unknown. It now reads the category of its dispute group, as12.6and13.1already did:non-fraudfor12.x,fraudfor10.x.GET /v1/pre_dispute_alertsanswers the new category for existing alerts too; webhooks already delivered are not sent again. - Changed.
POST /v1/refunds: when your gateway credential could not be read just then (our credential service did not answer), the refund answered403 account_not_activated, as if your account had no gateway key. It now answers503 upstream_error. Nothing was sent to the gateway; retry with the sameIdempotency-Key. A refusal stated by the credential service (no key,sandbox_key_required) answers as before. See Refunds. - Fixed (docs). The API reference still listed the old cascade set (
202,260,51,61,65,N7as retried on another processor) and showed an insufficient-funds decline cascading. Only200and201/05cascade, as the Charges guide already said; the example now cascades a do-not-honor decline. A wallet charge is single-attempt only when it carries a cryptogram: a Google PayPAN_ONLYcard may cascade like any card. The decline table now lists240,460and300, and no224. See Charges. - Fixed (docs).
pre_dispute_alert.*webhooks do not carry the same object asGET /v1/pre_dispute_alerts: the polled alert addsauto_rule, the processor, the sale's gateway id and the received / resolved times. Everyauto_rule.reasonvalue is now listed with what it means.reasonreadsFRAUDULENTon every Kount-fed alert, whatever its type. See Webhooks. - Fixed (docs). Charge webhooks carry the raw 3-D Secure facts (
threeDsEci,threeDsVersion,threeDsCavvResult,threeDsLiabilityShift), not theauthentication/liabilityreadingGET /v1/charges/{id}adds. The 2026-10-23 entry named the CAVV key in snake case; the webhook key isthreeDsCavvResult. - Fixed (docs).
three_ds.authenticatedis alsotruewhen no ECI can be read and the 3-D Secure provider reported a shift; Mastercard ECI06and07read asnot_authenticated. - Fixed (docs). A void that becomes a refund refunds what is left of the charge, also on
an unsettled charge already partly refunded or disputed, and answers
status: succeededwithamount_refunded. - Fixed (docs). Signed requests: the nonce is spent before the signature is checked, so
retry with a new
X-Nonce; a nonce store outage answers503 service_degraded. Rate limits are the same for every key, with no per-merchant setting.three_ds_session_usedalso answers the same key after 24 hours.customer.*disputeCountcounts alerts and RDR records too. TheframeAncestorsmount option is in the options tables, andPATCH /v1/payment_intents/{id}listsparameter_unknownand the 50-minor-unit floor. - Fixed. A blocklist IP written with upper-case letters (an IPv6 address such as
2001:DB8::1) never matched a charge, because the charge-time check compares lower case.POST /v1/reportnow stores IP values in lower case, andGET /v1/blocklistreturns them that way, existing entries included. Phone and BIN blocklist entries are stored as digits and are now checked on every charge; a list type that no charge check reads (country, email domain) is refused instead of being stored.
2026-10-23#
-
Changed. Wallet alert holds restored following the partner's decision on 2026-10-23:
skipWalletSales: trueholds every alert on a known Apple Pay / Google Pay sale, with or without ECI, including fraud, customer disputes and unknown categories (wallet-sale). Unknown wallets still wait for NMI V4 (wallet-check-pending). This supersedes the wallet evidence and fraud-only restriction recorded below; the option remains off by default. The 3-D Secure hold remains fraud-only with its CAVV checks. Visa RDR waits and the charged-back-sale guard are unchanged. See Webhooks. -
Fixed.
worthypayments.jsTypeScript definitions:/sdk/v1/worthypayments.d.tsand/sdk/worthypayments.d.tsnow include the types the bundle already exposes.mountExpressCheckout()reached the bundle without them, so a TypeScript project calling it failed to compile against the published file. Added:mountExpressCheckouton the client, andExpressCheckoutOptions,ExpressCheckoutHandle,ExpressCheckoutUpdate,ExpressPayment,ExpressBilling,ExpressShipping,ShippingMethod,ShippingAddress,ShippingSheet,ShippingUpdate,SheetLineItemandWalletsDrawn. The script itself is unchanged. See mountExpressCheckout. -
Changed. Payment Workflow cascade retries a declined charge on the next processor only for a generic decline:
200(declined by processor),201and05(do not honor). Insufficient funds (202,51), amount and frequency limits (61,65), a CVV failure (N7) and a decline with further instructions (260) are no longer retried on another processor: the same issuer answers them the same way seconds later, and each retry counts as a reattempt with the card networks. A route's own cascade codes can still only narrow this set. See Charges. -
Added.
frameAncestorsonmountPaymentElement: the origins of the pages that frame your checkout, nearest first. With it, a checkout framed two levels deep renders in a Firefox older than 148, which reports only the page directly above; Chrome, Edge, Safari and Firefox 148 or later report the whole chain without it. Each origin must be on the key's allowed domains. See Payment Element. -
Fixed (docs). A checkout inside an iframe shows Apple Pay and Google Pay only when every iframe above the mounting page carries
allow="payment"; Apple Pay needs Safari 17 or later there. The guide now says so. See Payment Element. -
Fixed (docs). The guides said a wallet's device token "cannot be saved" and that the gateway had to accept wallet tokens first. Saving an Apple Pay payment (or a Google Pay device token) for renewals is a current WorthyPayments limitation, not the gateway's: we do not store a wallet's token today. What the API does is unchanged. See Subscriptions paid by wallet.
-
Changed. Pre-dispute alert rules: a 3-D Secure or wallet hold now holds only a fraud alert (Ethoca fraud, or a CDRN fraud reason code). 3-D Secure and wallet cryptograms shift liability for fraud only, so a customer-dispute alert on a protected sale now follows the rule like any other alert, and its
auto_rulenames the rule that refunded it. A wallet sale is held only when its authentication is on record. See Webhooks. -
Changed. For a merchant that receives Visa RDR, a rule's refund of an Ethoca or CDRN alert on a Visa sale waits until 4 hours before the alert's deadline, in case Visa RDR refunds the cardholder first. While it waits,
auto_rule.reasonisrdr-wait. -
Added.
auto_rule.reasonvaluessale-already-charged-back(the sale already has a chargeback, so the alert is not refunded: the cardholder has the money back) andrdr-after-platform-refund(on an RDR alert whose sale the platform had already refunded). -
Added.
auto_rule.alert_category(fraud,non-fraud,unknown) andauto_rule.hold_waived(non-fraud-alert,unknown-alert-category) on a pre-dispute alert: how the alert was read when a rule's 3-D Secure or wallet hold matched, and why the hold stood aside. Both are""when no hold matched. See Webhooks. -
Added.
rdron the dispute object (GET /v1/disputes,GET /v1/disputes/{id}and the nesteddisputeof everydispute.*webhook):truefor a Visa Rapid Dispute Resolution record, which the issuer has already credited from your funds and which is not a chargeback. An acquirer's RDR case used to look like an accepted chargeback, withalert_source"". We never counted these as chargebacks; withrdryour own counts can leave them out too. See Webhooks. -
Changed. A 3-D Secure session authenticates one charge.
POST /v1/chargeswiththree_ds.session_idbinds the session to the firstIdempotency-Keythat presents it; the same key may present it again for 24 hours (the resubmit after a challenge, a retry), and a charge under any other key, or the same key later, is refused before anything is charged with400 invalid_request_error,code: three_ds_session_used,param: three_ds.session_id. It used to be accepted, carrying the first payment's authentication to a second one. An integration that resubmits with the same key, as these guides describe, is unaffected. See 3-D Secure. -
Added.
three_ds.authentication(authenticated,attempted,not_authenticated,unknown) andthree_ds.liability(shifted,not_shifted,unknown) on a charge read and the charge returned byPOST /v1/charges.liability: "unknown"is a charge with 3-D Secure whose verdict was never recorded, which an omittedliability_shiftcould not tell apart from "not shifted". See Charges. -
Fixed.
three_ds.authenticatedwastrueon any charge carrying an ECI, including an attempt (06, Mastercard01) and an ECI that means no authentication (07, Mastercard00). It is nowtrueonly for an authenticated ECI, andcard.three_d_secure.resultreadsattempt_acknowledgedfor an attempt instead ofauthenticated. -
Added.
three_ds.cavv_resultandthree_ds.cryptogram_validation(passed,failed,information_only,not_validated,unknown) on a charge read: the issuer's verification of the 3-D Secure cryptogram at authorization, as the gateway reported it (Visa, Amex and Discover; never Mastercard). Thetransaction.enrichedwebhook'sdetailcarries the same result asthreeDsCavvResult, andtransaction.updatednamesthree_ds_cavv_resultinchangedFieldswhen it lands. See Charges. -
Added.
three_ds.liability_reasonon a charge read and the charge returned byPOST /v1/charges: whyliabilityreads as it does (authenticated,attempt_acknowledged,provider_reported,issuer_check_denied,not_authenticated,not_granted,not_recorded). See Charges. -
Changed. The liability verdict now accounts for that verification. A charge whose cryptogram the issuer failed (
1,4,7,9,I), passed for information only (B), or could not check because invalid data was sent (0) readsthree_ds.liability: "not_shifted"and omitsliability_shift, even with an authenticated ECI: such a charge does not move a fraud chargeback to the issuer. The dashboard's "Liability" reads "Merchant" on it. A result the issuer did not validate (C,D,6,U) leaves the verdict as the ECI gave it.
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. A Firefox older than 148 reports only the frame directly above. 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.
