Python SDK for the Paychainly crypto payment platform
Project description
paychainly
Python SDK for the Paychainly crypto payment platform. Accept USDT on BNB Smart Chain — manage customers, deposit addresses, payment links, invoices, and withdrawals without writing raw HTTP calls.
Table of contents
- Installation
- Setup
- Quick start
- Pattern 1 — Wallet / deposit system
- Pattern 2 — Wallet + payment link
- Pattern 3 — Order checkout (logged-in user)
- Pattern 4 — Guest checkout (no account)
- Receiving payments
- Error handling
- Async usage
- API reference
Installation
pip install paychainly
Requires Python 3.10+.
Setup
Create one client instance and reuse it across your app.
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 |
| User wallet with fixed-amount hosted page | Pattern 2 — Wallet + payment link |
| E-commerce order / SaaS subscription (logged-in) | Pattern 3 — Order checkout (logged-in) |
| One-off payment, no account needed | Pattern 4 — Guest checkout |
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.
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:
# 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.
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
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.
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.
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:
# 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:
# 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.
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. 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):
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):
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:
# 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)
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
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.
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paychainly-1.0.1.tar.gz.
File metadata
- Download URL: paychainly-1.0.1.tar.gz
- Upload date:
- Size: 18.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff617644cd02784987ec336a19056aad1a59ceb21b5124cb9b957966b1eec69b
|
|
| MD5 |
a913f5d704f34c07ce1ad0e05f680076
|
|
| BLAKE2b-256 |
6c73580d9d45cc6b3d2afc945f9624398eb4614550760b2a798ee2e7da309b46
|
File details
Details for the file paychainly-1.0.1-py3-none-any.whl.
File metadata
- Download URL: paychainly-1.0.1-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d46af099155161bc88d89ab602ede5a47b932a3f3f41f9be6bfbf5d39f36f36
|
|
| MD5 |
cb8f78f1165206d75d7c8dc08cb7f612
|
|
| BLAKE2b-256 |
ed94ff3fe6b155cfd25c65cbf8c10110147b5a9b5834f409c1b32e4212db04f1
|