Payments
Payments — retrieve, list & the ACH lifecycle
After you create a payment (via the embed / Checkout JS, a payment link, an invoice, or the API), use these endpoints to confirm its current state before fulfilling. This is especially important for ACH / US bank payments, which settle over several days rather than instantly — so you should treat them as processing until they reach a terminal state.
Authentication & permissions
These are public merchant-API endpoints. Exchange your key + secret for a short-lived access token via POST /merchant/api-auth/login (headers x-api-key / x-api-secret), then send it as Authorization: Bearer <token>. Both endpoints require the payment.read scope, which is part of the default payment permission group (grant it under Settings → Developer Settings if your key doesn't have it).
Retrieve a payment — GET /v1/merchant/payment/:id
:id is the payment's id — the same value returned when the payment was created and, for the embed, the data.paymentId you receive. (Note the path is singular /merchant/payment/…, not payments or secure-payments.)
curl https://api.vezmo.com/api/v1/merchant/payment/PAYMENT_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"This endpoint resolves both settled payments and ACH payments that are still processing (an in-flight bank payment is held separately while it settles, but this endpoint still returns it — it does not 404).
Response — the state field is the source of truth
Branch your fulfilment logic on the normalized state field:
{
"success": true,
"message": "Payment retrieved",
"data": {
"id": "cmqz9utka00km9tr9qblb8w77",
"transactionId": "VZMTXN...", // your white-label reference
"amount": 250.00,
"currency": "USD",
"method": "ach", // "card" | "ach" | "wallet"
"state": "processing", // <-- branch on THIS (see table below)
"status": "PROCESSING", // raw status (kept for compatibility)
"pending": true, // present (true) only for in-flight ACH
"customer": { // your own record, from the checkout
"id": "cml...", // null for guest checkouts
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+15551234567" // null if not collected
},
"billingAddress": { // null when no address was collected
"line1": "123 Market St",
"line2": "Suite 400",
"city": "San Francisco",
"state": "CA",
"postalCode": "94103",
"country": "US"
},
"createdAt": "2026-06-27T12:00:00.000Z"
}
}state values:
awaiting_verification— a manually-entered bank (ACH) payment was submitted but the buyer must first verify their bank via micro-deposits (the “verify your bank” step). Real and pending — do not fulfil yet, but do record it. It completes (→processing→succeeded) once verified, or expires after ~10 days. These rows also carryrequiresVerification: true. List them with?state=awaiting_verificationand subscribe to thepayment.awaiting_verificationwebhook.processing— the ACH is in flight and still settling. Do not fulfil yet. Keep polling (or wait for thepayment.successwebhook).succeeded— the payment settled successfully. Safe to fulfil. (For card/wallet this is immediate; for ACH it happens days later.)failed— terminal failure. The payment will not settle (an ACH return, a card decline, or a canceled/abandoned attempt — see ACH failures).canceled— an in-flight bank (ACH) payment was canceled before the debit was submitted — by you (dashboard or the cancel endpoint) or at the payments partner. Terminal, no money moved, the buyer is emailed. Distinct fromfailed(a decline or bank return). List them with?state=canceled; thepayment.canceledwebhook fires.refunded/partially_refunded— the payment was (partly) refunded.incomplete— the buyer started checkout but never submitted (abandoned / not confirmed). Terminal, no money coming — distinct fromawaiting_verification(submitted, pending) andfailed(a real decline / ACH return). List them with?state=incomplete.pending/authorized— created but not yet processing/settled.
Recommended ACH pattern: after you receive data.paymentId, poll GET /v1/merchant/payment/:id (e.g. once or twice a day) and fulfil only when state === "succeeded"; stop on state === "failed" or "canceled". A 404 means the id doesn't exist in your account (or the wrong mode — a test key only sees test payments).
List payments — GET /v1/merchant/payment
Returns your settled / terminal payments, paginated and filterable (by date range, status, client). Supports page, limit, range / startDate + endDate, and search.
curl "https://api.vezmo.com/api/v1/merchant/payment?page=1&limit=50" \
-H "Authorization: Bearer $ACCESS_TOKEN"Reconcile in-flight ACH — ?pending=true
Because ACH settles over several days, a payment that is currently processing is held separately and does not appear in the default list above (which shows settled/terminal payments). To list the in-flight ACH payments still settling — ideal for a batch reconciliation job — pass ?pending=true (or the equivalent ?state=processing):
curl "https://api.vezmo.com/api/v1/merchant/payment?pending=true&page=1&limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"Every row in that list has state: "processing". Re-run the job periodically; as each one settles it will disappear from this list and become succeeded (or failed), retrievable by id via GET /v1/merchant/payment/:id.
Reconcile verify-pending ACH — ?state=awaiting_verification
A manually-entered bank payment must be verified by the buyer via micro-deposits before it starts settling, so it sits in a state earlier than processing for up to ~10 days. Like in-flight ACH, these are not in the default list nor in ?pending=true (which is processing-only). List them with their own filter so a reconciliation job can discover them:
curl "https://api.vezmo.com/api/v1/merchant/payment?state=awaiting_verification&page=1&limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"Every row has state: "awaiting_verification", requiresVerification: true, and method: "ach". As each buyer verifies, the row moves to processing (and appears in ?pending=true), then succeeded; if the ~10-day window lapses it becomes failed. The best signal is the payment.awaiting_verification webhook, fired the moment the payment is submitted for verification.
ACH webhook lifecycle
For the full webhook reference (endpoints, headers, retries, signature) see the Webhooks page. The ACH-specific sequence is:
- Manual bank entry —
payment.awaiting_verification. If the buyer typed their bank details manually, the payment is submitted but must be verified via micro-deposits first. We sendpayment.awaiting_verificationat that moment (and the buyer gets a branded verify link by email). Record it as pending; it continues topayment.processingonce verified. Skipped for instantly-verified banks, which go straight topayment.processing. - Initiation —
payment.processing. When an ACH payment startsprocessingwe send apayment.processingevent, so you can record it as pending in real time (no polling required). You can also list the in-flight payments via?pending=true/?state=processing(above). Never infer the in-flight state from a failed event. - Settlement (days later) —
payment.success. This fires when the ACH actually settles. For ACH,payment.successis the settlement signal (not initiation) — it is the event to fulfil on. - Return / failure —
payment.failed. See the next section for how to read it correctly. - Cancel —
payment.canceled. An in-flight ACH canceled before the debit was submitted (by you via the cancel endpoint or the dashboard, or at the payments partner). Terminal; nothing was debited. Never fires for a settled payment — those can only be refunded.
Reading payment.failed for ACH (important)
A payment.failed event is always terminal — the payment will not settle. It comes in two flavours, and the difference is informational, not “final vs not final”:
- With a decline code (
declineCode/decline.networkDeclineCodeset, e.g. anR01-style code) — a real ACH return after the debit was attempted (e.g. insufficient funds, account closed). - Without a decline code (
declineCode: null) with a reason such as"PaymentIntent is not succeeded. Current status: canceled"— the payment attempt was canceled / abandoned (the buyer never completed the bank authorization, or a manual micro-deposit verification was never finished, so the attempt was canceled instead of settling).
Note: a payment that was alreadyprocessingorawaiting_verificationand is then canceled arrives aspayment.canceledwithstate: "canceled", not aspayment.failed.
state === "processing" (from GET /v1/merchant/payment/:id), or the absence of any terminal webhook — not the absence of a decline code. A genuinely processing ACH is never canceled by the checkout session; “canceled” only happens to attempts that never reached processing.Cancel a bank payment
A bank debit sits with the payments partner for about a business day before it is posted to the bank. In that window an ACH payment can be canceled outright — it is free, and the buyer is never debited — where a card payment could only be refunded (fee kept, 3-day credit). Cancel is available while the payment's state is processing or awaiting_verification; once the partner has submitted the debit, the call is refused and the payment must be refunded (POST /v1/merchant/payment/:id/refund) after it settles.
curl -X POST https://api.vezmo.com/api/v1/merchant/payment/cmqz9utka00km9tr9qblb8w77/cancel \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "reason": "requested_by_customer" }'reason is optional (default requested_by_customer) and is recorded with the payment and at the payments partner. Accepted values: requested_by_customer, duplicate, fraudulent, abandoned.
{
"success": true,
"message": "Payment canceled",
"data": {
"paymentId": "cmqz9utka00km9tr9qblb8w77",
"status": "canceled",
"state": "canceled",
"reason": "requested_by_customer",
"canceledBy": "api",
"canceledAt": "2026-09-10T09:12:44.000Z"
}
}What each response means:
- 200 — canceled. The payment now reads
state: "canceled", apayment.canceledwebhook is sent, and the buyer receives an email saying no money was taken. Calling again with the returnedpaymentIdreturns 200 with the same result (idempotent). For hosted-checkout, invoice and product payments the settled record gets its own id, so use thepaymentIdfrom the response (or frompayment.canceled) from then on. - 409 — too late: the partner has already submitted the debit. Nothing changed on our side. Wait for
payment.success, then refund. - 400 — not an in-flight bank payment (a card payment, or one that already settled — refund it instead).
- 404 — unknown id in your account (or the wrong mode: a test key only sees test payments).
| Cancel | Refund | |
|---|---|---|
| When | Before the debit is submitted (processing / awaiting_verification) | After the payment settled (succeeded) |
| Cost | Free | Processing fee is kept |
| Buyer | Never debited; emailed | Credited in ~3 business days |
| Webhook | payment.canceled | payment.refunded |
| Cards | Not available — refund instead | Yes |
Both settlement models behave the same: the cancel is applied wherever the payment runs, and the merchant can only ever cancel their own payments. Every cancel is recorded in your audit trail.
Verify a bank payment (micro-deposit)
A bank (ACH) payment whose account was entered manually waits in state: "awaiting_verification" until the buyer confirms the micro-deposit — normally on the Vezmo-hosted page we email them. This route lets you submit that deposit on the buyer's behalf, from your own app: read verification.type on the payment (bank payments → verify from your own app), then send the matching field.
# verification.type = "descriptor_code": the 6-character code on the $0.01 deposit
curl -X POST https://api.vezmo.com/api/v1/merchant/payment/cmqz9utka00km9tr9qblb8w77/verify-bank \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "descriptorCode": "SM11AA" }'
# verification.type = "amounts": the two deposits, in cents
curl -X POST https://api.vezmo.com/api/v1/merchant/payment/cmqz9utka00km9tr9qblb8w77/verify-bank \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "amounts": [32, 45] }'{
"success": true,
"message": "Bank verified",
"data": {
"paymentId": "cmqz9utka00km9tr9qblb8w77",
"status": "processing",
"state": "processing",
"verifiedAt": "2026-09-14T09:12:44.000Z",
"verifiedBy": "api"
}
}What each response means:
- 200 — verified. The debit starts:
payment.processingis sent now andpayment.successwhen it settles (orpayment.failedif the bank returns it). - 400 — the code or amounts did not match. Check the statement with the buyer and try again. Each try counts: after too many wrong attempts the bank is locked and the payment fails (409 from then on, plus a
payment.failedwithdeclineCode: "verification_attempts_exceeded"). Never guess. - 409 — can no longer be verified: the window lapsed, the bank was locked, the payment was canceled, or it was already verified. The message says which.
- 404 — unknown id in your account, not a bank payment awaiting verification, or the wrong mode (a test key only sees test payments).
Re-send the verification email
To re-send the buyer's email instead — optionally to a corrected address — call the resend route. The response carries the same verification block as GET /v1/merchant/payment/:id, including the link, so you can also show or share it yourself. Both routes need the payment.verify scope, which every key that can create payments already has.
# to the address on file
curl -X POST https://api.vezmo.com/api/v1/merchant/payment/cmqz9utka00km9tr9qblb8w77/verify-bank/resend \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{}'
# to a corrected address (it replaces the one on file)
curl -X POST https://api.vezmo.com/api/v1/merchant/payment/cmqz9utka00km9tr9qblb8w77/verify-bank/resend \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "email": "buyer@example.com" }'{
"success": true,
"message": "Verification email sent",
"data": {
"paymentId": "cmqz9utka00km9tr9qblb8w77",
"sentTo": "buyer@example.com",
"sentAt": "2026-09-14T10:02:11.000Z",
"verification": {
"type": "descriptor_code",
"url": "https://user.vezmo.com/verify-bank/bv_…",
"deadlineAt": "2026-09-24T09:12:44.000Z",
"attempts": 0,
"emailSentTo": "buyer@example.com",
"emailSentAt": "2026-09-14T09:12:44.000Z",
"reminderSentAt": "2026-09-14T10:02:11.000Z"
}
}
}- 400 — no email on file and none given. Pass
email. - 409 — the verification is no longer open (expired, locked, canceled or already verified).
- 404 — unknown id in your account, or the wrong mode.
A resend counts as the one reminder we would otherwise send after a few days, so the buyer is never emailed twice in a day.
Missed a webhook? Reconcile by polling
Webhook deliveries are retried automatically: up to 4 attempts at 0, +6h, +12h, and +24h (each attempt is recorded in your delivery log under Developer Settings). There is currently no manual replay/redelivery API. So if your endpoint was down and you missed events, the reliable way to catch up is to poll: list your in-flight ACH with ?pending=true and re-fetch each payment by id to read its current state. (Treat webhooks as a fast-path; the GET endpoints are the source of truth.)
Quick reference
GET /v1/merchant/payment/:id— one payment's currentstate(settled or in-flight ACH).GET /v1/merchant/payment— settled/terminal payments (paginated, filterable).GET /v1/merchant/payment?pending=true— in-flight ACH still settling.payment.successwebhook — the ACH settlement signal (fulfil here).payment.failedwebhook — terminal failure (with a decline code = an ACH return; without = canceled/abandoned).POST /v1/merchant/payment/:id/cancel— cancel an in-flight ACH before the debit is submitted (free; 409 once too late).payment.canceledwebhook — an in-flight ACH was canceled; nothing was debited.GET /v1/merchant/payment?state=awaiting_verification— manually-entered banks waiting on the buyer's micro-deposit; each row carries averificationobject (deposit type, shareable link, deadline, attempts).payment.awaiting_verificationwebhook — a bank payment entered that state; carries the sameverificationobject.POST /v1/merchant/payment/:id/verify-bank— submit the buyer's micro-deposit code or amounts from your own app (payment.verifyscope; 400 wrong value, 409 no longer verifiable). See Verify a bank payment.POST /v1/merchant/payment/:id/verify-bank/resend— re-send the buyer's verification email, optionally to a corrected address.