Vezmo App

Billing

Subscriptions

Sell recurring plans and let VezmoPay bill your customers automatically. You create a product with one or more plans (a price on a cadence), then create a subscription for a customer. VezmoPay collects the first payment, saves the payment method, and charges every cycle — with automatic retries (dunning) if a charge fails. Every subscription charge settles into your VezmoPay balance just like any other sale.

Required API-key permissions

  • subscription.create — create, cancel, pause, resume, update or change the plan of a subscription.
  • subscription.read — list and retrieve subscriptions, and list the plans, customers and a customer's saved payment methods that a create needs.

Every new key has both, and keys created earlier received them when the subscription API opened.

Authentication

Calls authenticate like the rest of the API, in two steps, on your server. Your API key and secret never go to a browser or an app.

  1. POST /merchant/api-auth/login with your key and secret in the x-api-key and x-api-secret headers. It returns an access token at data.accessToken.token (valid 30 minutes).
  2. Send that token as Authorization: Bearer <accessToken> on every subscription call.
Get an access token
curl -X POST https://api.vezmo.com/api/v1/merchant/api-auth/login \
  -H "x-api-key: $VEZMO_API_KEY" \
  -H "x-api-secret: $VEZMO_API_SECRET"

# data.accessToken.token → use it as the Bearer token below

See Create token for the full response. Sandbox keys are under VezmoPay → Developers → API keys.

Subscription routes are limited per IP: 3 a second, 20 per 10 seconds, 60 a minute. Over that returns 429. See Pagination & limits.

Create a subscription

POST /merchant/subscription

  • planId (required) — the plan to subscribe the customer to. List them with GET /merchant/subscription/plans. A product made in Products & Services with a recurring price also lists its plans at billingProduct.plans[].id (GET /merchant/products). Sandbox keys can use those catalog plans too; the subscription, and every charge on it, stays in sandbox.
  • customerId, or customerEmail to find or create the customer (with customerName and customerPhone). One of the two is required. customerId is a subscription customer id: list them with GET /merchant/subscription/customers. It is also customer.id on any subscription.
  • quantity — seats, default 1.
  • trialDays — 0 to 730. Overrides the plan's trial; 0 starts billing now even if the plan has one.
  • promotionCode — a promo code from Promo codes in your dashboard (one that applies to subscriptions), or a subscription coupon. A code that does not apply (unknown, expired, used up, not for subscriptions, or in another currency) gets 400 “This promo code is invalid.” A code that would make the first invoice free gets 400 “This promo code would fully discount the price — use a free trial instead.” The code counts as used when the first payment goes through, or once a free trial has a card on file. A checkout nobody finishes doesn't use it.
  • savedCardId or savedBankId — the saved method renewals charge. It must be the customer's own, saved with your business, and the customer must have agreed to recurring payments with your business on that card or bank on the hosted subscription checkout (a subscription they started there before). A card saved for one-click checkout is not that agreement. Otherwise you get 400: create the subscription without a saved method and send the checkout link. List the methods you may bill with GET /merchant/subscription/customers/{id}/payment-methods.
  • mandateConsentText — optional. Your record of the authorization text, kept on the mandate. It does not replace the customer's agreement above.
  • emailCheckoutLink — true to email the customer the checkout link. Ignored when you pass a saved method.

If you don't pass a saved payment method, the response includes a checkoutPath — a white-label hosted checkout link — and its checkoutToken. Send the link to your customer, or open the checkout in your own app with the token; when they enter and save their card, the subscription activates and renews automatically.

Vezmo's server takes the first card payment, on the hosted checkout and in the apps: it reads the customer's card first and charges the fee for that card (an American Express card, or a card issued abroad, has its own price in your fees), and only Vezmo confirms the payment. Once the older checkout flow is switched off (during this release, so a page opened before it can still finish), no page or script can pay it at another price. When the bank asks for 3D Secure, the customer completes it on the checkout and Vezmo confirms the payment right after; if they close the page at that moment, Vezmo still confirms it within minutes and the subscription activates. A US bank account and Instant ACH are each charged at their own price. A free trial only saves the card (nothing is charged): Vezmo's server checks that card with its bank and saves it; only when the bank asks for 3D Secure does the customer finish that step on the checkout, and then only the card Vezmo checked is saved. Its first charge, when the trial ends, is priced for that card in the same way. All of this is the same for a merchant-of-record business (whose fee is set when the payment settles).

When Instant ACH is on for your business, the hosted checkout offers it too: the customer signs in to their bank, and that bank account funds every renewal (and is saved on a free trial, with nothing charged until it ends). Instant ACH is in US dollars only, up to $7,500 per charge. A renewal is charged with it only while your business still offers it for that amount; otherwise the customer is asked for another way to pay, as when no method is on file.

With a saved payment method (savedCardId or savedBankId, which the customer agreed to as above) and no free trial, the first payment is charged right away; charge in the response says how it went.

Request
curl -X POST https://api.vezmo.com/api/v1/merchant/subscription \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-1042" \
  -d '{
    "planId": "<plan_id>",
    "customerEmail": "jane@example.com",
    "customerName": "Jane Doe",
    "emailCheckoutLink": true
  }'

Retries and Idempotency-Key

Send Idempotency-Key: <1-255 ASCII characters> to make a retry safe.

  • The same key with the same body returns the first call's response and never charges again.
  • The same key with a different body returns 422 “Idempotency key was reused with different parameters”.
  • A key outside 1-255 printable ASCII characters returns 400.
  • Keys are per business and per mode (live or sandbox).

Response:

201 Created
{
  "success": true,
  "data": {
    "id": "<subscription_id>",
    "subscriptionRef": "VS7K2P9Q4FXM",
    "status": "INCOMPLETE",
    "checkoutPath": "/checkout/subscription/<token>",
    "checkoutToken": "<token>",
    "checkoutLinkEmail": {
      "sentTo": "jane@example.com",
      "sentAt": "2026-09-27T10:15:00.000Z"
    },
    "firstInvoiceRef": "VI3N8W5X2MQ7",
    "charge": null
  }
}

The first charge

charge is null unless you passed a saved method and there is no free trial. Then it is { status, invoiceId, paymentId?, message? }, where status is one of:

  • SUCCEEDED — paid; the subscription is ACTIVE.
  • PROCESSING — paid, money on its way (a bank debit). It activates when it settles.
  • REQUIRES_ACTION — the bank asks the customer to confirm (for example 3D Secure).
  • FAILED — declined; message says why.
  • NO_METHOD — the saved method can't be used.
  • ALREADY_PAID / NOTHING_DUE — nothing was charged.

A decline never fails the request, and no checkout link is returned with a saved method. The subscription stays INCOMPLETE and the customer is emailed.

  • A soft decline is retried automatically: 4 retries over about 16 days (1, 3, 5 and 7 days apart).
  • Insufficient funds is retried over about 21 days (2, 5, 7 and 7 days apart).
  • A temporary processing error is retried over about a week (1, 1, 2 and 3 days apart).
  • A hard decline, a card that needs updating, a bank asking the customer to confirm (3D Secure), or no usable payment method is never charged again with the same method. The customer is emailed a link to their manage page, where they add a card and pay. The reminders follow the same 1, 3, 5 and 7-day schedule; when they run out, the subscription becomes UNPAID.
  • When the retries of a first payment run out, the subscription is canceled (subscription.canceled, cancellationReason: payment_failed). When a renewal's retries run out, it becomes UNPAID.
  • A subscription still waiting for its first payment 30 days after it was created, with no retry scheduled, ends: CANCELED, cancellationReason: checkout_expired.

In your mobile app, pass checkoutToken to the Vezmo SDK as subscriptionToken: the payment sheet shows the plan, takes the first payment or starts the free trial, and returns the subscription to your app. See the mobile SDKs: React Native, iOS, Android, Flutter.

Build the full checkout URL as https://user.vezmo.com{checkoutPath} and send it to the customer — or pass "emailCheckoutLink": true and we email it to them for you, in the same request.

checkoutLinkEmail is null unless you asked for the email. It holds sentTo and sentAt when the email went out, or error with the reason when it did not (for example, the customer has no email address on file). The subscription is created either way — a failed email never fails the request, so check this field and fall back to sending checkoutPath yourself.

In test mode, emails go only to your team's addresses. With a test key, a customer outside your team is not emailed: checkoutLinkEmail is { "sentTo": null, "sentAt": null, "emailSkipped": "test_mode", "message": "In test mode, emails go only to your team's addresses." }. It doesn't use up the link's resend wait or its daily sends. The link itself works: open checkoutPath yourself. A test subscription's other emails to its customer (receipts, trial, failed payment, cancellation) follow the same rule: they reach the customer only when that is your own or a team member's address.

Plans, customers and payment methods

These three reads give you the ids a create needs. Each needs subscription.read.

List plans

GET /merchant/subscription/plans?productId=&take=&skip= — the active plans this key can subscribe a customer to, newest first. Each item's id is a planId. product.catalogProductId is the Products & Services id, or null.

200 OK
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "<plan_id>",
        "nickname": "Pro monthly",
        "amount": "9.99",
        "currency": "USD",
        "interval": "MONTH",
        "intervalCount": 1,
        "trialDays": 14,
        "setupFee": null,
        "createdAt": "2026-09-01T10:00:00.000Z",
        "product": { "id": "<billing_product_id>", "name": "Pro", "catalogProductId": "<product_id>" }
      }
    ],
    "total": 1
  }
}

List customers

GET /merchant/subscription/customers?search=&take=&skip= — your subscription customers. search matches the email or the name. Each item's id is a customerId.

List a customer's payment methods

GET /merchant/subscription/customers/{id}/payment-methods — the saved cards and banks you may bill on a new subscription: only the ones the customer agreed to recurring payments on with your business. An unknown customer returns 404. Each card has a kind: card, or instantAch for a bank account saved with Instant ACH, which has no brand or last4 and shows the customer's masked email instead (pass it as savedCardId; US-dollar plans only).

200 OK
{
  "success": true,
  "data": {
    "cards": [
      { "savedCardId": "<saved_card_id>", "kind": "card", "brand": "visa", "last4": "4242" },
      { "savedCardId": "<saved_card_id>", "kind": "instantAch", "brand": null, "last4": null, "maskedEmail": "an••@example.com" }
    ],
    "banks": [{ "savedBankId": "<saved_bank_id>", "bankName": "Example Bank", "last4": "6789" }]
  }
}

The two lists answer { items, total }. take is 0 to 100 (default 25) and skip is 0 to 100000.

List subscriptions

GET /merchant/subscription — optional query status, take, skip. The response is { items, total }.

  • status takes one value: INCOMPLETE, TRIALING, ACTIVE, PAST_DUE, UNPAID, PAUSED or CANCELED. Anything else returns 400.
  • take is 0 to 100 (default 25), and skip is 0 to 100000. Anything else returns 400 “take must be a whole number from 0 to 100.”

Retrieve a subscription

GET /merchant/subscription/{id} — returns the subscription with its customer, plan items, and recent invoices. {id} is the subscription id or its subscriptionRef (VS…). Webhooks carry both. The other routes below take the id.

Cancel a subscription

POST /merchant/subscription/{id}/cancel

By default the subscription cancels at the end of the current period (the customer keeps access until then). Pass { "atPeriodEnd": false } to cancel immediately.

  • A cancel at the period end sends subscription.cancel_scheduled. The subscription stays active, and subscription.canceled comes when the period ends.
  • atPeriodEnd: false ends it now and sends subscription.canceled.
  • Booking a cancel that is already booked changes nothing and sends nothing.
  • A canceled subscription returns 400 “This subscription is already canceled.”
  • A cancel now waits for a payment of the subscription that is already being taken (up to 30 seconds), so nothing is ever charged after it: that payment lands first, and you can refund it. If it is still finishing after that, the cancel returns 409 with payment_in_progress; try again in a moment.

Pause & resume

POST /merchant/subscription/{id}/pause stops billing until you resume — the customer keeps their saved payment method and mandate. Like a cancel now, it waits for a payment already being taken (up to 30 seconds), and returns 409 payment_in_progress if one is still finishing.

POST /merchant/subscription/{id}/resume sets the subscription to ACTIVE. If the current period is still running, the next charge stays at its end. If it ended while paused, the paused time is not billed: a new period starts when you resume, and its first charge runs within the hour. It does so whatever status it was paused from, including a trial or a past-due subscription. Only a PAUSED subscription can be resumed (400 otherwise).

Update a subscription

POST /merchant/subscription/{id}/update — change the quantity (seats). Pass { "quantity": 5 }. The next invoice bills the new quantity.

quantity is clamped to 1-100000. A request without quantity changes nothing but still sends subscription.updated.

Change plan

POST /merchant/subscription/{id}/change-plan — move an ACTIVE or TRIALING subscription to another plan in the same currency.

Request body
{
  "planId": "<plan_id>",
  "quantity": 3,
  "prorationBehavior": "immediate"
}
  • quantity is optional; it keeps the current quantity.
  • prorationBehavior: "immediate" (default) settles the difference for the rest of the current period now, at the price the period was billed (the subscription's coupon or promo code, when it applies to this period, comes off both plans): an upgrade is charged right away, a downgrade is credited to the next renewal.
  • An upgrade worth less than the smallest charge for your account is not charged now. It is added to the next renewal invoice: prorationInvoiceId is null and proration.carriedToNextInvoice is true.
  • "next_period" switches the plan with no proration; the new price starts at the next renewal. A trial always switches this way.
  • An upgrade's proration is charged at once. If that charge fails, the call still succeeds: the plan has changed and the proration invoice goes to retries like any other. subscription.updated carries that invoice.
200 OK
{
  "success": true,
  "message": "Subscription plan changed",
  "data": {
    "subscription": { "id": "<subscription_id>", "status": "ACTIVE", "…": "the subscription, as Retrieve returns it" },
    "prorationInvoiceId": "<invoice_id>",
    "proration": { "creditMajor": "4.99", "chargeMajor": "9.99", "netMajor": "5.00", "carriedToNextInvoice": false }
  }
}

Subscription statuses

  • INCOMPLETE — created, awaiting the first payment. Ends after 30 days without one.
  • TRIALING — in a free trial; not yet charged.
  • ACTIVE — good standing; renewing automatically.
  • PAST_DUE — a renewal failed; retries or reminders to the customer in progress.
  • UNPAID — retries or reminders ran out, or a payment was lost to a dispute; access should be revoked. Renewals stop. The customer can still pay the renewal that stopped it from their manage page while its period runs, which makes it ACTIVE again (not after a lost dispute).
  • PAUSED — billing paused.
  • CANCELED — ended (canceled, its first payment never collected, or its checkout expired after 30 days).

The customer's manage page

Every billing email to your customer links to their own manage page, white-labelled with your name and logo. There they can:

  • see their plan, next charge, card on file and billing history;
  • pay a renewal that failed, with the card on file or a new one. 3D Secure runs on the page when their bank asks for it. A payment that goes through makes the subscription ACTIVE again and sends subscription.renewed; a new card then replaces the one renewals are charged to;
  • cancel at the end of the current period.

As on the checkout, Vezmo's server takes every card payment there: it reads the card first and charges the fee for that card (an American Express card, or a card issued abroad, has its own price in your fees), and only Vezmo confirms the payment. Once the older flow is switched off (during this release, so a page opened before it can still finish), no page or script can pay it at another price. After 3D Secure, Vezmo confirms the payment right away; if the customer closes the page at that moment, Vezmo still confirms it within minutes. Too many declined cards on the page, or the same card declined too often anywhere, and the page says "This card can't be used here right now. Try another card, or try again later." (the API answers 429 too_many_declines, see declined cards); the page takes cards only, so it never suggests another way to pay.

Only card payments are taken there, and only of what is owed: a card is never saved there without a payment. A few payments are allowed a day per subscription and per business. Declined cards follow the same limits as every checkout (the subscription checkout too): at a limit, the page asks the customer to try again later or pay another way.

The "Update payment method" button in the payment-failed email opens the payment step directly. A paused or canceled subscription can't be paid or changed there, and a first payment is taken on the checkout link instead.

Webhooks

Subscribe to these events to keep your system in sync — each delivers a white-label payload with the subscriptionRef, status, plan, customer, livemode and (where relevant) invoice:

  • subscription.trial_started — a free trial started; nothing charged yet.
  • subscription.activated — first payment succeeded; now active.
  • subscription.renewed — a renewal payment succeeded (also sent when a renewal owes nothing, for example when an amount too small to charge moves to the next invoice).
  • subscription.payment_failed — a charge failed (a retry is scheduled, or action is needed). Each failed attempt is its own delivery. A reminder to the customer is not a charge and sends none.
  • subscription.paused / subscription.resumed — billing paused / reactivated.
  • subscription.updated — quantity/seats changed, the plan changed, or the reminders for a renewal ran out (status UNPAID).
  • subscription.cancel_scheduled — a cancel was booked for the end of the current period, by you or by the customer on their manage page. The subscription stays active until then. cancellationReason is merchant or customer.
  • subscription.canceled — the subscription ended. cancellationReason is one of:
    • merchant — you canceled now.
    • period_end — a booked cancel took effect.
    • payment_failed — the first payment's retries ran out.
    • checkout_expired — no first payment within 30 days.
  • subscription.refunded — a subscription payment was fully or partly refunded.
  • subscription.disputed — a subscription payment was disputed, or its dispute changed status (won or lost). A lost dispute makes the subscription UNPAID; a dispute won afterwards makes it ACTIVE again when nothing else is unpaid and its current period hasn't ended.

If an endpoint was subscribed to subscription.canceled before 18 Oct 2026, it was also given subscription.cancel_scheduled. It still hears about a booked cancel, now under that name.

The delivered body is { id, event, data }, as for every event. Its data, for example on subscription.renewed:

data — subscription.*
{
  "id": "VS7K2P9Q4FXM:subscription.renewed:VIKZB7Q2M4HD",
  "event": "subscription.renewed",
  "companyId": "…",
  "createdAt": "2026-11-16T09:00:04.000Z",
  "subscriptionId": "<subscription_id>",  // the id every /merchant/subscription/{id} route takes
  "subscriptionRef": "VS7K2P9Q4FXM",
  "customerId": "<customer_id>",
  "livemode": true,            // false for sandbox subscriptions
  "status": "ACTIVE",
  "quantity": 1,
  "currentPeriodStart": "2026-11-16T09:00:00.000Z",
  "currentPeriodEnd": "2026-12-16T09:00:00.000Z",
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "canceledAt": null,
  "nextBillingAt": "2026-12-16T09:00:00.000Z",
  // "cancellationReason": "period_end",  only on subscription.cancel_scheduled and subscription.canceled
  "plan": { "nickname": "Pro", "amount": 9.99, "currency": "USD", "interval": "MONTH", "intervalCount": 1 },
  "customer": { "email": "ana@example.com", "name": "Ana Silva" },
  "invoice": {                 // on events about a charge: activated, renewed, payment_failed, refunded,
                               // disputed, and updated after a plan change
    "invoiceId": "<invoice_id>", "invoiceRef": "VIKZB7Q2M4HD",
    "amountDue": 9.99, "amountPaid": 9.99, "currency": "USD", "status": "PAID",
    "periodStart": "2026-11-16T09:00:00.000Z", "periodEnd": "2026-12-16T09:00:00.000Z"
  },
  "data": { … }                // the same fields again, for integrations built on the earlier shape
}

An event that can happen more than once is a separate delivery each time, with its own envelope id. This covers subscription.updated, paused, resumed, each payment_failed attempt, each refunded refund and each disputed status. A retried delivery of the same occurrence keeps its id, so you can dedupe on it.

Payloads are HMAC-signed. See the Webhooks guide for signature verification, retries, and delivery logs.