Vora.js changelog
What changed in each release of the Von Payments browser SDK (vora.js).
https://js.vonpay.com/v1/vora.jsupdates automatically. New releases roll out in stages, so this page is where to see what changed.- A pinned URL (
https://js.vonpay.com/v1.37.0/vora.js) never changes. Pin a version if you need to control when you move. - This file is served at
https://js.vonpay.com/v1/CHANGELOG.mdand on docs.vonpay.com.
The format follows Keep a Changelog. Releases before 1.30.0 are not listed here.
1.38.1 — 2026-10-05
1.38.1 is the first release of the 1.38.0 changes below. 1.38.0 was never rolled out on
https://js.vonpay.com/v1/vora.js (it was only available at its pinned URL), so 1.38.1 includes
everything listed under 1.38.0 plus these two fixes: when the auto-updating URL moves from 1.37.0
to 1.38.1, you get both. If you pinned v1.38.0, move to v1.38.1.
Fixed
- When the bank declines a card on its 3-D Secure page and the checkout re-opens the session for
another card, the buyer's next card is now charged. Previously the first submit after the buyer
came back was answered with the earlier decline, the new card was never sent to the bank, and
collection.submit()returnedframe_payment_declinedwithretryable: false. Nothing changes for a payment with no earlier decline. - The deprecated
card.tokenize()now reports a hold (a session created withcaptureMethod: "manual") the waycollection.submit()does:chargeStatus: "succeeded",charged: falseandpaymentIntentId, with no token. Previously a card saved with a hold could come back as a plain saved card. Capture or void the hold with your secret key, and before you do, read the payment (GET /v1/payment_intents/{id}) and check itsmetadata.session_idis this session.
1.38.0 — 2026-10-05
Added
- The embedded card form (
embed) now places a hold when the session was created withcaptureMethod: "manual", instead of taking the payment. The buyer's money is authorised, not taken; capture or void it later withPOST /v1/payment_intents/{id}/captureor/void. Any othercaptureMethodtakes the payment, as before. collection.submit()reports a hold aschargeStatus: "succeeded"withcharged: falseandpaymentIntentId(the hold to capture). That id comes through the shopper's browser: before you capture, void or fulfil, read the payment with your secret key (GET /v1/payment_intents/{id}) and check itsmetadata.session_idis this session. If the result of the hold did not arrive in time it reportschargeStatus: "pending"withcharged: false: do not charge again; read the session with your secret key (GET /v1/sessions/{id}):paymentStatus: "held"and itspaymentIntentIdname the hold. A hold is never reported ascharged: true.- On a hold,
collection.submit()never returns a card token, even when the buyer saved their card: the card is saved on our side, and you list it with your secret key (GET /v1/payment_methods?buyer_id=…). (A token's normal next step is a charge, and charging it on top of a hold would bill the buyer twice.) If the buyer asked to save the card and it was not saved,setupForFutureUseisfalse: the card cannot be charged again. If the checkout reports that the payment was captured rather than held,submit()returnscharged: true. - One payment per embedded checkout. After the embedded card form places a payment (charged,
held, or pending), a second
collection.submit()on the same checkout is refused before anything is sent, withframe_session_not_readyandretryable: false: it would be a second payment on the buyer's card. Start a new session for another payment. Sessions created withintegrationMode: "elements"are not affected.
Changed
- A declined payment's
retryableis now always set. It istrueonly when the checkout re-opened the session for another card (the card form has already been reset — let the buyer submit again). Otherwise it isfalse: the checkout session is finished, so create a new session for the buyer to pay again. Onfalsethe wallet buttons in the same collection are taken down and a resubmit is refused with nothing sent. Previously a decline with no retry instruction leftretryableunset and the wallet button up, and a lost response that turned out not to be charged said "ask the buyer to try again" on a session that could not take it. - When saving a card is declined and the checkout allows another attempt on the same session,
the card form is reset and
submit()returnsframe_payment_declinedwithretryable: true; the buyer can enter another card without a new session. Otherwise the decline staysretryable: false.
Fixed
- On the embedded card form,
collection.submit()no longer waits forever when the form refuses the card (for example, an invalid card number). It now rejects at once withframe_field_validation_failedand nothing is sent. If the form goes silent for any other reason,submit()stops waiting after the 3-D Secure challenge timeout (5 minutes at least) and checks the payment with the server: a payment that went through is reported as a success, a hold as a hold, one still in progress aschargeStatus: "pending", and one that did not complete asframe_3ds_challenge_timeout(nothing was charged; start a new session). - The embedded card form now reports the card's real last 4 digits and brand. Previously a card
paid without saving could come back as
last4: "0000"andbrand: "unknown". - When the card fields save a card instead of charging it and the issuer refuses the card,
submit()now returnsframe_payment_declinedwithretryable: false. It previously returnedframe_tokenization_failedsaying the response was malformed. Any other unexpected status on a card save returnsframe_tokenization_failednaming that status.
1.37.0 — 2026-10-02
Added
OptionsOptionalElementType, exported as a type: the element types whose options argumentcollection.create()lets you leave out.
Changed
- No Apple Pay / Google Pay buttons for CLF, IQD, ISK, LYD, MGA, UYI and UYW. For these
currencies the standards disagree on how many decimal places an amount has, so a wallet sheet
could show the buyer a total 10x or 100x off what is charged. The express checkout element
draws no buttons and its
changeevent reportsframe_wallet_unavailablewith a message naming the currency — the same code as "no wallet on this device", so show the card form. Card payments in these currencies are unchanged. Ifcollection.fetchUpdates()moves the total to one of these currencies, the buttons are taken down (closing an open sheet) with the same reason, and a wallet payment that still arrives is refused with nothing sent. setupForFutureUseon a successfultokenize()can now benull. It means you asked to save the card for later and the request was refused, because the session did not identify the buyer (buyerId,buyerEmailorbuyerat session create). The payment stands, but the card is saved for this payment only and cannot be charged again. Omitted still means "not asked for". A card charged when the buyer submits reports the same refusal the same way.collection.submit()keepssetupForFutureUseastrue/false: a refused save arrives asfalse(present, never omitted, nevertrue), meaning the card cannot be charged again. To save cards for later, identify the buyer on the session.
Fixed
- Apple Pay / Google Pay sheets could show amounts in BHD, JOD, KWD, OMR and TND 10x too large (1.234 KWD appeared as 12.34). They now use three decimal places everywhere the sheet shows money: the total, shipping-option prices and the total after a shipping change. A trailing zero is dropped (1.230 shows as 1.23); the amount is never rounded. If a wallet does not accept a three-decimal amount, its sheet does not open and nothing is charged. What is charged was always correct — only the displayed amount was wrong.
collection.create(type)with no options argument no longer crashes with aTypeError. The default placeholder and style apply, exactly as for{}.addressstill needs{ mode: "billing" }; leaving it out now fails withframe_field_validation_failednamingmode.- A card saved on the session reported the save you asked for rather than the one that was stored, so a card saved for one payment only could appear reusable. It now reports what was stored.
1.36.0 — 2026-09-28
Added
collection.fetchUpdates(): after you change the session's amount on your server (a coupon, a shipping choice), call it and wait for it before the buyer pays. It resolves with the new{ amount, currency }. The card form and the express checkout button then pay exactly that total, and a payment is refused — nothing charged,frame_wallet_total_mismatch— if the total changes again without anotherfetchUpdates(). Opt-in; nothing changes until you call it.- Apple Pay / Google Pay declines now carry
retryableandattemptsRemainingonframe_payment_declined, the same as card declines. Card and wallet attempts share one count. Takes effect once the checkout enables retries for your account. - New error
frame_sandbox_account_required: a test key was used on a live account, or on a sandbox account with no payment provider. Retrying cannot fix it — use a sandbox account's test keys. Previously this surfaced asframe_session_not_ready.
Changed
- An express checkout payment is refused, with nothing charged, if the total changes while the
Apple Pay / Google Pay sheet is open or while your
onConfirmruns. - A collection takes one payment at a time: a wallet payment while a card
submit()is in progress (or the reverse) is refused withframe_session_not_readybefore anything is sent. - Express checkout shipping option
idvalues must be printable ASCII (and at most 64 characters). Other values are refused at mount withframe_field_validation_failedinstead of failing after the buyer approves.
Removed
- The built-in test simulator, which the checkout retired on 2026-09-24.
/v1.36.0/adapters/sandbox.jsdoes not exist (older pinned versions still serve theirs), and/v1/sandbox-iframe.htmland/v1/sandbox-iframe.jsare no longer served. Nothing in 1.36.0 loads them. Test payments use a sandbox account connected to a payment provider, with that provider's test cards.
No change to how card fields are styled.
1.35.0 — 2026-09-24
Added
- Express checkout (
elements.create("express-checkout", …)) can now collect buyer details and handle shipping inside the Apple Pay / Google Pay sheet. All opt-in:collect: the buyer's email, name, phone, billing and/or shipping address.shippingOptionswithonShippingAddressChange/onShippingOptionChange: shipping choices and an updated total in the sheet.onConfirm: runs after the buyer approves and before any money moves. Answer{ ok: false }(or throw, or take longer than 15 seconds) and nothing is charged.
- New error codes
frame_wallet_total_mismatchandframe_wallet_confirm_failed(nothing charged in either case). - Contact fields and shipping options need wallet buttons the SDK draws itself (custom
elements-mode sessions); elsewhere they are refused at mount with
frame_unsupported_element.onConfirmworks everywhere.
Fixed
- Card payments routed through Stripe that need a 3-D Secure check now reach the buyer's bank. The SDK had refused Stripe's challenge page, so the payment expired unpaid.
- A double-tap or a re-tap on a wallet button is reported once, and a re-tap during 3-D Secure
reports
pendinginstead of a decline. Dedupe onpaymentIntentIdon your server as well. - A slow wallet charge could close the sheet as failed and then succeed. The sheet now closes without claiming an outcome, and the SDK checks the result with the checkout.
1.34.0 — 2026-09-23
Added
- A declined card can be retried on the same form.
frame_payment_declinednow carriesretryableandattemptsRemaining, and when a retry is allowed the card form resets itself for another card. Takes effect once the checkout enables retries for your account; until then a decline behaves as before. SessionsResource,FieldsResource,ElementsResource,WalletResourceandElementCollectResultare exported as types.
Fixed
- If the card form could not be rebuilt after a reset, the decline is reported as
retryable: falseand asks the buyer to reload.
1.33.1 — 2026-09-16
Fixed
- The error for an unusable 3-D Secure challenge URL now lists every reason the URL can be refused, instead of naming one cause it had not checked.
ChallengeActionis exported as a type.
1.33.0 — 2026-09-16
Added
- Browser telemetry records when
redirectToChallenge()sends a buyer to their bank, including on live keys, so a 3-D Secure drop-off can be diagnosed.
1.32.0 — 2026-09-16
Added
vora.redirectToChallenge(): when your server creates a payment intent and it comes backrequires_action, pass the challenge URL to this method to send the buyer to their bank. After the bank, the buyer lands on thereturnUrlyou gave when creating the payment intent (server SDK 2.5.0 or later). An untrusted or unusable URL throwsframe_3ds_requiredand navigates nobody.
1.31.0 — 2026-09-15
Changed
- The card form appears sooner: the SDK opens the connection to the card-field host as soon as it is known, before loading the rest of the form. No change to what is sent or charged.
1.30.0 — 2026-09-12
Fixed
- A charge that is authorized and held (
captureMethod: "manual") now reachessubmit()aschargeStatus: "succeeded"withcharged: false. Before,chargedwas missing, which could read as paid. Do not fulfil until your server captures. - A held Apple Pay / Google Pay payment is reported as a success with a
paymentIntentId(use it to capture or void). Before, it was reported as a failure. pendingandrequires_actionresults now carrycharged: false.