Changelog

View as Markdown

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

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

What is listed here#

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

TagMeaning
BreakingCould break a correctly-written integration, as defined in What counts as a breaking change.
ChangedBehaves differently, but an integration that follows these guides keeps working.
AddedA new field, value, event, parameter or callback property. Additive; ignore what you do not use.
FixedThe API now does what these guides already said. Fixed (docs) means the guide was wrong and now describes what the API does.

2026-11-02#

  • Changed. Webhook detail.settlementBatchId is omitted instead of "0" on refunded, enriched and capture events that carry stored transaction detail. GET /v1/charges/{id} also omits settlement.batch_id instead of "0"; when the batch was the only settlement value, the whole settlement object is absent. Real batch ids are unchanged. The guides already say empty fields are omitted.
  • Fixed. GET /v1/customers/{id}/three_ds returns an HTTP 500 or 503 API error (type: api_error, code: upstream_error) when enforcement state cannot be read, instead of reporting require_3ds: false. POST and DELETE on 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.rules accepts four more selectors for the saved-cards-across-stores (Link) steps of the Payment Element: .CodeInput (the 6-digit code field), .Action and .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 on night or any dark colorText they 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 as card_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 webhook detail's cardExpMonth / cardExpYear were 0 before. A card charge also keeps its card.bin, card.last4 and card.brand where 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's card.bin, card.last4, card.brand and expiry, which were empty: worthypayments.js now sends the sealed card's card.bin, card.last4 and expiry beside wp_token on the confirm. If you confirm a wp: token yourself, send them too. See Payment Element.
  • Added. Error code wp_token_card_mismatch (400, do not retry): a wp: charge whose sealed card does not start with the bin or end with the last4 sent beside it is refused, on the confirm and on POST /v1/charges. Both are optional; a charge that sends neither is not checked. On POST /v1/charges a wp: card's card.brand is now derived from card.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.email as 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 in billing_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 detail carries fromSavedPaymentMethod: true when the charge was made on a saved payment method (a pm_…): 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 when walletType names 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.wallet is { "type": "apple_pay" } with the card_last4 / card_brand of the buyer's own card, and the webhook's walletType is apple_pay. Before, it read as a card with no wallet. card.last4 and card.bin still belong to the token. Nothing changes in what you send. See Charges.
  • Changed. An Apple Pay payment on a recurring Payment Element or express element is saved for renewals. Its confirm now answers card_save: saved with a pm_… (before: skipped / card_not_saveable); charge each renewal on that pm_… with recurring: true, a merchant-initiated charge that refers to this first payment. The wallet credential carries a new recurring: true when the sheet was opened as a recurring payment request: mountWalletButton sets 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_error and cancellation_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 is requires_payment_method or canceled; before, only the declined confirm said why, so your server could not read back a decline the buyer saw in the Element. Both are null otherwise. 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 200 canceled with cancellation_reason: failed_attempts and its last_payment_error; a later confirm is 400 payment_intent_attempts_exceeded, and a confirm of an intent you canceled (or that expired) is 400 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.js resolves that fifth confirm as requires_payment_method with its last_payment_error, so the Payment Element shows the decline (it read the new canceled as processing before).
  • Added. POST /v1/payment_intents/{id}/cancel (payment_intents:create) cancels an open payment intent, with cancellation_reason requested_by_customer (default), duplicate, fraudulent or abandoned. A processing or succeeded intent is 400 payment_intent_unexpected_state.
  • Changed. POST /v1/setup_intents refuses 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/customers repeated with the idempotency_key of a customer that was deleted since answered the deleted customer as if it were live. It now answers 409 customer_deleted (param: idempotency_key).
  • Added. POST /v1/customers/search with { "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/ping answers settleable_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 status was always enabled. It is now disabled for an endpoint that receives no deliveries, with disabled_reason: delivery_failures (the platform switched it off after a sustained run of failed deliveries) or disabled_by_merchant; null while enabled. Roll-secret answers the real status too.
  • Changed. Creating a webhook endpoint or rolling its secret without an Idempotency-Key is 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. An Idempotency-Key you send answers for 24 hours.
  • Changed. A webhook endpoint url that names localhost or 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. A url that is not https was already refused; it now answers 400 url_invalid too.
  • Fixed. The 400 idempotency_error and 409 idempotency_in_progress envelopes now carry request_id, as every other error does.
  • Added. GET /v1/charges/{id} answers amount_refunded_pending: the part of amount_refunded whose refund the gateway has not confirmed yet (a refund request answered 409 idempotency_in_progress). A 201 refund is confirmed and never fails later. See Refunds.
  • Added. POST /v1/webhook-endpoints/{id} (webhooks:write) changes an endpoint's url and/or enabled_events (the list is replaced). The answer is the endpoint as GET /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 is 404 resource_missing. See Webhooks.
  • Added. Every event from GET /v1/events and GET /v1/events/{id} carries replay: true when it was written during a backfill or historical replay (it records the past, and no webhook was sent for it), false otherwise. 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 gains source (platform or gateway) and created (Unix seconds). Before, a portal refund counted in the charge's amount_refunded but was listed under no charge.
  • Fixed. GET /v1/events and GET /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/customers with that email creates a new customer with a new id, where it answered 409 already_exists, and GET /v1/customers?email= finds the new customer. POST /v1/customers/:id can likewise change a customer's email to it. An email that belongs to a customer that is not deleted is still a 409. The deleted customer stays deleted, with its id and history.
  • Fixed. Paying an invoice on its hosted_invoice_url page 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: its amount was above the invoice's amount_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 with latest_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 on GET /v1/invoices and sends invoice.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 coupon is 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_INVOICE subscription 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_INVOICE renewal 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_url page 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_url page 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; the hosted_invoice_url page always has the current one. See Invoices.
  • Added. The charge webhooks' data.object.detail carries the shipping address the charge was made with: shippingName, shippingPhone, shippingLine1, shippingCity, shippingState, shippingPostal and shippingCountry, each omitted when you sent no value. They ride every event that carries the charge's detail: charge.authorized, charge.succeeded (automatic or manual capture), charge.failed, transaction.enriched and a charge.refunded for a refund issued here, with their transaction.* twins. A sale or refund synced from your gateway carries none, and charge.canceled / charge.expired carry 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_intents and PATCH /v1/payment_intents/{id} take shipping_address (the flat shape POST /v1/charges takes), line_items (up to 100: name, amount in minor units, quantity 1–10000, sku, category) and billing_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; PATCH replaces each one sent. shipping_address is answered on create, update and retrieve to secret and restricted keys (null when none). Confirm takes billing.first_name / billing.last_name, which, like the intent's billing_details, are used for the charge's billing name and the customer record instead of splitting billing.name. Before, an intent's charge carried the intent's single description line as its basket, no shipping address, and a name split on its last space. See Payment Element.
  • Added. note on POST /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 a note field: the create response, GET /v1/refunds/{id} and GET /v1/refunds return the stored note, null when there is none. A note over 500 characters, or one that is not a string, is 400 parameter_invalid, param: "note". See Refunds.
  • Fixed. GET /v1/refunds/{id} and GET /v1/refunds return the refund's reason, as the Refunds guide showed; they read reason: null for every refund. It is the value the create takes (requested_by_customer, duplicate, fraudulent), and null for 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_items on POST /v1/charges: up to 100 { name, amount, quantity?, sku?, category? } items, amount the 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 as line_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/charges does not return them. An invalid item is 400 parameter_invalid naming it, e.g. param: "line_items[2].amount". See Charges.
  • Added. shipping on GET /v1/charges/{id}, GET /v1/charges and GET /v1/customers/{id}/transactions: the shipping address the charge was made with, as stored, { name, phone, line1, line2, city, state, postal_code, country }, with line2 folded into line1 (so line2 is null) and "[redacted]" for a customer you erased, as billing_details reads. null when there is none. Before, only the create response echoed it. The create response now carries shipping: null when you sent no shipping_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/, calls api.worthypayments.com/v1 and frames element.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 with WORTHYPAYMENTS_SDK_URL and WORTHYPAYMENTS_API_BASE in wp-config.php. The order screen's dashboard link opens app.worthypayments.com. Updates still come from app.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 from GET /v1/events hourly and by a daily reconciliation, so the secret key also needs disputes:read and events: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 the shipping_address and the buyer's first and last name as billing_details, and an open intent's update removes a part the order no longer has; a refund's typed reason is sent as its note, 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_name and phone, and a shipping_address when the order ships; every payment's metadata adds site_url, and subscription_id on 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 metadata key, 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 declined soft_decline_3ds for 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-subscriptions finishes 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 a 429 is waited out; every page prints the --from-id to resume from; one command runs at a time. revert-subscriptions reads what each subscription held before from the subscription itself: --log is 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 reported error instead of stopping the revert. --batch defaults 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-mapping drops 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. On GET /v1/pre_dispute_alerts, network_status therefore reads PENDING and then SENT on these alerts too, and a NEW alert can read SENT; FAILED if the answer could not be sent before the deadline. Each change moves updated_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 REFUNDED straight away: pre_dispute_alert.updated is sent with outcome: REFUNDED and refundStatus: SETTLED, and auto_rule.reason still says why (sale-already-refunded, sale-refunded-outside-platform). It used to stay NEW until 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.updated with outcome: 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.updated with outcome: 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 receive pre_dispute_alert.updated with outcome: MATCHED for 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. 200 is 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" beside skipWalletSales: true. A customer dispute or an alert that names no category is then decided by the rule, and auto_rule.hold_waived says 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_validation can be not_recorded: the charge carries 3-D Secure, but no issuer check of the cryptogram was recorded, so liability rests 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). liability and liability_reason do not change.
  • Changed. A charge that threads three_ds.session_id is refused with 400 invalid_request_error, code: three_ds_card_mismatch, param: three_ds.session_id when 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_ONLY payment (the buyer's own card, no cryptogram) made through the Payment Element, mountCheckout or hosted checkout is now recorded as Google Pay: the charge reads card.wallet: { "type": "google_pay" } and its webhooks carry walletType: "google_pay", where both used to be empty. It is still charged, scored and stepped up to 3-D Secure as a card, and card.last4 / card.bin are 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_type on POST /v1/charges and on a payment intent confirm: "google_pay" marks a card as a Google Pay PAN_ONLY credential. Any other value is 400 parameter_invalid, param: "card.wallet_type".
  • Fixed (docs). A wallet block without a cryptogram is refused; the guides said a Google Pay one was charged as a card. Send that card as card with card.wallet_type. The Payment Element guide no longer calls PAN_ONLY the usual case on iOS (our Google Pay button runs in Chromium browsers only), and says how to accept device tokens only: wallets: 'never' and mountWalletButton with allowedAuthMethods: ['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.js dropped a Google Pay card's expiry month and year. The wallet sends them as numbers and the SDK read them as text, so the wallet credential onToken received carried empty exp_month and exp_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.captured and transaction.succeeded for a charge your gateway processed directly (made outside WorthyPayments and synced from the gateway) omitted ip and instrument.bin even 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}) add reason_code, the provider's own code (Kount's alert type such as ETHOCA_DISPUTE, or the CDRN issuer reason code such as 13.1), and category, that code read as fraud, non-fraud or unknown. reason stays FRAUDULENT on every Kount-fed alert as before. The pre_dispute_alert.created / .updated webhooks carry the same two fields in data.object.alert (reasonCode, category), and the AUTO_REFUNDED update now also carries autoRule, the decision that refunded the alert, as GET states it. See Webhooks.
  • Fixed. Kount Ethoca alerts imported from a Kount export had no reason_code, so they read category: 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 their updated_at. The flat reasonCode of a Kount alert's MATCHED update said FRAUDULENT; it is now the provider's code, the same value as data.object.alert.reasonCode. An alert decision recorded before this date keeps the category it was judged on, so its auto_rule.alert_category can differ from the alert's category now.
  • Fixed. A pre-dispute alert whose CDRN code is one of Visa's three-level conditions (12.6.1 duplicate processing, 12.6.2 paid by other means) read category: unknown. It now reads the category of its dispute group, as 12.6 and 13.1 already did: non-fraud for 12.x, fraud for 10.x. GET /v1/pre_dispute_alerts answers 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 answered 403 account_not_activated, as if your account had no gateway key. It now answers 503 upstream_error. Nothing was sent to the gateway; retry with the same Idempotency-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, N7 as retried on another processor) and showed an insufficient-funds decline cascading. Only 200 and 201 / 05 cascade, 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 Pay PAN_ONLY card may cascade like any card. The decline table now lists 240, 460 and 300, and no 224. See Charges.
  • Fixed (docs). pre_dispute_alert.* webhooks do not carry the same object as GET /v1/pre_dispute_alerts: the polled alert adds auto_rule, the processor, the sale's gateway id and the received / resolved times. Every auto_rule.reason value is now listed with what it means. reason reads FRAUDULENT on 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 the authentication / liability reading GET /v1/charges/{id} adds. The 2026-10-23 entry named the CAVV key in snake case; the webhook key is threeDsCavvResult.
  • Fixed (docs). three_ds.authenticated is also true when no ECI can be read and the 3-D Secure provider reported a shift; Mastercard ECI 06 and 07 read as not_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: succeeded with amount_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 answers 503 service_degraded. Rate limits are the same for every key, with no per-merchant setting. three_ds_session_used also answers the same key after 24 hours. customer.* disputeCount counts alerts and RDR records too. The frameAncestors mount option is in the options tables, and PATCH /v1/payment_intents/{id} lists parameter_unknown and 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/report now stores IP values in lower case, and GET /v1/blocklist returns 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: true holds 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.js TypeScript definitions: /sdk/v1/worthypayments.d.ts and /sdk/worthypayments.d.ts now 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: mountExpressCheckout on the client, and ExpressCheckoutOptions, ExpressCheckoutHandle, ExpressCheckoutUpdate, ExpressPayment, ExpressBilling, ExpressShipping, ShippingMethod, ShippingAddress, ShippingSheet, ShippingUpdate, SheetLineItem and WalletsDrawn. 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), 201 and 05 (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. frameAncestors on mountPaymentElement: 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_rule names 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.reason is rdr-wait.

  • Added. auto_rule.reason values sale-already-charged-back (the sale already has a chargeback, so the alert is not refunded: the cardholder has the money back) and rdr-after-platform-refund (on an RDR alert whose sale the platform had already refunded).

  • Added. auto_rule.alert_category (fraud, non-fraud, unknown) and auto_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. rdr on the dispute object (GET /v1/disputes, GET /v1/disputes/{id} and the nested dispute of every dispute.* webhook): true for 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, with alert_source "". We never counted these as chargebacks; with rdr your own counts can leave them out too. See Webhooks.

  • Changed. A 3-D Secure session authenticates one charge. POST /v1/charges with three_ds.session_id binds the session to the first Idempotency-Key that 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 with 400 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) and three_ds.liability (shifted, not_shifted, unknown) on a charge read and the charge returned by POST /v1/charges. liability: "unknown" is a charge with 3-D Secure whose verdict was never recorded, which an omitted liability_shift could not tell apart from "not shifted". See Charges.

  • Fixed. three_ds.authenticated was true on any charge carrying an ECI, including an attempt (06, Mastercard 01) and an ECI that means no authentication (07, Mastercard 00). It is now true only for an authenticated ECI, and card.three_d_secure.result reads attempt_acknowledged for an attempt instead of authenticated.

  • Added. three_ds.cavv_result and three_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). The transaction.enriched webhook's detail carries the same result as threeDsCavvResult, and transaction.updated names three_ds_cavv_result in changedFields when it lands. See Charges.

  • Added. three_ds.liability_reason on a charge read and the charge returned by POST /v1/charges: why liability reads 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) reads three_ds.liability: "not_shifted" and omits liability_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: the url of a checkout link and a payment link, a charge's receipt_url, the hosted pay link on invoices and subscriptions, and the links in the emails we send. Links issued earlier on https://app.paygeyt.com keep working; both hosts serve the same pages. If your code checks the host of these URLs, or a Content-Security-Policy on your site lists it, allow app.worthypayments.com as well.
  • Changed. The inbound webhook URLs the dashboard shows you to paste into a provider portal (PayArc, Kount) now use https://webhooks.worthypayments.com. A URL you registered earlier on https://webhooks.paygeyt.com keeps working; both hosts deliver to the same place, so nothing needs to change in a portal that already has one.
  • Changed. The Node SDK (@worthypayments/node) defaults baseUrl to https://api.worthypayments.com. https://api.paygeyt.com serves the same API with the same keys; pass { baseUrl: "https://api.paygeyt.com" } to stay on it.
  • Fixed (docs). Charges, Integration hard rules and three other guides still warned that a card-testing burst could auto-block your server's IP on a server-side charge, so renewals needed an allowlist. That stopped being true on 2026-10-19: a charge sent with a secret key records your server's address apart from the buyer's and never scores or blocklists it. Nothing to allowlist; send a device_session to score a server-built charge on the buyer's IP.

2026-10-21#

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

2026-10-20#

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

2026-10-19#

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

2026-10-18#

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

2026-10-17#

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

2026-10-16#

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

2026-10-15#

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

2026-10-14#

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

2026-10-13#

  • Changed. POST /v1/check can deny with BLOCKED_BYPASS_LINK when a device or IP links to a blocked customer. On charges, that link check obeys your Cascade switches. See Linked-account blocks.

2026-10-11#

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

2026-10-10#

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

2026-10-09#

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

2026-10-08#

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

2026-10-07#

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

2026-10-06#

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

2026-10-05#

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

2026-10-02#

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

2026-01-01#

Initial public contract for the Worthy Payments API:

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

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

The interactive reference is at /developers/api-reference.