- Python 100%
| caishenpay | ||
| tests | ||
| caishenpay.py | ||
| caishenpay_wsgi.py | ||
| config.yaml | ||
| config.yaml.example | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
Caishen Pay
Python crypto payment gateway for processing XMR transactions.
Currently only supports single merchant private use as the signing encryption key is global. Multi-tenant support is in the works.
Requirements
- monero wallet generated
- monero-wallet-rpc
- Python 3.9+
- gunicorn
config.yaml(copyconfig.yaml.example)CAISHENPAY_API_KEYandCAISHENPAY_ENCRYPTION_SECRETin the environment
Usage
gunicorn --workers 4 --bind 0.0.0.0:8000 caishenpay_wsgi:app
Invoice status polling background task starts inside create_app(). Every gunicorn worker tries to start it but the FileLock (<tempdir>/caishenpay-ispw.lock) lets only a single worker run the poller at a time. If that worker dies, the arbiter spawns a replacement that runs create_app() again and takes the lock. If no worker holds the lock, invoices stay pending.
Do not use preload_app as the lock must be acquired after fork in each worker.
Authentication
All /v1 routes require HTTP Bearer auth. Send the value of CAISHENPAY_API_KEY:
Authorization: Bearer <CAISHENPAY_API_KEY>
The scheme name is case-insensitive (Bearer, bearer, BEARER). The token is compared exactly; extra spaces around the key are not stripped. Missing header, wrong scheme, or wrong key all return the same 401:
{
"error": "Unauthorized"
}
Example Usage
1. Create Invoice (POST /v1/invoices)
Request (Success)
curl -X POST http://localhost:8000/v1/invoices \
-H "Authorization: Bearer $CAISHENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "0.150000000000",
"currency": "XMR",
"external_id": "order-10023",
"callback_url": "https://merchant.example.com/api/webhook"
}'
Response (201 Created)
{
"invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
"external_id": "order-10023",
"deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
"deposit_amount": "0.150000000000",
"deposit_currency": "XMR",
"seen_amount": "0",
"status": "PENDING",
"created_at": "2026-08-16T12:00:00Z",
"expires_at": "2026-08-16T13:00:00Z",
"finalized_at": null,
"callback_url": "https://merchant.example.com/api/webhook"
}
Request (Invalid Payload / Validation Error)
curl -X POST http://localhost:8000/v1/invoices \
-H "Authorization: Bearer $CAISHENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "-1.0",
"currency": "INVALID"
}'
Response (400 Bad Request)
{
"errors": {
"amount": [
"Must be greater than 0."
],
"currency": [
"Must be one of: XMR."
]
}
}
Response (503 Service Unavailable)
{
"error": "Wallet backend unavailable"
}
2. Lookup Single Invoice by ID Path (GET /v1/invoices/<invoice_id>)
Request (Success)
curl "http://localhost:8000/v1/invoices/9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (200 OK)
{
"invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
"external_id": "order-10023",
"deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
"deposit_amount": "0.150000000000",
"deposit_currency": "XMR",
"seen_amount": "0.150000000000",
"status": "COMPLETED",
"created_at": "2026-08-16T12:00:00Z",
"expires_at": "2026-08-16T13:00:00Z",
"finalized_at": "2026-08-16T12:15:30Z",
"callback_url": "https://merchant.example.com/api/webhook"
}
Request (Invoice Not Found)
curl "http://localhost:8000/v1/invoices/nonexistent-id" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (404 Not Found)
{
"error": "Invoice not found"
}
3. Query Invoices or List All (GET /v1/invoices)
Request (List All Invoices)
curl "http://localhost:8000/v1/invoices" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (200 OK)
{
"invoices": [
{
"invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
"external_id": "order-10023",
"deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
"deposit_amount": "0.150000000000",
"deposit_currency": "XMR",
"seen_amount": "0.150000000000",
"status": "COMPLETED",
"created_at": "2026-08-16T12:00:00Z",
"expires_at": "2026-08-16T13:00:00Z",
"finalized_at": "2026-08-16T12:15:30Z",
"callback_url": "https://merchant.example.com/api/webhook"
}
]
}
Request (Query by invoice_id parameter)
curl "http://localhost:8000/v1/invoices?invoice_id=9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Request (Query by external_id parameter)
curl "http://localhost:8000/v1/invoices?external_id=order-10023" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Request (Conflicting Parameters Error)
curl "http://localhost:8000/v1/invoices?invoice_id=9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d&external_id=order-10023" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (400 Bad Request)
{
"error": "Provide only one of invoice_id or external_id"
}
4. Check Balance (GET /v1/balance)
Request (Total Balance across all configured wallets)
curl "http://localhost:8000/v1/balance" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (200 OK)
{
"balance": "12.450000000000"
}
Request (Balance for specific Currency)
curl "http://localhost:8000/v1/balance?currency=XMR" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (200 OK)
{
"balance": "12.450000000000"
}
Request (Deposit Address Lookup without Currency - Error)
curl "http://localhost:8000/v1/balance?deposit_address=888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (400 Bad Request)
{
"error": "Currency must be provided for address lookups"
}
Request (Unsupported Currency)
curl "http://localhost:8000/v1/balance?currency=DOGE" \
-H "Authorization: Bearer $CAISHENPAY_API_KEY"
Response (400 Bad Request)
{
"error": "Unsupported currency"
}
Callbacks
Pass callback_url when creating an invoice. The poller POSTs JSON to that URL once, when the invoice reaches a final status:
| Status | When |
|---|---|
COMPLETED |
Full deposit_amount confirmed |
EXPIRED |
Payment window closed with nothing seen |
UNDERPAID |
Payment window closed and confirmed amount is still short |
No callback for PENDING, DETECTED, or CONFIRMING. No callback if callback_url was omitted.
The body is the same payload as GET /v1/invoices. Your endpoint must return HTTP 200. Delivery is a single POST; failed deliveries are not retried yet.
Example (POST to callback_url)
{
"invoice_id": "9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d",
"external_id": "order-10023",
"deposit_address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkFxbANsAnJYPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
"deposit_amount": "0.150000000000",
"deposit_currency": "XMR",
"seen_amount": "0.150000000000",
"status": "COMPLETED",
"created_at": "2026-08-16T12:00:00Z",
"expires_at": "2026-08-16T13:00:00Z",
"finalized_at": "2026-08-16T12:15:30Z",
"callback_url": "https://merchant.example.com/api/webhook"
}
AI Usage
Core logic and API design written by me. AI tools were utilized for generating unit tests, acting as a security advisor during the design phase, and scaffolding documentation.