Skip to main content

Official ZyndPay Python SDK — accept USDT payments with a few lines of code

Project description

zyndpay

Official ZyndPay Python SDK — accept USDT TRC20 payments with a few lines of code.

PyPI version Python License: MIT


Requirements

  • Python 3.8+
  • A ZyndPay account and API key

Installation

pip install zyndpay

Quickstart

from zyndpay import ZyndPay

zyndpay = ZyndPay("zyp_live_sk_...")

# Create a payment request
payin = zyndpay.payins.create(amount="100")
print(payin["data"]["address"])     # Send USDT TRC20 here
print(payin["data"]["paymentUrl"])  # Redirect your customer here

# Check your balance
balance = zyndpay.balances.get()
print(balance["data"]["balance"])   # e.g. "97.00"

Configuration

zyndpay = ZyndPay(
    api_key="zyp_live_sk_...",              # required
    webhook_secret="whsec_...",             # optional — needed for webhook verification
    base_url="https://api.zyndpay.io/v1",  # optional — override for self-hosted
    timeout=30,                             # optional — seconds (default: 30)
    max_retries=2,                          # optional — retries on network errors (default: 2)
)

API key types

Prefix Type
zyp_live_sk_ Live secret key
zyp_live_pk_ Live publishable key
zyp_test_sk_ Sandbox secret key
zyp_test_pk_ Sandbox publishable key

Payins

Create a payin

payin = zyndpay.payins.create(
    amount="100",                      # USDT amount (minimum 1)
    external_ref="order_9f8e7d",       # your internal order ID (optional)
    expires_in_seconds=3600,           # 1 hour — default is 30min (optional)
    metadata={"user_id": "usr_123"},   # stored as-is (optional)
    success_url="https://yoursite.com/success",
    cancel_url="https://yoursite.com/cancel",
)

print(payin["data"]["transactionId"])  # "uuid"
print(payin["data"]["address"])        # TRC20 deposit address
print(payin["data"]["paymentUrl"])     # hosted payment page URL
print(payin["data"]["qrCodeUrl"])      # QR code data URL
print(payin["data"]["amount"])         # "100"
print(payin["data"]["status"])         # "AWAITING_PAYMENT"
print(payin["data"]["expiresAt"])      # ISO timestamp

Get a payin

payin = zyndpay.payins.get("pay_abc123")

List payins

result = zyndpay.payins.list(status="CONFIRMED", page=1, limit=20)
for payin in result["data"]["items"]:
    print(payin["id"], payin["status"])

print(result["data"]["total"])

Card payments (Visa / Mastercard)

Redirect the customer to a hosted checkout page. Amount is in fiat (XOF). Fee: 5%.

payin = zyndpay.payins.create(
    amount="65000",           # XOF amount
    currency="XOF",
    payment_method="CARD",
    external_ref="order_card_123",
    success_url="https://yoursite.com/success",
    cancel_url="https://yoursite.com/cancel",
)

# Redirect the customer to the hosted checkout
redirect_url = payin["data"]["hostedPaymentUrl"]

Mobile Money payins (Orange BF / Moov BF)

Amount is in fiat (XOF). The customer stays on your page — no redirect. Fee: 3.5%.

payin = zyndpay.payins.create(
    amount="65000",               # XOF amount
    currency="XOF",
    payment_method="MOBILE_MONEY",
    customer_phone="+22670000000",  # E.164 format (required)
    operator_code="ORANGE_BF",      # optional — auto-detected from phone prefix
    external_ref="order_momo_456",
)

data = payin["data"]
if data["nextStep"] == "otp":
    # Prompt the customer for the OTP they received by SMS
    confirmed = zyndpay.payins.submit_otp(data["transactionId"], "123456")
    print(confirmed["status"])  # "CONFIRMING" → "CONFIRMED"
else:
    # nextStep === "wait" — display instruction and wait for webhook
    print(data["instruction"])  # e.g. "Confirm payment in your Orange Money app"

Supported operator_code values

Code Network Country
ORANGE_BF Orange Burkina Faso
MOOV_BF Moov Burkina Faso

Payin statuses

Status Description
PENDING Just created
AWAITING_PAYMENT Deposit address assigned, waiting for funds
CONFIRMING Payment detected, waiting for confirmations
CONFIRMED Payment confirmed — balance credited
EXPIRED Payment window elapsed
OVERPAID More than expected was sent
UNDERPAID Less than expected was sent
FAILED Processing failed

Wallets, Conversions, and FCFA Payouts (multi-wallet API)

The multi-wallet API exposes one balance per (currency, rail) pair — for example a USDT_TRC20 wallet plus an XOF mobile-money wallet.

# 1. List wallets
wallets = zyndpay.wallets.list()
usdt = next(w for w in wallets if w["currency"] == "USDT_TRC20")
xof  = next(w for w in wallets if w["currency"] == "XOF")

# 2. Whitelist an FCFA mobile-money destination
dest = zyndpay.fiat_destinations.create(
    kind="MOMO",
    label="My Orange",
    momo_operator="ORANGE",
    momo_phone="22670000000",
    is_primary=True,
)

# 3. Convert USDT → XOF (synchronous wallet-to-wallet)
zyndpay.conversions.convert_between_wallets(
    from_wallet_id=usdt["id"],
    to_wallet_id=xof["id"],
    from_amount="100",
)

# 4. Pay the FCFA balance out to the whitelisted destination
zyndpay.withdrawals.create(
    amount="60000",
    wallet_id=xof["id"],
    fiat_destination_id=dest["id"],
)

The legacy conversions.create(amount, channel, ...) is deprecated (sunset 2026-07-25). Use the two-step convert_between_wallets + withdrawals.create flow above.


Paylinks

Payment links you can share with customers — fixed-price, variable-price, or recurring.

Create a paylink

paylink = zyndpay.paylinks.create(
    title="Premium Plan",
    type="FIXED",              # "FIXED" | "VARIABLE" | "RECURRING"
    amount="25",               # USDT — omit for VARIABLE
    currency="USD",
    description="Monthly subscription",
    success_url="https://yoursite.com/thank-you",
    cancel_url="https://yoursite.com/cancel",
)

print(paylink["id"])      # "plk_abc123"
print(paylink["url"])     # shareable payment URL
print(paylink["status"])  # "ACTIVE"

Get / list / update / delete

paylink = zyndpay.paylinks.get("plk_abc123")

result = zyndpay.paylinks.list(status="ACTIVE", page=1, limit=20)
for pl in result["items"]:
    print(pl["id"], pl["status"])

zyndpay.paylinks.update("plk_abc123", title="New Title")
zyndpay.paylinks.delete("plk_abc123")

Stats and orders

stats = zyndpay.paylinks.get_stats("plk_abc123")
print(stats["totalRevenue"], stats["orderCount"])

dash = zyndpay.paylinks.get_dashboard_stats()

orders = zyndpay.paylinks.list_orders("plk_abc123", page=1, limit=50)
csv = zyndpay.paylinks.export_orders_csv("plk_abc123")

Promo codes

promo = zyndpay.paylinks.create_promo_code("plk_abc123",
    code="SAVE10", discount_type="PERCENT", discount_value=10, max_uses=100
)
codes = zyndpay.paylinks.list_promo_codes("plk_abc123")
zyndpay.paylinks.toggle_promo_code("plk_abc123", promo["id"], False)
zyndpay.paylinks.delete_promo_code("plk_abc123", promo["id"])

Templates

tpl = zyndpay.paylinks.create_template(name="My Template", config={})
zyndpay.paylinks.save_as_template("plk_abc123", "Saved template")
templates = zyndpay.paylinks.list_templates()
zyndpay.paylinks.delete_template(tpl["id"])

Subscriptions (recurring paylinks)

subs = zyndpay.paylinks.list_subscriptions("plk_abc123")
zyndpay.paylinks.cancel_subscription("plk_abc123", subs[0]["id"])

Payouts

Send USDT directly to an external wallet address.

Estimate fees before submitting

estimate = zyndpay.payouts.estimate(
    amount="200",
    destination_address="TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    currency="USDT_TRC20",
    chain="TRON",
)
print(estimate["fee"])        # network fee in USDT
print(estimate["netAmount"])  # amount recipient receives

Create a payout

payout = zyndpay.payouts.create(
    amount="200",
    destination_address="TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    currency="USDT_TRC20",   # default
    chain="TRON",            # default
    external_ref="payout_order_789",
    metadata={"note": "vendor payment"},
    idempotency_key="idempotency-key-456",
)
print(payout["status"])  # "PENDING" → "BROADCAST" → "CONFIRMED"

Get / list payouts

tx = zyndpay.payouts.get("payout_id")

result = zyndpay.payouts.list(
    status=["CONFIRMED", "BROADCAST"],
    from_date="2026-01-01",
    to_date="2026-04-30",
    page=1,
    limit=50,
)

Bulk Payments

Send to hundreds of addresses in a single batch — draft → validate → execute lifecycle.

# 1. Create a draft batch
batch = zyndpay.bulk_payments.create()

# 2. Add recipients
zyndpay.bulk_payments.add_items(batch["id"], [
    {"destinationAddress": "TXaaa...", "amount": "50", "externalRef": "emp_1"},
    {"destinationAddress": "TXbbb...", "amount": "75", "externalRef": "emp_2"},
])

# Or import from CSV/XLSX
# template = zyndpay.bulk_payments.download_template("csv")
# zyndpay.bulk_payments.import_file(batch["id"], "/path/to/payroll.csv")

# 3. Validate (checks balance, calculates fees)
validated = zyndpay.bulk_payments.validate(batch["id"])
print(validated["totalAmount"], validated["totalFee"])

# 4. Execute
executed = zyndpay.bulk_payments.execute(batch["id"])
print(executed["status"])  # "PROCESSING"

# 5. Monitor
detail = zyndpay.bulk_payments.get(batch["id"])
print(detail["items"])  # per-recipient status

# Retry failed items / cancel
zyndpay.bulk_payments.retry(batch["id"])
zyndpay.bulk_payments.cancel(batch["id"])

# Export results as CSV
csv = zyndpay.bulk_payments.export(batch["id"])

Batch statuses

Status Description
DRAFT Building the batch
VALIDATED Fees calculated, ready to execute
PROCESSING Items being broadcast
COMPLETED All items settled
PARTIALLY_COMPLETED Some items failed
CANCELLED Cancelled before execution

Sandbox / Test Mode

Use your sandbox API key (zyp_test_sk_...) and pass sandbox=True when creating a payin. Then call simulate to instantly confirm it without real funds.

zyndpay = ZyndPay("zyp_test_sk_...")

# Create a sandbox payin
payin = zyndpay.payins.create(amount="100", sandbox=True)

# Instantly simulate confirmation
confirmed = zyndpay.payins.simulate(payin["data"]["transactionId"])
print(confirmed["status"])  # "CONFIRMED"

Withdrawals

Request a withdrawal

withdrawal = zyndpay.withdrawals.create(
    amount="50",                          # USDT amount
    idempotency_key="idempotency-key-123" # optional
)

print(withdrawal["status"])     # "PENDING_REVIEW"
print(withdrawal["fee"])        # "1.50" (1% fee, min $1.50)
print(withdrawal["netAmount"])  # "48.50"

Get / list withdrawals

withdrawal = zyndpay.withdrawals.get("wdr_abc123")

result = zyndpay.withdrawals.list(status="CONFIRMED", page=1, limit=20)

Cancel a withdrawal

zyndpay.withdrawals.cancel("wdr_abc123")  # only while PENDING_REVIEW

Withdrawal statuses

Status Description
PENDING_REVIEW Awaiting admin approval
APPROVED Approved, queued for processing
PROCESSING Being broadcast to blockchain
BROADCAST Transaction sent
CONFIRMED On-chain confirmed
REJECTED Rejected by admin
CANCELLED Cancelled by merchant
FAILED Broadcast failed

Transactions

# Get a single transaction
tx = zyndpay.transactions.get("txn_abc123")

# List with filters
result = zyndpay.transactions.list(
    type="PAYIN",           # "PAYIN" | "PAYOUT"
    status="CONFIRMED",
    from_date="2026-01-01",
    to_date="2026-03-31",
    page=1,
    limit=50,
)

Balances

balance = zyndpay.balances.get()
print(balance["data"]["currency"])  # "USDT_TRC20"
print(balance["data"]["balance"])   # current balance

Webhooks

ZyndPay sends signed webhook events to your endpoint. Always verify the signature before processing.

Verify a webhook (Flask example)

from flask import Flask, request, abort
from zyndpay import ZyndPay

zyndpay = ZyndPay(
    api_key="zyp_live_sk_...",
    webhook_secret="whsec_...",
)

app = Flask(__name__)

@app.route("/webhooks/zyndpay", methods=["POST"])
def handle_webhook():
    # IMPORTANT: use raw body — do not parse JSON before verifying
    payload = request.get_data(as_text=True)
    signature = request.headers.get("X-ZyndPay-Signature", "")

    try:
        event = zyndpay.webhooks.verify(payload, signature)
    except ValueError as e:
        return str(e), 400

    # All payin events include: transactionId, status, currency, chain, externalRef
    if event["event"] == "payin.confirmed":
        # Also has: amount, amountRequested, txHash, confirmedAt
        print("Payment confirmed:", event["data"]["externalRef"], event["data"]["amount"])
    elif event["event"] == "payin.expired":
        print("Payment expired:", event["data"]["externalRef"])
    elif event["event"] == "withdrawal.confirmed":
        print("Withdrawal confirmed:", event["data"])

    return {"received": True}, 200

Webhook event types

Event Trigger
payin.created Payin created
payin.confirming Payment detected on-chain
payin.confirmed Payment fully confirmed
payin.expired Payin expired before payment
payin.overpaid More than expected received
payin.underpaid Less than expected received
payin.failed Processing error
payout.created Payout created
payout.broadcast Sent to blockchain
payout.confirmed On-chain confirmed
payout.failed Processing failed
withdrawal.requested Withdrawal created
withdrawal.approved Approved by admin
withdrawal.rejected Rejected by admin
withdrawal.broadcast Sent to blockchain
withdrawal.confirmed On-chain confirmed
withdrawal.failed Broadcast failed
merchant.kyb_approved KYB review approved
merchant.kyb_rejected KYB review rejected
merchant.live_activated Account activated to live
merchant.suspended Account suspended
api_key.rotated API key rotated
api_key.revoked API key revoked
balance.low_threshold Balance fell below threshold
bulk_batch.completed Bulk batch fully settled
bulk_batch.partially_completed Some bulk items failed
bulk_batch.failed Bulk batch failed

Error Handling

All SDK errors inherit from ZyndPayError and include status_code and an optional request_id.

from zyndpay import (
    ZyndPayError,
    AuthenticationError,
    ValidationError,
    NotFoundError,
    ConflictError,
    RateLimitError,
)

try:
    payin = zyndpay.payins.create(amount="5")  # below minimum
except ValidationError as e:
    print("Bad request:", e)          # "amount must be >= 25"
    print("Status code:", e.status_code)  # 400
except AuthenticationError:
    print("Invalid API key")
except NotFoundError:
    print("Resource not found")
except ConflictError as e:
    print("Conflict:", e)
except RateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")
except ZyndPayError as e:
    print(f"API error {e.status_code}: {e}")

License

MIT — see LICENSE

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zyndpay-1.7.5.tar.gz (42.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zyndpay-1.7.5-py3-none-any.whl (31.2 kB view details)

Uploaded Python 3

File details

Details for the file zyndpay-1.7.5.tar.gz.

File metadata

  • Download URL: zyndpay-1.7.5.tar.gz
  • Upload date:
  • Size: 42.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for zyndpay-1.7.5.tar.gz
Algorithm Hash digest
SHA256 60e033e355284ab3e8d9a4489155c65c825c946b334c0900230186f52efc90dc
MD5 0c97499ef228b0a8ae31a6af260db602
BLAKE2b-256 bdc31e32b7977c227f7237fc95893fadaaf111bec286a0cd1ae0194cc5d53868

See more details on using hashes here.

File details

Details for the file zyndpay-1.7.5-py3-none-any.whl.

File metadata

  • Download URL: zyndpay-1.7.5-py3-none-any.whl
  • Upload date:
  • Size: 31.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for zyndpay-1.7.5-py3-none-any.whl
Algorithm Hash digest
SHA256 a7440f4e37123ff12a8ec4db6df500e863c2c6bc5ffae109187cc5ea0f9eb500
MD5 03a313a18469592833de80e0ebb3251e
BLAKE2b-256 c5583e28968ec59121aed80edd81cac9da613687d02df22bf08f171d5b613cf2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page