Accept payments
Elements
Build your own checkout with composable fields. Elements lets you build your own checkout UI and drop in a secure VezmoPay payment field. The customer's card details are captured inside that field and tokenized directly to the processor — they never touch your servers, so you stay PCI SAQ‑A (the lightest level). Use this when you want full control of the layout and styling; use Secure Payments if you'd rather embed a ready-made form.
How it works
- On your server, create a payment session with your VezmoPay API key and pass the returned
clientTokento the page. - On the page, load
vezmo-elements.js, mount the payment field into your own element, and confirm.
Vezmo's server confirms the payment. When you call confirm(), the SDK sends what the buyer chose to Vezmo (POST …/pay): we read the card or method, price it exactly (an American Express, a card from another country, a method with its own price) and confirm the payment. Nothing is confirmed in the browser. If the bank wants 3‑D Secure, or the buyer finishes on Klarna or another method's page, that step runs in your page and Vezmo then completes the payment. You call the same confirm() and get the same results as before; see the Elements API if you integrate over raw HTTP. Always load the script from our URL: a copy you saved or host yourself must be refreshed, or it stops working.
1. Create a session (server) → clientToken
Authenticate (two-step exchange — see Authentication), then create a secure-payment session. The amount is set here, server-side (it can never be changed from the browser). Collect your customer's name and email on your own form and pass them in client.
curl -X POST https://api.vezmo.com/api/v1/merchant/secure-payments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Order #1234",
"amount": 49.00,
"currency": "USD",
"client": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"country": "US",
"postalCode": "94103"
},
"successUrl": "https://your-store.com/return"
}'
# → { "data": { "securePayment": { "clientToken": "eyJ…", … } } }Customer fields. Send the customer in a client object. When you do, name, email, country (ISO code, e.g. US) and postalCode are required; a request missing one is rejected with 400. line1, city, state and phone are optional, but a complete address improves fraud screening.
For a card payment we forward only the customer's name and address to the card network for verification (AVS and fraud checks), never their email or phone. Email is forwarded only for bank (ACH), saved-card and wallet payments; bank payments need it for the debit mandate. Every field you send is kept on your payment record.
2. Mount the payment field (browser)
Add a container, load the SDK, then mount. The field is fully themeable via the appearance option.
<div id="vezmo-payment"></div>
<button id="pay">Pay</button>
<script src="https://api.vezmo.com/api/v1/vezmo-elements.js"></script>
<script>
const vezmo = VezmoElements();
vezmo.checkout(CLIENT_TOKEN, {
layout: "tabs", // "tabs" | "accordion"
appearance: {
variables: { colorPrimary: "#503af2", borderRadius: "12px" }
}
}).then(function (session) {
session.mount("#vezmo-payment");
document.getElementById("pay").addEventListener("click", function () {
session.confirm({ returnUrl: "https://your-store.com/return" })
.then(function (r) {
if (r.error) showError(r.error.message);
else if (r.status === "succeeded") showSuccess();
else if (r.status === "processing") showPending(); // e.g. ACH
});
});
});
</script>Payment methods
The payment field automatically offers the methods enabled for the session — cards, wallets (Apple Pay / Google Pay), and bank transfer (ACH) when the amount is in USD and the account supports it (session.bankSupported). You don't need to configure anything — bank transfer simply appears as an option inside the field when it's eligible, and an ACH payment resolves with status: "processing" while it settles (usually a few business days), then emits payment.success via webhook. Cash App Pay, Klarna, Affirm, Afterpay and Amazon Pay appear the same way when you turn them on and the payment qualifies. UPI (for buyers paying in Indian rupees) and Pix (Brazilian reais) are on the hosted checkout and the embed, not in Elements yet: such a payment through Elements is by card. Instant ACH has a field of its own: see Instant ACH below.
Instant ACH
With Instant ACH the buyer pays from their US bank account and the payment is confirmed at once: status: "succeeded", not processing. Vezmo allows it for your business first; then you switch it on under Payment methods. US dollars only, from $0.50 to $7,500 per payment. session.instantAchSupported says whether this payment offers it.
It has its own field. Mount it in its own container and show it as its own choice beside the payment field. When the buyer chooses it, pay with confirmInstantAch() instead of confirm().
<div id="vezmo-payment"></div>
<div id="vezmo-instant-ach"></div>
<button id="pay">Pay</button>
<script>
VezmoElements().checkout(clientToken).then(function (session) {
session.mount("#vezmo-payment");
var payByBank = false; // your own "Card / Instant ACH" choice
if (session.instantAchSupported) {
session.mountInstantAch("#vezmo-instant-ach", {
email: customerEmail, // optional: fills the bank sign-in
onChange: function (s) {
// ready: the buyer linked their bank. unavailable: not offered for this payment.
if (s.unavailable) hideInstantAchChoice();
if (payByBank) document.querySelector("#pay").disabled = !s.ready;
},
});
}
document.querySelector("#pay").addEventListener("click", function () {
var paying = payByBank
? session.confirmInstantAch({ returnUrl: "https://your-store/return" })
: session.confirm({ returnUrl: "https://your-store/return" });
paying.then(function (r) {
if (r.status === "succeeded") showSuccess();
else showError(r.error.message);
});
});
});
</script>onChange reports { ready, unavailable }. Enable your Pay button for Instant ACH only when ready is true. When unavailable is true, the bank option isn't offered for this payment (for example, over the limit), so hide the choice. Instant ACH has its own price per payment, the same as a card unless we agreed another with you. If the customer picks a card inside their Link wallet instead of their bank, a merchant-of-record business (whose fee is set when the payment settles) pays the card price for it, international surcharge included. For any other business the fee is set before the customer chooses, so it stays the Instant ACH price. Disputes come in as debit_not_authorized; see Disputes.
Klarna, Affirm, Afterpay and Amazon Pay
The buyer finishes these on the method's own page, so confirm() needs a returnUrl (without one it resolves failed and says so, before anything is created). When the buyer picks one, confirm() sends them to the method; the method sends them back to your returnUrl with the result on the URL. On that page, open the same session and call handleReturn(): it reads the result, records it with Vezmo and resolves like confirm() (or null when the page load is not a return).
<script src="https://api.vezmo.com/api/v1/vezmo-elements.js"></script>
<script>
// The same clientToken the checkout used: put your order id in the returnUrl
// and look the token up on your server (it is valid for 30 minutes by default).
VezmoElements().checkout(clientToken)
.then(function (session) { return session.handleReturn(); })
.then(function (r) {
if (!r) return; // not a return
if (r.status === "succeeded") showSuccess();
else if (r.status === "processing") showPending();
else showError(r.error.message); // not completed: nothing was charged
});
</script>Each of these has its own price per payment. Fulfil on the payment.success webhook as always — it arrives whether or not the buyer reaches your returnUrl.
Collect a billing address (optional)
Add an Address Element in its own container to collect the customer's billing address. With the all-in-one field it's submitted automatically on confirm(); with split card fields (below) it's attached as the card's billing details.
<div id="vezmo-address"></div>
<div id="vezmo-payment"></div>
// after checkout(...).then(function (session) { ... }):
session.mountAddress("#vezmo-address", { mode: "billing" });
session.mount("#vezmo-payment");
// confirm() now includes the address automaticallySplit card fields (ultra-custom layout)
For full control of the layout, mount Card Number, Expiry and CVC into three of your own elements. This mode is card-only (no wallets or bank — use the all-in-one field for those) and is mutually exclusive with mount(). 3‑D Secure is still handled automatically.
<div id="card-number"></div>
<div id="card-expiry"></div>
<div id="card-cvc"></div>
<div id="vezmo-address"></div> <!-- optional billing address -->
<button id="pay">Pay</button>
<script src="https://api.vezmo.com/api/v1/vezmo-elements.js"></script>
<script>
VezmoElements().checkout(CLIENT_TOKEN).then(function (session) {
session.mountCardFields({
number: "#card-number",
expiry: "#card-expiry",
cvc: "#card-cvc"
});
session.mountAddress("#vezmo-address", { mode: "billing" }); // optional
document.getElementById("pay").addEventListener("click", function () {
// billing details come from the Address Element (and/or billingDetails below)
session.confirm({
returnUrl: "https://your-store.com/return",
billingDetails: { email: "ada@example.com" }
}).then(function (r) {
if (r.error) showError(r.error.message);
else if (r.status === "succeeded") showSuccess();
});
});
});
</script>3-D Secure
confirm() handles 3‑D Secure automatically (frictionless or a challenge): the challenge opens in your page, then Vezmo's server confirms the card. Some challenges redirect the customer, so always pass a returnUrl (and handle it with handleReturn(), above); cards usually complete inline. Regulatory SCA (EU and others) always applies regardless of your account settings.
Result & webhooks
confirm() resolves with { status, error? } where status is succeeded, processing (e.g. ACH settling), or failed. The payment is settled by Vezmo server-side and emits the same webhooks as every other VezmoPay charge — payment.success, payment.refunded, and the dispute events. See Webhooks. Never rely on the browser result alone for fulfilment — confirm via the webhook.
Theming (appearance)
Pass an appearance object to match your brand: theme (light/night), variables (colorPrimary, borderRadius, fontFamily, and more), and fine-grained rules. The session default follows the theme you set when creating it.
Saved cards (returning customers)
A returning customer can pay with a card they saved on a previous order — no re-typing. Cards are saved to the VezmoPay network, recognized by the customer's email, and every reuse is gated by a 6-digit code emailed to the saved address (so a leaked email can never charge a saved card).
1. Let a customer save their card
When you create the session, set saveCard: true only after the customer ticks a “save my card” box (and pass their email in client). A successful card payment then saves the card for reuse. The checkout payload echoes session.canSaveCard.
Saving a card always triggers 3‑D Secure — even if 3DS is otherwise off for your account. A card kept on file is established with strong customer authentication. Paying with it again later (session.paySaved()) happens with the customer present, so their bank may still ask for 3D Secure; the SDK opens that step.
// server, at session create:
{ "title": "Order #1234", "amount": 49.00, "currency": "USD",
"client": { "name": "Ada Lovelace", "email": "ada@example.com",
"country": "US", "postalCode": "94103" },
"saveCard": true }2. Offer saved cards on your next checkout
For a new session with the same customer email, list their reusable cards, let them pick one, verify the emailed code, and charge — all without mounting a card field.
VezmoElements().checkout(CLIENT_TOKEN).then(async function (session) {
const cards = await session.savedMethods();
// [{ savedCardId, brand, last4, expMonth, expYear, maskedEmail }]
if (cards.length) {
const card = cards[0];
// send the code to the saved email, then verify what the customer types
await session.requestSavedOtp(card.savedCardId); // → { maskedEmail }
await session.verifySavedOtp(card.savedCardId, code);
// opens 3D Secure here if the customer's bank asks for it
const r = await session.paySaved(card.savedCardId); // → { status }
if (r.status === "succeeded") showSuccess();
else if (r.status === "failed") showError(r.error.message);
} else {
session.mount("#vezmo-payment"); // no saved card → normal flow
}
});Saved cards are per-merchant: a customer sees a saved card at a given store only after they've paid that store once. The list is masked (brand + last 4 only) and contains no processor identifiers.
Test cards
4242 4242 4242 4242- success
4000 0000 0000 3220- 3‑D Secure required
4000 0000 0000 0002- declined
4000 0000 0000 0259- disputed (fraudulent)
Going live
- Add your production domain to your Trusted origins (VezmoPay → Developers).
- Your account must be approved to collect payments.
- Use your live API key; the SDK loads the correct (live) keys automatically.