Metadata-Version: 2.4
Name: paychainly
Version: 1.0.1
Summary: Python SDK for the Paychainly crypto payment platform
Project-URL: Homepage, https://paychainly.com
Project-URL: Repository, https://github.com/paychainly/python-sdk
Project-URL: Issues, https://github.com/paychainly/python-sdk/issues
Author-email: Paychainly <hello@paychainly.com>
License: MIT
Keywords: bnb,crypto,paychainly,payments,sdk,usdt
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# paychainly

Python SDK for the [Paychainly](https://paychainly.com) crypto payment platform. Accept USDT on BNB Smart Chain — manage customers, deposit addresses, payment links, invoices, and withdrawals without writing raw HTTP calls.

[![PyPI version](https://img.shields.io/pypi/v/paychainly)](https://pypi.org/project/paychainly/)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![Network](https://img.shields.io/badge/network-BNB%20Smart%20Chain-yellow)](https://www.bnbchain.org/)

---

## Table of contents

- [Installation](#installation)
- [Setup](#setup)
- [Quick start](#quick-start)
- [Pattern 1 — Wallet / deposit system](#pattern-1--wallet--deposit-system)
- [Pattern 2 — Wallet + payment link](#pattern-2--wallet--payment-link)
- [Pattern 3 — Order checkout (logged-in user)](#pattern-3--order-checkout-logged-in-user)
- [Pattern 4 — Guest checkout (no account)](#pattern-4--guest-checkout-no-account)
- [Receiving payments](#receiving-payments)
- [Error handling](#error-handling)
- [Async usage](#async-usage)
- [API reference](#api-reference)

---

## Installation

```bash
pip install paychainly
```

Requires Python 3.10+.

---

## Setup

Create one client instance and reuse it across your app.

```python
from paychainly import Paychainly

client = Paychainly(
    api_key="pk_live_...",                   # required — from your dashboard
    base_url="https://api.paychainly.com",  # default, optional
    timeout=30.0,                            # seconds, default 30
    retries=3,                               # retry on 5xx/network errors
    retry_delay=0.5,                         # base delay in seconds (exponential backoff)
)
```

---

## Quick start

Not sure which pattern to use? Pick based on your use case:

| Use case | Pattern |
|---|---|
| User wallet / balance top-up (address only) | [Pattern 1 — Wallet](#pattern-1--wallet--deposit-system) |
| User wallet with fixed-amount hosted page | [Pattern 2 — Wallet + payment link](#pattern-2--wallet--payment-link) |
| E-commerce order / SaaS subscription (logged-in) | [Pattern 3 — Order checkout (logged-in)](#pattern-3--order-checkout-logged-in-user) |
| One-off payment, no account needed | [Pattern 4 — Guest checkout](#pattern-4--guest-checkout-no-account) |

---

## Pattern 1 — Wallet / deposit system

Each user gets one **permanent deposit address**. They send USDT to it — you credit their balance. Best for top-up flows, wallets, and balance-based systems.

### Step 1 — Get or create the customer

`customers.create()` raises `ApiError` with status `409` if the customer already exists. Always wrap it with a fallback to `get_by_identifier()` so it is safe to call on every login or page load.

```python
from paychainly import Paychainly, ApiError

client = Paychainly(api_key="pk_live_...")

def get_or_create_customer(user_id: str, email: str = None, name: str = None):
    """Returns existing customer or creates one. Safe to call on every login."""
    try:
        return client.customers.create(
            identifier=user_id,
            email=email,
            name=name,
        )
    except ApiError as err:
        if err.status == 409:
            return client.customers.get_by_identifier(user_id)
        raise
```

The returned `Customer` object:

```python
# Customer(
#   id=42,
#   customer_uid="dd8693ec-8b5f-43ef-b4e5-1d0a088df1a3",  # Paychainly UUID
#   identifier="user_abc123",                               # your user ID
#   email="john@example.com",
#   name="John Doe",
#   created_at="2026-06-02T10:00:00.000Z",
#   updated_at="2026-06-02T10:00:00.000Z",
# )
```

### Step 2 — Get the wallet address

`mode="reuse"` returns the **same address every time** for this customer — call it as many times as you want.

```python
def get_wallet_address(user_id: str):
    wallet = client.addresses.generate(
        token_symbol="USDT",
        network="BNB",
        mode="reuse",
        customer={"identifier": user_id},
    )
    return {
        "address":      wallet.address,       # "0xABC..." — show this to the user
        "network":      wallet.network,       # "BNB"
        "token_symbol": wallet.token_symbol,  # "USDT"
    }
```

### Step 3 — Put it together

```python
def setup_user_wallet(user_id: str, email: str = None, name: str = None):
    customer = get_or_create_customer(user_id, email, name)
    wallet   = get_wallet_address(user_id)
    return {
        "customer_id":  customer.id,
        "customer_uid": customer.customer_uid,
        "address":      wallet["address"],
        "network":      wallet["network"],
        "token_symbol": wallet["token_symbol"],
    }

# Call this whenever the user opens their wallet page
wallet = setup_user_wallet("user_abc123", email="john@example.com")
print("Deposit address:", wallet["address"])
# → "Send USDT (BEP-20) to 0xABC... on BNB Smart Chain"
```

### Step 4 — How users send USDT

After showing `wallet["address"]` to your user they have three options:

- **Copy and paste** — show the address as text with a copy button; user opens any wallet app and pastes it
- **QR code** — encode `wallet["address"]` as a QR code for mobile wallets
- **Connect wallet** — use MetaMask / WalletConnect on the frontend to trigger a direct USDT transfer

---

## Pattern 2 — Wallet + payment link

Same permanent address as Pattern 1, but you also create a **hosted payment page** for each top-up — useful when you want a countdown timer, QR page, or a fixed-amount checkout experience.

```python
def create_wallet_topup(user_id: str, amount: str, order_id: str):
    # 1. Ensure the permanent wallet address exists
    wallet = client.addresses.generate(
        token_symbol="USDT",
        network="BNB",
        mode="reuse",
        customer={"identifier": user_id},
    )

    # 2. Create a payment link tied to that address
    link = client.payment_links.create_for_address(
        wallet.address,
        amount=amount,                          # e.g. "50.00"
        memo=f"Top-up #{order_id}",             # shown on the checkout page
        expiry_hours=24,
        metadata={"order_id": order_id, "user_id": user_id},
    )

    return {
        "address":    wallet.address,
        "pay_url":    link.pay_url,             # hosted page with QR + timer
        "expires_at": link.expires_at,
    }

topup = create_wallet_topup("user_abc123", amount="50.00", order_id="topup_001")
# Redirect user to topup["pay_url"] or show topup["address"] yourself
```

---

## Pattern 3 — Order checkout (logged-in user)

Each order gets its own checkout: a **fresh deposit address + hosted payment page**, linked to the logged-in customer. `payment_links.create()` does everything in one call.

```python
def create_checkout(order_id: str, amount: str, user_id: str,
                    memo: str = None, note: str = None, metadata: dict = None):
    link = client.payment_links.create(
        unique_id=order_id,          # idempotency key — safe to retry with the same order_id
        token_symbol="USDT",
        network="BNB",
        amount=amount,               # e.g. "49.99" — omit for open-amount
        memo=memo or f"Order #{order_id}",
        note=note,
        expiry_hours=24,
        metadata={"order_id": order_id, **(metadata or {})},
        customer={"identifier": user_id},
    )
    return link

link = create_checkout(
    order_id="order_001",
    amount="49.99",
    memo="Premium Plan",
    user_id="user_abc123",
)
```

The returned `PaymentLink` object:

```python
# PaymentLink(
#   id=77,
#   slug="abc123xyz",
#   unique_id="order_001",
#   address="0xDEPOSIT...",
#   pay_url="https://paychainly.com/pay/abc123xyz",
#   amount="49.99",
#   token_symbol="USDT",
#   network="BNB",
#   status="active",
#   expires_at="2026-06-03T10:00:00.000Z",
#   metadata={"order_id": "order_001"},
# )
```

You have three options from the same response — pick one:

```python
# Option 1 — redirect to Paychainly's hosted checkout page
#   Includes QR code, countdown timer, MetaMask connect, and payment confirmation
redirect_url = link.pay_url
# → https://paychainly.com/pay/abc123xyz

# Option 2 — show in your own custom UI
print(f"Send {link.amount} {link.token_symbol} to: {link.address}")
print(f"Expires at: {link.expires_at}")

# Option 3 — QR-encode link.address yourself (any QR library works)
```

---

## Pattern 4 — Guest checkout (no account)

Identical to Pattern 3 but no `customer` field — no customer record is created.

```python
def create_guest_checkout(order_id: str, amount: str, memo: str = None):
    link = client.payment_links.create(
        unique_id=order_id,
        token_symbol="USDT",
        network="BNB",
        amount=amount,
        memo=memo or f"Order #{order_id}",
        expiry_hours=24,
        # no customer= field
    )
    return link

link = create_guest_checkout(order_id="order_002", amount="29.99")

# Redirect guest to the hosted checkout page
redirect_url = link.pay_url
# → https://paychainly.com/pay/xyz456abc
```

---

## Receiving payments

### Webhooks (recommended)

Set your webhook URL in the [dashboard](https://paychainly.com/dashboard). Paychainly calls your endpoint the moment USDT arrives.

> **Important:** Read the **raw request body** before JSON-parsing. Signature verification requires the unmodified bytes.

**Flask — wallet pattern (credit user balance):**

```python
from flask import Flask, request, abort
from paychainly import Webhooks, WebhookSignatureError

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_..."

@app.post("/webhooks/paychainly")
def paychainly_webhook():
    raw_body  = request.get_data()          # raw bytes — do NOT call request.json first
    signature = request.headers.get("X-Paychainly-Signature", "")

    try:
        event = Webhooks.verify(raw_body, signature, WEBHOOK_SECRET)
    except WebhookSignatureError:
        abort(400)

    if event.event == "deposit_detected":
        customer = client.customers.get_by_deposit_address(event.to_address)
        credit_user_balance(customer.identifier, event.amount)
        print(f"+{event.amount} USDT credited to {customer.identifier}")
        print(f"TX: {event.tx_hash} — block {event.block_number}")

    return "", 200   # always 200 — prevents retries
```

**FastAPI — checkout pattern (fulfil an order):**

```python
from fastapi import FastAPI, Request, HTTPException
from paychainly import Webhooks, WebhookSignatureError

app = FastAPI()
WEBHOOK_SECRET = "whsec_..."

@app.post("/webhooks/paychainly")
async def paychainly_webhook(request: Request):
    raw_body  = await request.body()
    signature = request.headers.get("x-paychainly-signature", "")

    try:
        event = Webhooks.verify(raw_body, signature, WEBHOOK_SECRET)
    except WebhookSignatureError:
        raise HTTPException(status_code=400)

    if event.event == "deposit_detected":
        links = client.payment_links.get_by_address(event.to_address)
        if links:
            order_id = links[0].metadata.get("order_id")
            await fulfill_order(order_id, event.amount, event.tx_hash)

    return {"status": "ok"}
```

The webhook event object:

```python
# PaychainlyEvent(
#   event="deposit_detected",
#   tx_hash="0xabc123...",
#   from_address="0xUSER_WALLET...",   # who sent the USDT
#   to_address="0xDEPOSIT...",         # your user's deposit address
#   amount="50.00",
#   block_number=48123456,
#   timestamp=1748862000,
#   user_id="user_abc123",             # identifier you set on the customer
# )
```

### Polling (development only)

```python
def check_for_deposit(address: str):
    result = client.transactions.list_by_address(address, limit=5)
    if result.total > 0:
        tx = result.data[0]
        print(f"{tx.amount} USDT — {tx.status} — {tx.tx_hash}")
        return tx
    return None
```

---

## Error handling

```python
from paychainly import ApiError

try:
    client.customers.create(identifier="user_123")
except ApiError as err:
    print(err.status)   # HTTP status code, e.g. 409
    print(err.code)     # machine-readable code, e.g. "DUPLICATE_IDENTIFIER"
    print(err.message)  # human-readable description
```

| Status | Code | Cause |
|---|---|---|
| `409` | `DUPLICATE_IDENTIFIER` | Customer already exists. Use `get_by_identifier()` instead. |
| `400` | `INVALID_SIGNATURE` | Webhook signature mismatch. Check your webhook secret. |
| `401` | — | Missing or invalid API key. |
| `404` | — | Resource not found. |

---

## Async usage

Every method has an async equivalent. Use `AsyncPaychainly` in FastAPI, async Django, or any asyncio app.

```python
import asyncio
from paychainly import AsyncPaychainly, ApiError

async def main():
    async with AsyncPaychainly(api_key="pk_live_...") as client:

        # Pattern 1 — wallet
        try:
            customer = await client.customers.create(identifier="user_abc123")
        except ApiError as err:
            if err.status == 409:
                customer = await client.customers.get_by_identifier("user_abc123")
            else:
                raise

        wallet = await client.addresses.generate(
            token_symbol="USDT",
            network="BNB",
            mode="reuse",
            customer={"identifier": customer.identifier},
        )
        print("Wallet address:", wallet.address)

        # Pattern 3 — order checkout (logged-in)
        link = await client.payment_links.create(
            unique_id="order_001",
            token_symbol="USDT",
            network="BNB",
            amount="49.99",
            customer={"identifier": customer.identifier},
        )
        print("Pay URL:", link.pay_url)

asyncio.run(main())
```

---

## Pattern comparison

| # | Pattern | Customer | Address mode | Payment link | Best for |
|---|---|---|---|---|---|
| 1 | Wallet — address only | Required | `reuse` — permanent | Not needed | Open-ended deposits, balance top-ups |
| 2 | Wallet — with payment link | Required | `reuse` — same address | `create_for_address()` per top-up | Fixed-amount top-ups with hosted page |
| 3 | Order checkout — logged-in user | Required | `generate_new` per order | `payment_links.create()` per order | E-commerce, invoices, subscriptions |
| 4 | Guest checkout — no account | Not needed | `generate_new` per order | `payment_links.create()` per order | One-off payments, anonymous checkout |

---

## API reference

### `client.customers`

| Method | Description |
|---|---|
| `create(identifier, ...)` | Create a new customer. Raises `ApiError(409)` if identifier exists. |
| `get(id)` | Get customer by Paychainly numeric ID. |
| `get_by_identifier(identifier)` | Get customer by your own user ID. |
| `get_by_email(email)` | Get customer by email address. |
| `get_by_uid(customer_uid)` | Get customer by Paychainly UUID. |
| `get_by_deposit_address(address)` | Look up which customer owns a deposit address. |
| `list(...)` | List customers with optional pagination. |
| `list_all(...)` | Fetch all customers across pages automatically. |
| `update_by_identifier(identifier, ...)` | Update customer fields by your user ID. |
| `update_by_email(email, ...)` | Update customer fields by email. |

### `client.addresses`

| Method | Description |
|---|---|
| `generate(token_symbol, network, ...)` | Generate a deposit address. Use `mode="reuse"` for permanent wallets. |
| `get(id)` | Get address by numeric ID. |
| `get_by_address(address)` | Get address record by on-chain address string. |
| `list(...)` | List all deposit addresses. |
| `list_all(...)` | Fetch all addresses across pages automatically. |
| `revoke(id)` | Revoke a deposit address by ID. |
| `revoke_by_address(address)` | Revoke a deposit address by its on-chain address string. |

### `client.payment_links`

| Method | Description |
|---|---|
| `create(unique_id, token_symbol, network, ...)` | Create a payment link with a fresh deposit address and hosted page. |
| `get(id)` | Get payment link by numeric ID. |
| `get_by_slug(slug)` | Get payment link by URL slug. |
| `get_by_unique_id(unique_id)` | Get payment link by your order/reference ID. |
| `get_by_address(address)` | Get payment links for a deposit address. |
| `create_for_address(address, ...)` | Create a payment link for an existing deposit address (Pattern 2). |
| `list(...)` | List all payment links. |
| `list_all(...)` | Fetch all payment links across pages automatically. |

### `client.transactions`

| Method | Description |
|---|---|
| `get(id)` | Get transaction by numeric ID. |
| `get_by_hash(tx_hash)` | Get transaction by on-chain hash. |
| `list(...)` | List all transactions with optional filters. |
| `list_all(...)` | Fetch all transactions across pages automatically. |
| `list_by_address(address, ...)` | List transactions for a specific deposit address. |

### `client.withdrawals`

| Method | Description |
|---|---|
| `create(idempotency_key, network, to_address, amount, fee_mode, ...)` | Initiate a USDT withdrawal. |
| `get(id)` | Get withdrawal by numeric ID. |
| `list(...)` | List all withdrawals. |
| `list_all(...)` | Fetch all withdrawals across pages automatically. |
| `list_by_address(address, ...)` | List withdrawals for a specific address. |
| `cancel(id)` | Cancel a pending withdrawal. |

### `client.invoices`

| Method | Description |
|---|---|
| `get(tx_id_or_hash)` | Get invoice by transaction ID or hash (auto-detected). |
| `get_by_id(id)` | Get invoice by numeric transaction ID. |
| `get_by_hash(tx_hash)` | Get invoice by on-chain transaction hash. |

### `client.sandbox`

| Method | Description |
|---|---|
| `credit(address, amount)` | Simulate a USDT deposit in sandbox mode. Triggers the full webhook + sweep flow. |

### `client.system`

| Method | Description |
|---|---|
| `health()` | Check API health status (DB, RPC, gas wallet). |

### `Webhooks` (static class)

| Method | Description |
|---|---|
| `Webhooks.verify(raw_body, signature, secret)` | Verify a webhook signature. Raises `WebhookSignatureError` if invalid. Returns `PaychainlyEvent`. |
| `Webhooks.sign(payload, secret)` | Generate an HMAC-SHA256 signature for a payload dict. Useful for testing. |

---

## Links

- [Dashboard](https://paychainly.com/dashboard)
- [API documentation](https://paychainly.com/docs)
- [Node.js SDK](https://www.npmjs.com/package/@paychainly/sdk)
- [MCP server — use Paychainly from Claude Desktop](https://mcp.paychainly.com)
- [PyPI package](https://pypi.org/project/paychainly/)
