Vezmo App

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.)

Request
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:

200 OK
{
  "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 carry requiresVerification: true. List them with ?state=awaiting_verification and subscribe to the payment.awaiting_verification webhook.
  • processing — the ACH is in flight and still settling. Do not fulfil yet. Keep polling (or wait for the payment.success webhook).
  • 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 from failed (a decline or bank return). List them with ?state=canceled; the payment.canceled webhook 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 from awaiting_verification (submitted, pending) and failed (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.

Request
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):

Request
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:

Request
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 send payment.awaiting_verification at that moment (and the buyer gets a branded verify link by email). Record it as pending; it continues to payment.processing once verified. Skipped for instantly-verified banks, which go straight to payment.processing.
  • Initiation — payment.processing. When an ACH payment starts processing we send a payment.processing event, 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.success is 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.networkDeclineCode set, e.g. an R01-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 already processing or awaiting_verification and is then canceled arrives as payment.canceled with state: "canceled", not as payment.failed.
Do not treat “failed with no decline code” as still pending. A canceled/abandoned attempt is terminal — it will never settle. The correct way to know something is still in flight is 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.

POST /v1/merchant/payment/:id/cancel — requires the payment.cancel scope
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.

200 OK
{
  "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", a payment.canceled webhook is sent, and the buyer receives an email saying no money was taken. Calling again with the returned paymentId returns 200 with the same result (idempotent). For hosted-checkout, invoice and product payments the settled record gets its own id, so use the paymentId from the response (or from payment.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).
CancelRefund
WhenBefore the debit is submitted (processing / awaiting_verification)After the payment settled (succeeded)
CostFreeProcessing fee is kept
BuyerNever debited; emailedCredited in ~3 business days
Webhookpayment.canceledpayment.refunded
CardsNot available — refund insteadYes

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.

POST /v1/merchant/payment/:id/verify-bank — requires the payment.verify scope
# 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] }'
200 OK
{
  "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.processing is sent now and payment.success when it settles (or payment.failed if 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.failed with declineCode: "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.

POST /v1/merchant/payment/:id/verify-bank/resend — requires the payment.verify scope
# 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" }'
200 OK
{
  "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 current state (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.success webhook — the ACH settlement signal (fulfil here).
  • payment.failed webhook — 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.canceled webhook — 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 a verification object (deposit type, shareable link, deadline, attempts).
  • payment.awaiting_verification webhook — a bank payment entered that state; carries the same verification object.
  • POST /v1/merchant/payment/:id/verify-bank — submit the buyer's micro-deposit code or amounts from your own app (payment.verify scope; 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.