Skip to main content

VEXPay Python SDK

The official Python library for the VEXPay API: accept Venezuelan payments (Pago Móvil C2P, cards, débito inmediato), pay out to merchants, create checkout sessions, and verify signed webhooks.

  • Sync (VexPay) and async (AsyncVexPay) clients with the same methods
  • Typed Pydantic v2 models generated from the VEXPay OpenAPI contract
  • Safe retries: every POST carries an Idempotency-Key, so a retried charge never charges twice
  • Auto-pagination
  • Webhook signature verification with replay protection
  • Python 3.9+

Install

pip install vexpay

Quickstart

from vexpay import VexPay

vexpay = VexPay()  # reads VEXPAY_API_KEY from the environment

quote = vexpay.quotes.retrieve({"usdAmount": 25})
print(f"$25 = Bs {quote.vesAmount} at BCV {quote.bcvRate}")

Keep your API key on the server. Use your test-mode key while you build; it runs against the sandbox provider. Request bodies and query parameters use the API's field names (usdAmount, debtorBankCode, …) — pass a dict or one of the models in vexpay._generated.models.

Collect a Pago Móvil C2P payment

C2P is two steps: create an intent (the payer's bank sends them a token by SMS), then charge with the token your customer types in.

from vexpay import VexPay

vexpay = VexPay()

payer = {
    "usdAmount": 25,
    "debtorId": "V12345678",
    "debtorCellPhone": "584141234567",
    "debtorBankCode": 102,
}

intent = vexpay.payments.c2p.request({**payer, "externalRef": "order-1042"})

token = input("Bank token: ")  # the customer reads it from their bank SMS

payment = vexpay.payments.c2p.execute({**payer, "intentId": intent.intentId, "token": token})
print(payment.paymentId, payment.status)  # "COMPLETED" or "PENDING" — confirm with the payment.* webhook

Checkout sessions (hosted or embedded)

Let VEXPay collect the payment details. Redirect the buyer to url, or embed the checkout on your own site with @vexpay/js and the one-time clientSecret.

from vexpay import VexPay

vexpay = VexPay()

session = vexpay.checkout.sessions.create(
    {
        "amountUsd": 25,
        "reference": "order-1042",
        "description": "Pedido #1042",
        "successUrl": "https://shop.example/gracias",
        "metadata": {"orderId": "1042"},
    }
)
print(session.url)  # redirect here, or send session.clientSecret to the browser (never log or store it)

# Later — fulfil only after checking on the server:
current = vexpay.checkout.sessions.retrieve(session.id)
if current.status == "paid":
    print("ship order 1042")

The hosted page lets the buyer pick the currency — bolívares, stablecoins or Colombian pesos — and offers every method your account accepts. Limit it with methods (c2p, vpos, usdt, usdc, cop).

USDT

USDT settles in USDT, into your VEXPay USDT balance (never converted to VES). It must be enabled on your account — otherwise these calls fail with method_not_allowed (403). Pay-ins cost 2.5 % + 0.50 USDT; payouts cost the network fee listed by crypto.networks.list().

from vexpay import VexPay, VexPayError

vexpay = VexPay()

try:
    networks = vexpay.crypto.networks.list({"amountUsdt": "50.00"})
except VexPayError as err:
    if err.code != "method_not_allowed":
        raise
    networks = []  # USDT isn't enabled on this account

if any(n.receiveEnabled for n in networks):
    # Hosted USDT checkout: the buyer picks a network and gets a deposit address.
    session = vexpay.checkout.sessions.create(
        {
            "amountUsd": 25,
            "reference": "order-1042",
            "methods": ["usdt"],
            "successUrl": "https://shop.example/gracias",
        }
    )
    print(session.url)
    print(vexpay.crypto.balance.retrieve())

Static deposit addresses (crypto.deposit_addresses.create({"customerRef": "user_123", "network": "BEP20"})) fire payment.completed with that customerRef on every deposit. Payouts (crypto.payouts.create({... "idempotencyKey": "withdrawal-981"})) debit amount + network fee and end in payout.completed (with txHash) or payout.failed (refunded). USDT payment.completed events add amountUsdt, feeUsdt, network, txHashes and checkoutSession.reference. Don't fulfil when underpaid is True.

USDC

USDC (Polygon and Base) uses the same calls with "currency": "USDC", has its own balance and must be enabled on your account separately. Without currency, every call stays in USDT.

usdc = vexpay.crypto.balance.retrieve({"currency": "USDC"})
balances = vexpay.crypto.balances.list()  # USDT and USDC

vexpay.crypto.payouts.create(
    {
        "currency": "USDC",
        "network": "BASE",
        "address": "0x…",
        "amount": "50.00",
        "idempotencyKey": "withdrawal-982",
    }
)

Responses and events include currency, amount and fee for both coins (amountUsdt / feeUsdt stay on USDT only). Credit your customer in the currency the webhook reports.

Convert VES or COP to USDT

Turn available VES or Colombian pesos into your USDT balance. A quote locks the rate (market USDT rate in that currency plus your spread) for 60 seconds; accepting it debits the source at once (conversion.created) and returns a PENDING conversion. VEXPay then delivers the USDT and sends conversion.completed (or conversion.canceled, with the VES or COP returned). Conversions are on for every account that has USDT enabled (COP also needs COP enabled) — otherwise the calls fail with conversions_not_enabled (403). In test mode they complete immediately. COP amounts are whole pesos.

quote = vexpay.conversions.quotes.create({"sourceAmountVes": "10000.00"})  # or {"targetAmountUsdt": "50.00"}
print(quote.rate, quote.targetAmountUsdt, quote.expiresAt)

conversion = vexpay.conversions.create(
    {"quoteId": quote.id, "reference": "treasury-2026-10-03"},
    idempotency_key="convert-2026-10-03",
)

for c in vexpay.conversions.list({"status": "PENDING"}):
    print(c.id, c.sourceAmount, c.sourceCurrency, "→", c.targetAmountUsdt)
vexpay.conversions.cancel(conversion.id)  # only while PENDING

# Colombian pesos: same flow with sourceCurrency "COP".
cop_quote = vexpay.conversions.quotes.create({"sourceCurrency": "COP", "sourceAmount": "1000000"})

# Auto-convert 50% of each completed COP payment (after the fee) to USDT; 0 turns it off.
vexpay.conversions.settings.update({"autoConvert": {"COP": {"percent": 50}}})

Colombian pesos (COP)

Collect pesos from buyers in Colombia through Bre-B, Nequi or Daviplata. Amounts are whole pesos, payments complete asynchronously (payment.completed / payment.failed), and completed payments go to your COP balance net of the COP fee. COP must be enabled on your account — otherwise the calls fail with method_not_allowed (403).

from vexpay import VexPay

vexpay = VexPay()

# Bre-B: no buyer data. Show next.qrPngBase64 as an image and next.transferKey as text.
breb = vexpay.cop.payments.create(
    {"amountCop": 100000, "channel": "breb", "reference": "order-1042"},
    idempotency_key="order-1042-cop",
)

buyer = {"email": "juan@example.com", "phone": "3001234567", "documentType": "CC", "documentNumber": "1234567890"}

# Nequi: the buyer approves a push in the Nequi app.
vexpay.cop.payments.create({"amountCop": 50000, "channel": "nequi", "buyer": buyer})

# Daviplata: the buyer reads you the SMS code; submit it.
davi = vexpay.cop.payments.create({"amountCop": 50000, "channel": "daviplata", "buyer": buyer})
vexpay.cop.payments.submit_otp(davi.id, {"otp": "123456"})

status = vexpay.cop.payments.retrieve(breb.id).status  # pending | completed | failed | canceled | refunded
available = vexpay.cop.balance.retrieve().availableCop

cancel(id) cancels a pending payment and refund(id) refunds a completed one in full within 96 hours. A webhook for a COP payment has data["method"] == "COP", with channel, amountCop and lowercase status.

Test mode (test API keys use a sandbox):

Channel What to use Result
Bre-B No buyer data QR and key from the sandbox. Never send real money to them — sandbox keys are reachable from real banks and the money is not credited.
Nequi Any valid data, e.g. phone 3001234567, CC 1234567890, any email Completes on its own within about a minute
Daviplata Same buyer data (CC, CE or TI only) OTP 123456, 000000 or 111111 completes it; any other code fails with invalid_otp

Testing card payments

With a test API key, VPOS test cards return a fixed outcome without reaching the bank: 4242424242424242 succeeds, 4000000000009995 is declined with G51 (insufficient funds), 4000000000000259 completes and is then charged back, and more. A decline fails exactly like a live one (422, the bank code in code and failureCode, a payment.failed webhook). With a live key these numbers are refused with 400 test_card_in_live_mode. Full list: test cards.

Webhooks

VEXPay signs every delivery with a VexPay-Signature header. Verify it with the raw request body — parsing and re-serializing JSON changes the bytes and breaks the signature (Flask: request.get_data(), Django: request.body, FastAPI: await request.body()).

import os
from typing import Optional

from vexpay import PaymentEvent, SignatureVerificationError, Webhook


def handle_webhook(raw_body: bytes, signature: Optional[str]) -> int:
    """Framework-agnostic handler: returns the HTTP status to answer with."""
    try:
        event = Webhook.construct_event(raw_body, signature, os.environ["VEXPAY_WEBHOOK_SECRET"])
    except SignatureVerificationError:
        return 400

    if isinstance(event, PaymentEvent) and event.event == "payment.completed":
        data = event.data
        reference = data.externalRef or (data.checkoutSession.reference if data.checkoutSession else None)
        print("paid", data.paymentId, reference)
    return 204

Deliveries older than 5 minutes are rejected as possible replays; pass tolerance= (seconds) to change that. Each delivery also carries a VexPay-Event-Id header you can use to deduplicate. In your own tests, sign fixtures with Webhook.generate_test_header(payload, secret).

Errors

Every error extends VexPayError and carries status, the API code (e.g. insufficient_balance), and the response body.

from vexpay import ConflictError, InvalidRequestError, NotFoundError, VexPay, VexPayError

vexpay = VexPay()

try:
    vexpay.payouts.create(
        {
            "merchantId": "4f0c6f1e-2b7d-4c1a-9e3f-5a6b7c8d9e0f",
            "monto": "1523.40",
            "concepto": "Liquidación semana 30",
            "externalRef": "po_2026_30_maria",
        }
    )
except InvalidRequestError as error:
    print("fix the request:", error.code, error.message)
except NotFoundError:
    print("no such merchant")
except ConflictError as error:
    print("externalRef reused with a different payload", error.body)
except VexPayError as error:
    print(error.status, error.code, error.message)
Class When
AuthenticationError 401 — missing or invalid API key
InvalidRequestError 400, 403, 410, 422 — fix the request
NotFoundError 404
ConflictError 409 — e.g. external_ref_conflict, idempotency_key_reused
RateLimitError 429, after retries
APIError 5xx, after retries
APIConnectionError / APITimeoutError no HTTP answer, after retries
SignatureVerificationError a webhook did not verify

Retries and idempotency

The SDK retries connection errors, timeouts, 429, 5xx, and 409 idempotency_request_in_progress up to max_network_retries times (default 2) with exponential backoff, honouring Retry-After. Every POST gets a random Idempotency-Key that stays the same across its retries. Supply your own to make retries safe across process restarts too:

from vexpay import VexPay

vexpay = VexPay(max_network_retries=3, timeout=20.0)

quote = vexpay.quotes.retrieve({"usdAmount": 10}, timeout=5.0)
session = vexpay.checkout.sessions.create(
    {"amountUsd": 10, "reference": "order-1043"},
    idempotency_key="order-1043-session",
)
print(quote.vesAmount, session.id)

Pagination

list() on payouts, merchants, and products returns the first page; iterate it to walk every item across pages:

from vexpay import VexPay

vexpay = VexPay()

page = vexpay.merchants.list({"limit": 100})
print(len(page.items), page.next_cursor)

for merchant in vexpay.merchants.list({"status": "verified", "isActive": "true"}):
    print(merchant.merchantId, merchant.name)

recent = vexpay.payouts.list({"limit": 50}).to_list(limit=200)
print(len(recent))

Async

AsyncVexPay has the same resources; await the calls, and use async for to paginate:

import asyncio

from vexpay import AsyncVexPay


async def main() -> None:
    async with AsyncVexPay() as vexpay:
        banks = await vexpay.banks.list()
        print(len(banks), "banks")

        first_page = await vexpay.products.list({"limit": 10})
        print(len(first_page.items))

        async for payout in vexpay.payouts.list():
            print(payout.payoutId, payout.status)


asyncio.run(main())

Configuration

from vexpay import VexPay

with VexPay(
    api_key="sk_test_…",               # default: VEXPAY_API_KEY env var
    base_url="http://localhost:3010",  # default: https://api.pay.vexwallet.co
    timeout=30.0,
    max_network_retries=2,
) as vexpay:
    print(len(vexpay.banks.list()), "banks")

Pass http_client=httpx.Client(...) (or httpx.AsyncClient) to control proxies, connection pooling, or instrumentation.

Resources

Namespace Methods
banks list
quotes retrieve
balance retrieve
balance.transactions list (auto-paginating; every movement in your VES balance — reconcile with ledgerNetVes)
payments retrieve, retrieve_by_ref, reverse
payments.c2p request, execute
payments.vpos create
payments.pago_movil verify
payments.debit · credit · operations · dispersals · change débito/crédito inmediato and disbursements (advanced payments)
checkout.sessions create, retrieve
crypto.balance retrieve
crypto.balances list
crypto.deposit_addresses create
crypto.networks list
crypto.payouts create, retrieve
conversions create, list, retrieve, cancel
conversions.quotes create
conversions.settings retrieve, update
cop.payments create, retrieve, submit_otp, cancel, refund
cop.balance retrieve
merchants create, list, retrieve, retrieve_by_ref, update, delete, retrieve_balance, transfer, list_audit_events, start_verification, confirm_verification
merchants.payout_methods list, create, set_default, delete, start_verification, confirm_verification
payouts create, create_instant, create_batch, list, retrieve, retrieve_by_ref
products create, list, retrieve, update, delete, create_link, list_links
payment_links retrieve, update, delete
tenant_payout_account retrieve, upsert, create, start_verification, confirm_verification
webhook_endpoints create, list, update, delete, send_test
Webhook / client.webhooks construct_event, generate_test_header (offline)

License

MIT

Metadata

Release files for vexpay 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vexpay 0.7.0
File Size Uploaded
vexpay-0.7.0.tar.gz 41.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vexpay 0.7.0
File Interpreter ABI Platform
vexpay-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 88.2 kB

Release files / vexpay-0.7.0.tar.gz

Download URL vexpay-0.7.0.tar.gz
Size 41.7 kB
Tags Source
SHA-256 checksum
How to use checksums
ac63226aeb8e0f29a6de5dd0740efa18411e92a45b5bbd7828ff04db1e074b2b
BLAKE2b-256 checksum
How to use checksums
052ab080ea862dab188502e02023fb0baf868a25c63866b16b3f4e00afc948e2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / vexpay-0.7.0-py3-none-any.whl

Download URL vexpay-0.7.0-py3-none-any.whl
Size 46.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
99fadb07e9e52aae45491f4a1911a2e55e8640d3e9bbba88a81476ce1f8c2212
BLAKE2b-256 checksum
How to use checksums
f762c757feab200b3e71aebb76fcbdef381173d025c2b566052cb6ce5e1619b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.0

2 release files

0.8.0

2 release files

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page