Skip to main content

Vora.js changelog

What changed in each release of the Von Payments browser SDK (vora.js).

  • https://js.vonpay.com/v1/vora.js updates 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.md and 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() returned frame_payment_declined with retryable: false. Nothing changes for a payment with no earlier decline.
  • The deprecated card.tokenize() now reports a hold (a session created with captureMethod: "manual") the way collection.submit() does: chargeStatus: "succeeded", charged: false and paymentIntentId, 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 its metadata.session_id is this session.

1.38.0 — 2026-10-05​

Added​

  • The embedded card form (embed) now places a hold when the session was created with captureMethod: "manual", instead of taking the payment. The buyer's money is authorised, not taken; capture or void it later with POST /v1/payment_intents/{id}/capture or /void. Any other captureMethod takes the payment, as before.
  • collection.submit() reports a hold as chargeStatus: "succeeded" with charged: false and paymentIntentId (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 its metadata.session_id is this session. If the result of the hold did not arrive in time it reports chargeStatus: "pending" with charged: false: do not charge again; read the session with your secret key (GET /v1/sessions/{id}): paymentStatus: "held" and its paymentIntentId name the hold. A hold is never reported as charged: 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, setupForFutureUse is false: the card cannot be charged again. If the checkout reports that the payment was captured rather than held, submit() returns charged: 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, with frame_session_not_ready and retryable: false: it would be a second payment on the buyer's card. Start a new session for another payment. Sessions created with integrationMode: "elements" are not affected.

Changed​

  • A declined payment's retryable is now always set. It is true only when the checkout re-opened the session for another card (the card form has already been reset — let the buyer submit again). Otherwise it is false: the checkout session is finished, so create a new session for the buyer to pay again. On false the wallet buttons in the same collection are taken down and a resubmit is refused with nothing sent. Previously a decline with no retry instruction left retryable unset 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() returns frame_payment_declined with retryable: true; the buyer can enter another card without a new session. Otherwise the decline stays retryable: 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 with frame_field_validation_failed and 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 as chargeStatus: "pending", and one that did not complete as frame_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" and brand: "unknown".
  • When the card fields save a card instead of charging it and the issuer refuses the card, submit() now returns frame_payment_declined with retryable: false. It previously returned frame_tokenization_failed saying the response was malformed. Any other unexpected status on a card save returns frame_tokenization_failed naming that status.

1.37.0 — 2026-10-02​

Added​

  • OptionsOptionalElementType, exported as a type: the element types whose options argument collection.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 change event reports frame_wallet_unavailable with 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. If collection.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.
  • setupForFutureUse on a successful tokenize() can now be null. It means you asked to save the card for later and the request was refused, because the session did not identify the buyer (buyerId, buyerEmail or buyer at 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() keeps setupForFutureUse as true / false: a refused save arrives as false (present, never omitted, never true), 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 a TypeError. The default placeholder and style apply, exactly as for {}. address still needs { mode: "billing" }; leaving it out now fails with frame_field_validation_failed naming mode.
  • 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 another fetchUpdates(). Opt-in; nothing changes until you call it.
  • Apple Pay / Google Pay declines now carry retryable and attemptsRemaining on frame_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 as frame_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 onConfirm runs.
  • A collection takes one payment at a time: a wallet payment while a card submit() is in progress (or the reverse) is refused with frame_session_not_ready before anything is sent.
  • Express checkout shipping option id values must be printable ASCII (and at most 64 characters). Other values are refused at mount with frame_field_validation_failed instead 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.js does not exist (older pinned versions still serve theirs), and /v1/sandbox-iframe.html and /v1/sandbox-iframe.js are 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.
    • shippingOptions with onShippingAddressChange / 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_mismatch and frame_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. onConfirm works 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 pending instead of a decline. Dedupe on paymentIntentId on 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_declined now carries retryable and attemptsRemaining, 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, WalletResource and ElementCollectResult are exported as types.

Fixed​

  • If the card form could not be rebuilt after a reset, the decline is reported as retryable: false and 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.
  • ChallengeAction is 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 back requires_action, pass the challenge URL to this method to send the buyer to their bank. After the bank, the buyer lands on the returnUrl you gave when creating the payment intent (server SDK 2.5.0 or later). An untrusted or unusable URL throws frame_3ds_required and 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 reaches submit() as chargeStatus: "succeeded" with charged: false. Before, charged was 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.
  • pending and requires_action results now carry charged: false.