Accept payments
Elements API
This is the raw HTTP reference for the token-gated endpoints behind Elements and the embeddable Secure Payments checkout. In almost all cases you should use the vezmo-elements.js (or vezmo.js) SDK, which calls these for you — this page is for teams integrating over raw HTTP or debugging.
How these endpoints authenticate
They are not API-key endpoints. Each is scoped by a client token (:token in the path) — the short-lived, single-session clientToken you got back from creating a session server-side with your API key (see Secure Payments → create a session). They are public (no Authorization header) but every call is:
- token-scoped — one session only; the amount, currency and customer are fixed server-side at session create and cannot be changed from here;
- origin-gated — requests must come from one of the merchant's Trusted Origins;
- rate-limited, and (for saved cards) gated by an email OTP and a session-email ownership check.
All responses share the standard envelope { "success": boolean, "message": string, "data": ... }. The fields below describe data.
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.
Load the checkout
POST /secure-payments/:token/checkout
Returns everything the browser needs to render the payment field. It creates nothing: the payment is created when the buyer pays (…/pay, below), so a buyer who opens the page and leaves leaves no payment behind. No request body.
Query
redirectMethods=1(optional) — your page can send the buyer to Klarna, Affirm, Afterpay or Amazon Pay and read the result when they come back (vezmo-elements.jssends it). Without it those methods are left out ofenabledMethodsandelements.…/payalways takes them.currency=EUR(optional) — the buyer picked another currency fromlocalOffer.choices: the response carrieslocalOfferin that currency. See Local Currency.
Response (data)
localOffer— the buyer's own currency, priced, when they may pay in it (elsenull). See Local Currency.clientSecret—null; the payment is created by…/pay.elements— the options the payment field starts with (deferred mode,paymentMethodCreation: 'manual'). Treat them as opaque.enabledMethods— which methods this session offers (see Payment methods).publishableKey,connectedAccountId— opaque values the SDK uses to start the payment field in the right mode and account. Don't store or parse them.amount,currency— server-authoritative; fixed at session create.livemode—falsefor sandbox sessions.bankSupported— whether bank transfer (ACH) is offered for this session.canSaveCard— whether this session may save the card (the session was created withsaveCard: trueand a customer email).theme— the session's default checkout theme (light/dark/auto).title,merchantName,customerName,customerEmail,successUrl,cancelUrl,paymentId.
Pay
POST /secure-payments/:token/pay
Our server prices and confirms every payment. The browser turns what the buyer chose into a payment method (Stripe.js elements.submit(), then createPaymentMethod({ elements }) with the payment field created with paymentMethodCreation: "manual") and sends it here. We read the card or method — its brand and issuing country, a wallet, Klarna and the other methods with their own price — set the exact fee, create the payment for that card or method and confirm it. Nothing is confirmed in the browser, so no page can pay at a fee nobody priced. Takes the session's currency, or the buyer's own (localOffer).
Request body
paymentMethodId— the payment method (pm_…) made on the page just now from the payment field or split card fields: a card, a wallet, a bank or a pay-later method. Never one saved on a customer. Don't send a confirmation token from the web: card payments are confirmed only by our server, and a token from the payment field can't be used for that.returnUrl— your page's http(s) address, for a method that leaves the page (Klarna, Affirm, Afterpay, Amazon Pay, a bank's 3D Secure page): the buyer comes back there. Required with a pay-later payment method (400return_url_requiredwithout it). Make that payment method with the buyer's billing details, country included: its price depends on where the buyer is, and our server prices it from them before it confirms.integration(required) —elementsfor your own page.localOffer(optional) — the buyer chose their own currency:localOffer.tokenfrom…/checkout.409witherrors[0].codelocal_price_changedmeans the held rate expired: load the checkout again and let the buyer choose again. See Local Currency.
Response (data)
state—succeeded,processing(a bank debit on its way), orrequires_action.requires_actioncomes withclientSecret: the buyer has a step left (3D Secure, a bank or pay-later page). Run it with Stripe.jshandleNextAction({ clientSecret })— the secret does nothing else; it cannot confirm the payment — then call…/confirm, which confirms a card on our server and answers with the outcome. WithrequiresVerification: trueit is a typed bank account waiting on its micro-deposits: skip the step and call…/confirmfor the verify link.paymentId,transactionId(when succeeded),method(what the buyer paid with, for a receipt).
A decline is a 400 with the reason in our words, the reason's code in errors[0].code (e.g. card_declined) and the payment in errors[0].paymentIntentId. method_unavailable means that way to pay isn't offered for this payment. 409 when the session is already paid, 410 when the token has expired.
An intent for a method the browser pays itself
POST /secure-payments/:token/intent
Only for a method the browser finishes itself on our hosted checkout: body { "method": "ach" } (or instantAch, cashapp, upi, pix), plus localOffer when the buyer chose their currency. The payment is created for that method alone, at its price, so its clientSecret pays with that method only. A card (and Apple Pay / Google Pay), Klarna, Affirm, Afterpay and Amazon Pay are never paid this way: …/pay (their price depends on the card or on where the buyer is). vezmo-elements.js doesn't use this route.
If you integrated …/intent directly. Until 26 October 2026 this route handed out a payment your page then confirmed with Stripe.js. It no longer does: without a method (or with card) it answers 400 with errors[0].code use_pay_route. Move the confirm to …/pay: replace stripe.confirmPayment(…) / confirmCardPayment(…) with createPaymentMethod → POST …/pay → handleNextAction when requires_action → POST …/confirm. vezmo-elements.js does all of this for you, with the same confirm() results as before. A merchant-of-record business (whose fee is set when the payment settles) works the same way since 27 October 2026. Since then the same also goes for klarna, affirm, afterpayClearpay and amazonPay: with one of those …/intent answers use_pay_route; send the payment method to …/pay with a returnUrl, run the method's page with handleNextAction, and call …/confirm on your returnUrl page. A copy of vezmo-elements.js you saved or host yourself must be replaced with the one we serve (https://api.vezmo.com/api/v1/vezmo-elements.js): an older copy calls …/intent and stops working once the change is complete.
Finalize the payment
POST /secure-payments/:token/confirm
Call this after …/pay (and the buyer's step, when there was one), and on your returnUrl page after a method that left the page. A card back from 3D Secure is confirmed here, on our server, priced for that card; the answer is the outcome (a decline is a 400 in our words). If the bank asks for 3D Secure again, the answer is { "requiresAction": true, "clientSecret": "…" }: run handleNextAction with it and call …/confirm once more, each time the bank asks (we stop after three and ask the buyer to try again; nothing is charged). When the card still waits for our confirm (another step on this payment is running, or our processor didn't answer), the answer is { "pending": true, "status": "processing" }, and any error other than a 400 after 3D Secure means the same: processing, never failed. It also records the payer's IP and risk signals. If this call never reaches us (the buyer closed the tab right after 3D Secure), our server confirms that card itself within the hour, so fulfil on the webhook, which is authoritative either way.
Request body
paymentIntentId(required) — the payment: the part ofclientSecretbefore_secret_, orpayment_intenton your return page.
Response (data)
- The settled
payment(white-label) on success, or apending/processingshape for asynchronous methods (e.g. ACH), orrequiresVerification: truefor manual-ACH micro-deposits — then alsopaymentId,microdepositType(descriptor_code|amounts) andverifyUrl, the branded verify page (the same link the buyer is emailed). Invezmo-elements.js,confirm()resolves that case asstatus: 'requires_verification'withverification: { type, url }; your server can complete it later withPOST /merchant/payment/:id/verify-bank(details).
Saved cards (returning customers)
These power the saved-card flow. A card is only listed for a session whose customer email matches the saved card's network email, and only when a reusable copy exists at this merchant. Charging a saved card requires a fresh email code, verified for this checkout; it is used up by the payment and can't be reused for another checkout.
List saved cards
GET /secure-payments/:token/saved-cards
Returns the cards reusable for this session. data is an array of:
savedCardId— opaque id used by the OTP + pay calls below.brand,last4,expMonth,expYear— for display.maskedEmail— the (masked) email an OTP will be sent to.
An empty array means no reusable saved card — render your normal card field.
Send the OTP
POST /secure-payments/:token/saved-cards/otp/request
Body: { "savedCardId": "..." }. Emails a 6-digit code to the saved email. Returns { "maskedEmail": "ad••@example.com" }.
Verify the OTP
POST /secure-payments/:token/saved-cards/otp/verify
Body: { "savedCardId": "...", "code": "123456" }. Returns { "ok": true }. The code expires in 10 minutes and allows 5 attempts. A saved card gets at most 5 codes an hour and 15 a day (429 after that), and after 10 wrong codes in a day it takes none until the next day.
Charge the saved card
POST /secure-payments/:token/saved-cards/pay
Body: { "savedCardId": "..." }. Requires a verified OTP within the last 10 minutes. Charges the saved card with the customer present, so their bank may ask for 3D Secure. Returns the settled payment (or a processing shape), or { "requiresAction": true, "paymentIntentId": "...", "clientSecret": "..." }: open the bank's step with Stripe.js handleNextAction, then call POST /secure-payments/:token/confirm with the paymentIntentId. session.paySaved() in the SDK does both for you.
Errors
Errors use the same envelope with success: false and a white-label message (no processor names or ids). Common statuses: 401 (invalid/expired token), 403 (origin not allowed, or the saved card isn't for this session, or no verified OTP), 404 (not found), 429 (rate limited, or payment_in_progress: another step on this payment is running).
Webhooks
Payments made through these endpoints settle through the same pipeline as every other VezmoPay charge and emit the same events — payment.processing, payment.success, payment.failed, payment.refunded, the dispute.* events, and payment_method.saved when a customer saves a card. See Webhooks. Always treat the webhook as the source of truth for fulfilment.
For ACH / US bank payments (which settle over several days, not instantly), payment.processing fires as soon as the debit is initiated so you can record it as pending in real time, and payment.success is the later settlement signal to fulfil on. See Payments & the ACH lifecycle.