Developer guide
VEXPay siteAPI reference
Browse documentation

Subscriptions and recurring billing

Charge customers every day, week, month or year. Prices are in dollars; each invoice is paid in bolívares at the day’s rate, by link or by automatic debit. VEX Pay issues the invoices, emails your customers, retries and cancels for you.

What you get

Subscriptions turn a one-time sale into recurring revenue without building a billing system. You define the plan once; VEX Pay creates an invoice every period, sends it to your customer in your name, collects it, follows up when it is not paid, and tells your server about every step with webhooks.

  • Prices in dollars, paid in bolívares. A plan costs 10.00 USD; the customer pays the Bs equivalent at your account’s current rate (BCV or market, whichever your account uses) at the moment they pay, so your revenue does not lose value between periods.
  • Two ways to collect. send_invoice emails a payment link every period (Pago Móvil, Débito Inmediato, card, stablecoins or Colombian pesos — whatever your account accepts). charge_automatically debits the customer’s bank account by domiciliación: they authorize once, then pay nothing by hand. See Automatic charging.
  • A hosted invoice page. Every invoice has its own payment page; you never handle payment screens for renewals.
  • Emails in Spanish, sent in your name. Invoices, reminders, receipts and charge notices go out as “<your business> vía VEX Pay”, with replies going to your business email.
  • Dunning built in. Every invoice gets daily collection attempts: the first when it is issued, then one a day (3 in total by default, up to 15). If it is still unpaid after the last one, the subscription is canceled — and comes back on its own if the customer pays that invoice late.
  • Trials, cancel at period end, metadata and cursor pagination, with an API shaped like the billing APIs you already know.
Good fitExample
MembershipsA gym charging $25 a month
Classes and tutoringA school charging $40 every 4 weeks
Software and mediaA SaaS plan at $15 a month, or $150 a year
ServicesWeekly cleaning, monthly maintenance, internet service

How it works

ObjectWhat it isId
ProductWhat you sell (you may already have it for payment links)UUID
PriceHow much and how often: a USD amount and an interval (day, week, month, year) × intervalCountprice_…
CustomerWho pays: name, email, and optionally phone, cédula/RIF and bankcus_…
SubscriptionA customer on a price, with a collection method and statussub_…
InvoiceWhat is owed for one period, with its hosted payment pagein_…
Setup sessionA 24-hour link where the customer authorizes domiciliaciónseti_…
Payment methodThe customer’s authorized domiciliaciónpm_…
  1. You create the subscription

    The first period starts now (or when the trial ends). VEX Pay issues the first invoice immediately.

  2. The invoice is collected

    With send_invoice the customer receives an email with the payment link. With charge_automatically VEX Pay debits their account.

  3. It is paid

    The invoice becomes paid, the money lands in your balance like any other payment, the customer gets a receipt, and you get invoice.paid.

  4. The next period starts

    At the end of each period the next one starts and a new invoice is issued. Periods are counted from the start date, so a subscription that starts on the 31st renews on the last day of shorter months.

Quickstart: your first subscription

Use your Test key: nothing is charged, and emails are only delivered to your own business-profile email (see Test mode below). Call the API from your server with curl or one of the SDKs.

1. Productbash
curl --request POST "${API_URL}/v1/products" \
  --header "content-type: application/json" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --data '{ "name": "Plan Pro", "priceUsd": "25.00" }'
# → { "id": "PRODUCT_ID", ... }
2. Pricebash
curl --request POST "${API_URL}/v1/prices" \
  --header "content-type: application/json" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --data '{
    "product": "PRODUCT_ID",
    "unitAmountUsd": 25,
    "recurring": { "interval": "month", "intervalCount": 1 },
    "nickname": "Pro mensual"
  }'
# → { "id": "price_…", "unitAmountUsd": "25.00", "currency": "USD", ... }
3. Customerbash
curl --request POST "${API_URL}/v1/customers" \
  --header "content-type: application/json" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --data '{
    "name": "Ana Pérez",
    "email": "ana@example.com",
    "phone": "04141234567",
    "idNumber": "V12345678",
    "metadata": { "userId": "u_981" }
  }'
# → { "id": "cus_…", ... }
4. Subscriptionbash
curl --request POST "${API_URL}/v1/subscriptions" \
  --header "content-type: application/json" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --data '{
    "customer": "cus_…",
    "price": "price_…",
    "collectionMethod": "send_invoice"
  }'
Node SDKjavascript
const product = await vexpay.products.create({ name: 'Plan Pro', priceUsd: '25.00' });

const price = await vexpay.prices.create({
  product: product.id,
  unitAmountUsd: 25,
  recurring: { interval: 'month', intervalCount: 1 },
  nickname: 'Pro mensual',
});

const customer = await vexpay.customers.create({
  name: 'Ana Pérez',
  email: 'ana@example.com',
  metadata: { userId: 'u_981' },
});

const subscription = await vexpay.subscriptions.create({
  customer: customer.id,
  price: price.id,
  collectionMethod: 'send_invoice',
});
Python SDKpython
product = vexpay.products.create({"name": "Plan Pro", "priceUsd": "25.00"})

price = vexpay.prices.create({
    "product": product.id,
    "unitAmountUsd": 25,
    "recurring": {"interval": "month", "intervalCount": 1},
    "nickname": "Pro mensual",
})

customer = vexpay.customers.create({
    "name": "Ana Pérez",
    "email": "ana@example.com",
    "metadata": {"userId": "u_981"},
})

subscription = vexpay.subscriptions.create({
    "customer": customer.id,
    "price": price.id,
    "collectionMethod": "send_invoice",
})
PHP SDKphp
$product = $vexpay->products->create(['name' => 'Plan Pro', 'priceUsd' => '25.00']);

$price = $vexpay->prices->create([
    'product' => $product->id,
    'unitAmountUsd' => 25,
    'recurring' => ['interval' => 'month', 'intervalCount' => 1],
    'nickname' => 'Pro mensual',
]);

$customer = $vexpay->customers->create([
    'name' => 'Ana Pérez',
    'email' => 'ana@example.com',
    'metadata' => ['userId' => 'u_981'],
]);

$subscription = $vexpay->subscriptions->create([
    'customer' => $customer->id,
    'price' => $price->id,
    'collectionMethod' => 'send_invoice',
]);
Responsejson
{
  "id": "sub_3fd84b2d9ad14f74a2d90f2f56984db5",
  "object": "subscription",
  "customer": "cus_…",
  "price": { "id": "price_…", "unitAmountUsd": "25.00", "currency": "USD", "recurring": { "interval": "month", "intervalCount": 1 }, ... },
  "status": "active",
  "collectionMethod": "send_invoice",
  "collectionAttempts": null,
  "paymentMethod": null,
  "currentPeriodStart": "2026-10-21T14:00:00.000Z",
  "currentPeriodEnd": "2026-11-21T14:00:00.000Z",
  "trialStart": null,
  "trialEnd": null,
  "cancelAtPeriodEnd": false,
  "canceledAt": null,
  "cancellationReason": null,
  "latestInvoice": "in_…",
  "methods": [],
  "metadata": {},
  "createdAt": "2026-10-21T14:00:00.000Z",
  "livemode": false
}

The first invoice exists already: read it with GET /v1/invoices?subscription=sub_… and open its hostedInvoiceUrl to see what your customer sees. In live mode Ana also receives it by email; in test mode the email is only delivered if the customer’s email is your business-profile email.

Prices

A price belongs to a product and fixes the amount and the cycle. The same product can have several prices — monthly and yearly, for example.

FieldRules
unitAmountUsdDollars, up to 2 decimals, at least your account’s minimum charge (otherwise 422 amount_below_minimum) and at most 100,000.
recurring.intervalday, week, month or year.
recurring.intervalCountDefault 1. The whole cycle can be at most one year: up to 365 days, 52 weeks, 12 months or 1 year. Every 2 weeks is { "interval": "week", "intervalCount": 2 }.
nickname, metadataOptional; your own labels (metadata: up to 20 string pairs).

Amount, cycle and product never change once the price exists — changing them answers 400. To raise a price, create a new price and archive the old one with POST /v1/prices/:id and { "active": false }. Existing subscriptions keep their price; new ones cannot use an archived price (price_inactive).

Customers

FieldWhy it matters
emailRequired for send_invoice — it is where invoices go. emailStatus becomes undeliverable if the address bounces; update the email to try again.
phone, idNumber, bankCodeOptional. They prefill the domiciliación authorization page, so the customer types less.
name, metadataShown in your portal; use metadata to store your own user id.

Update with POST /v1/customers/:id (send null to clear a field). DELETE /v1/customers/:id removes a customer with no live subscription; otherwise it answers 409 customer_has_active_subscriptions. Find a customer by email with GET /v1/customers?email=ana@example.com.

Subscriptions

FieldMeaning
customer, priceRequired. The price must be active.
collectionMethodsend_invoice (default) or charge_automatically.
collectionAttempts1–15 daily attempts per invoice before the subscription is canceled. Omit (or null) to use your account default, 3 unless you change it in Portal → Billing → Settings.
paymentMethodcharge_automatically only: the pm_… to debit. Default: the customer’s most recent domiciliación that is not inactive.
trialPeriodDays or trialEndA free trial: 1–730 days, or an ISO date up to 730 days ahead. No invoice until it ends.
cancelAtPeriodEndtrue ends the subscription when the current period finishes, with no new invoice.
methodsWhich hosted checkout methods its invoices offer: pago_movil, c2p, vpos, usdt, usdc, cop. Omit to offer every method your account accepts.
metadataYour own key/value pairs.
StatusMeaning
trialingIn the free trial.
activePaid up, or its invoice was issued less than a day ago.
past_dueAn invoice is unpaid one day after it was issued. Collection attempts continue.
canceledEnded. cancellationReason is requested (by you or at period end) or payment_failed (no payment after the last attempt).
To…Call
ListGET /v1/subscriptions?customer=cus_…&status=past_due&limit=25 — newest first; pass nextCursor as cursor for the next page.
Cancel at period endPOST /v1/subscriptions/:id with { "cancelAtPeriodEnd": true } (send false to undo).
Cancel nowDELETE /v1/subscriptions/:id. Open invoices are voided.
Switch collection methodPOST /v1/subscriptions/:id with { "collectionMethod": "charge_automatically" } (or back to send_invoice, which needs a customer email). Applies from the next invoice; the same checks as on create apply, including the per-charge limit (Automatic charging).
Change the attempts or methodsPOST /v1/subscriptions/:id with collectionAttempts or methods. Applies from the next invoice.

Invoices and the hosted invoice page

Each period has one invoice, numbered per account with no gaps (K7Q2M9XA-0001, -0002, …). It is due when issued. Its hostedInvoiceUrl is a payment page in your name: the customer picks a method and pays the Bs amount at that moment’s rate. Emails show the amount in dollars only, because the bolívar amount depends on the day they pay.

StatusMeaningCan still be paid?
openWaiting for paymentYes
paidPaid. paidAt, paymentId and billingFee are set—
uncollectibleGave up collecting (after the last attempt, or by you)Yes — a late payment still counts
voidCanceled; the payment page closesNo
To…Call
ListGET /v1/invoices?subscription=sub_… (also customer, status, cursor, limit).
VoidPOST /v1/invoices/:id/void — only an open invoice.
Stop collectingPOST /v1/invoices/:id/mark-uncollectible — the page stays payable.
Record a payment made outside VEX PayPOST /v1/invoices/:id/pay with { "paidOutOfBand": true } (cash, transfer). No money moves and no fee is charged.
Email it againPortal → Billing → Invoices → Resend, at most once every 10 minutes.
  • attemptsTotal, attemptCount and nextPaymentAttempt show where the invoice is in its collection attempts.
  • When the invoice is paid through VEX Pay, payment.completed includes invoice: { id, number, subscription }, so your existing payment handler knows which subscription it was for.

Collection, retries and cancellation

Every invoice gets N collection attempts, one a day: attempt 1 the moment it is issued, attempt 2 a day later, and so on. N is the subscription’s collectionAttempts (1–15), else your account default (3 unless you change it). N is fixed on the invoice when it is issued (attemptsTotal), so changing it later applies from the next invoice. Each attempt either emails the invoice (send_invoice) or debits the customer (charge_automatically; if no debit can be made, the attempt emails the invoice instead).

When (from issue)What happensWebhooks
Day 0Attempt 1: invoice email “Factura … de <your business>”, or the first automatic debitinvoice.created, invoice.payment_attempt
Day 1Still unpaid → the invoice is overdue and the subscription becomes past_due. Attempt 2: reminder email or a new debitinvoice.overdue, customer.subscription.updated, invoice.payment_attempt
Days 2 … N−1One attempt a day. The last email says “Último aviso”invoice.payment_attempt
Day NStill unpaid → its open invoices become uncollectible, the subscription is canceled with cancellationReason: "payment_failed", and the customer receives the “Suscripción cancelada por falta de pago” email with a button to pay and reactivateinvoice.marked_uncollectible, customer.subscription.deleted

With the default N = 3 that is: attempts on days 0, 1 and 2, past_due on day 1, canceled on day 3. With N = 1 the invoice becomes overdue and the subscription is canceled at the same time, one day after issue. Paying at any point stops the remaining attempts, emails a receipt and returns a past_due subscription to active. A past_due subscription keeps renewing: if a period ends while an invoice is unpaid, the next invoice is still issued.

Choosing N: 3 is a good default for monthly plans. Use more (up to 15) when your customers often pay late, and fewer for daily or weekly plans. Set the account default in Portal → Billing → Settings, or per subscription with collectionAttempts.

Emails to your customers

All emails are in Spanish, show your business name and logo from your business profile, come from facturas@mail.vexwallet.co as “<your business> vía VEX Pay”, and replies go to your business email. Each one is sent once — retries never send duplicates.

EmailWhen
Factura … de <your business>An invoice is issued (send_invoice), or an automatic debit could not be made
Recordatorio / Último avisoEach later collection attempt of an unpaid invoice that is not a successful debit, with “intento X de N”; the last one is the final notice before cancellation
Pago recibidoAn invoice is paid. For automatic debits it shows the Bs amount debited and the rate
Aviso de cobro3 days before each automatic renewal (when the payment method can still be debited), with a Bs estimate at today’s rate
Suscripción cancelada por falta de pagoAfter the last attempt, with a button to pay and reactivate

Invoice and reminder emails show the dollar amount, the invoice number, the date the subscription will be canceled if unpaid, and the button to the hosted invoice page. They never show a fixed Bs amount, because it is calculated when the customer pays.

Trials and notices

  • A trial starts with status trialing and no invoice. When it ends, the first paid period starts and its invoice is issued.
  • customer.subscription.trial_will_end fires 3 days before a trial ends (for trials longer than 3 days) — a good moment to remind the customer.
  • invoice.upcoming fires 3 days before each renewal that will happen, with the amount and the next period. Automatic-debit customers also receive the “Aviso de cobro” email then.

Fees and your balance

An invoice paid through VEX Pay is a normal payment: its method’s fee applies as usual (see Pricing) and the money follows the usual balance hold (see Balances & holds). On top of that, VEX Pay charges a billing fee of 0.5% of each paid invoice (unless your agreement says otherwise), in the currency the customer paid. It appears on the invoice as billingFee and in your balance movements.

Example: $25 plan paid by Pago Móvil at Bs 100/USDAmount
Customer paysBs 2,500.00
Pago Móvil feeper your rates — see Pricing
Billing fee (0.5%)Bs 12.50
You receiveBs 2,500.00 − Pago Móvil fee − Bs 12.50

An automatic debit (domiciliación) carries your bolívar pay-in rate, the same as Débito Inmediato (see Pricing), plus the billing fee. Invoices marked paid out of band (paidOutOfBand: true) move no money and carry no fee.

Webhooks

Subscribe your endpoint to the billing events you need (see Webhooks for registration and signature verification). Unless noted, data is the full object — the same JSON the API returns — plus livemode.

EventWhen`data`
customer.created / customer.updatedA customer was created or updatedCustomer
customer.deletedA customer was deletedCustomer + deleted: true
customer.subscription.createdA subscription was createdSubscription
customer.subscription.updatedA new period started, it went past_due or back to active, or you changed cancel-at-period-end, the collection method or the payment methodSubscription
customer.subscription.deletedCanceled — by you, at period end, or for non-payment (cancellationReason)Subscription
customer.subscription.trial_will_end3 days before the trial endsSubscription
customer.subscription.reactivatedA late payment brought a subscription canceled for non-payment backSubscription
invoice.createdAn invoice was issuedInvoice
invoice.upcoming3 days before a renewalsubscription, customer, amountDueUsd, currency, renewsAt, periodStart, periodEnd
invoice.payment_attemptEach collection attemptinvoice, attempt, attemptsTotal, nextPaymentAttempt, collectionMethod, attemptedAt
invoice.payment_failedAn automatic debit was rejected, or the invoice is above the per-charge limit (paymentId is then null)invoice, attempt, attemptsTotal, nextPaymentAttempt, paymentId, failureCode, failureMessage
invoice.overdueStill unpaid one day after issueInvoice
invoice.paidPaid — through VEX Pay or marked paid out of bandInvoice
invoice.voidedVoided by you or by an immediate cancelInvoice
invoice.marked_uncollectibleMarked uncollectible by you or after the last attemptInvoice
payment_method.attachedA customer authorized domiciliaciónPayment method (pending)
payment_method.updatedIt became active (first debit accepted) or inactive (a debit showed the authorization was canceled or suspended at the bank)Payment method
payment_method.detachedYou detached itPayment method
Register billing eventsbash
curl --request POST "${API_URL}/v1/webhooks" \
  --header "content-type: application/json" \
  --header "x-api-key: ${VEXPAY_API_KEY}" \
  --data '{
    "url": "https://example.com/webhooks/vexpay",
    "events": [
      "customer.subscription.created", "customer.subscription.updated",
      "customer.subscription.deleted", "customer.subscription.reactivated",
      "invoice.paid", "invoice.payment_failed", "invoice.overdue",
      "payment_method.attached", "payment_method.updated"
    ]
  }'
invoice.paidjson
{
  "event": "invoice.paid",
  "data": {
    "id": "in_3fd84b2d9ad14f74a2d90f2f56984db5",
    "object": "invoice",
    "number": "K7Q2M9XA-0001",
    "status": "paid",
    "customer": "cus_…",
    "subscription": "sub_…",
    "collectionMethod": "send_invoice",
    "amountDueUsd": "25.00",
    "amountPaidUsd": "25.00",
    "currency": "USD",
    "periodStart": "2026-10-21T14:00:00.000Z",
    "periodEnd": "2026-11-21T14:00:00.000Z",
    "dueDate": "2026-10-21T14:00:00.000Z",
    "attemptsTotal": 3,
    "attemptCount": 1,
    "nextPaymentAttempt": null,
    "paidAt": "2026-10-21T15:12:40.000Z",
    "paidOutOfBand": false,
    "paymentId": "9b2c…",
    "billingFee": { "amount": "12.50", "currency": "VES", "percent": "0.5" },
    "hostedInvoiceUrl": "https://pay.vexwallet.co/pay/in-…",
    "createdAt": "2026-10-21T14:00:00.000Z",
    "livemode": true
  },
  "timestamp": "2026-10-21T15:12:41.000Z"
}

Test mode

  • Everything above works with your Test key, with the same webhooks. Test and live objects are separate.
  • Emails go only to your business-profile email. A test-mode email is delivered only when the customer’s email is the email on your business profile; every other address is suppressed. Use that address on your test customers to see exactly what your customers receive.
  • Pay test invoices on their hosted page with the test payment methods.
  • Automatic charging works in test mode without asking VEX Pay to enable it: a simulator stands in for the bank. See Automatic charging for the phone numbers that simulate each outcome.
  • Test clocks are coming soon. Until then, try renewals and dunning with a daily price ("recurring": { "interval": "day" }): a new invoice every 24 hours, and an unpaid one reaches past_due after a day and cancellation after N days.

Errors

Errors use the usual shape: { "error": "<code>", "message": "…" }, plus field for invalid input.

StatuserrorMeaning
400invalid_dataA field is missing or invalid; see field
400customer_email_requiredsend_invoice needs a customer with an email
400price_inactiveThe price is archived
400plan_changes_not_supportedA subscription’s price cannot be changed
400subscription_canceledThe subscription is already canceled
400invoice_not_openOnly an open invoice can be voided, marked uncollectible or marked paid
400method_not_allowedDomiciliación is not enabled for your account
400automatic_charging_unavailableAutomatic charging (and setup sessions) are not enabled yet for live accounts. Never returned in test mode
400amount_exceeds_limitcharge_automatically for a price above Bs 1,500,000 at today’s rate; the message gives the approximate USD maximum. Use send_invoice
400payment_method_requiredThe customer has not authorized domiciliación yet
400payment_method_detachedThe payment method was detached
404not_foundNo such object in this account (or in this mode)
409customer_has_active_subscriptionsCancel the customer’s subscriptions before deleting it
422amount_below_minimumThe price is below your minimum charge

Full request and response schemas for every billing endpoint are in the API reference (API reference).