Metadata-Version: 2.4
Name: zyndpay
Version: 1.7.1
Summary: Official ZyndPay Python SDK — accept USDT payments with a few lines of code
Home-page: https://github.com/zyndpay/zyndpay-python
Author: ZyndPay
Author-email: dev@zyndpay.io
Keywords: zyndpay,crypto,usdt,trc20,payment-gateway
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28.0
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# zyndpay

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

[![PyPI version](https://img.shields.io/pypi/v/zyndpay)](https://pypi.org/project/zyndpay/)
[![Python](https://img.shields.io/pypi/pyversions/zyndpay)](https://pypi.org/project/zyndpay/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

---

## Requirements

- Python 3.8+
- A ZyndPay account and API key

---

## Installation

```bash
pip install zyndpay
```

---

## Quickstart

```python
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

```python
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

```python
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

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

### List payins

```python
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%**.

```python
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%**.

```python
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.

```python
# 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

```python
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

```python
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

```python
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

```python
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

```python
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)

```python
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

```python
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

```python
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

```python
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.

```python
# 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.

```python
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

```python
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

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

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

### Cancel a withdrawal

```python
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

```python
# 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

```python
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)

```python
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`.

```python
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](LICENSE)
