crypto-processing-client
Python client for crypto-processing-api — per-user custodial BTC and USDT balances on top of your own BTCPay Server.
This is the client. The service it talks to is self-hosted; there is no hosted API to sign up for.
pip install crypto-processing-client
Five minutes
from crypto_processing_client import CryptoProcessingClient
client = CryptoProcessingClient("https://pay.example.com", api_key="cpk_live_...")
deposit = client.create_deposit(external_user_id="user-42", asset="BTC")
print(deposit.address, deposit.checkout_link)
# ... the user pays, and you poll or wait for a webhook ...
balances = client.get_user_balances("user-42")
for balance in balances.balances:
print(balance.asset, balance.available)
withdrawal = client.create_withdrawal(
external_user_id="user-42",
asset="BTC",
amount="25000000", # gross, integer smallest units, a string
destination_address="bc1q...",
)
print(withdrawal.status) # pending_approval, or already moving
Every amount is a string, in both directions. amount and
expected_amount are integer numbers of the asset's smallest unit
("25000000" is 0.25 BTC). Everything the server returns is a decimal string
("0.25000000"). Do not put either through float.
Every timestamp is an ISO 8601 string with a literal +00:00. They are
strings here too, not datetimes, because the client must not re-render bytes
an integrator may be comparing.
Idempotency, which you get for free
Every mutating call carries an Idempotency-Key. One is minted per call, and
the same one is reused on every retry of that call — a retry with a new key
is a second deposit, not a retry. Pass your own when your system already has an
id for the operation:
client.create_deposit(external_user_id="user-42", asset="BTC", idempotency_key=f"order-{order.id}")
The client retries a 503 always, a 409 when the server sent a
Retry-After (which is how "your earlier attempt is still running" is told
apart from "this transition is illegal"), and a dropped connection — which is
safe precisely because the key is pinned. Tune or switch it off:
from crypto_processing_client import RetryPolicy
client = CryptoProcessingClient(url, key, retry=RetryPolicy(attempts=1))
Webhooks
from crypto_processing_client import parse_event, UnknownEventTypeError, WebhookVerificationError
@app.post("/platform-webhook")
async def platform_webhook(request: Request):
body = await request.body() # the raw bytes, never the parsed JSON
try:
event = parse_event(body, request.headers, secret=WEBHOOK_SECRET)
except WebhookVerificationError:
return Response(status_code=401)
except UnknownEventTypeError:
return Response(status_code=200) # a newer server sent a type you do not know
if already_handled(event["id"]): # the same evt_ id may arrive twice
return Response(status_code=200)
if event["type"] == "deposit.settled":
deposit = client.get_deposit(event["data"]["deposit_id"])
credit(deposit) # act on the GET, not on the webhook
return Response(status_code=200)
parse_event checks the signature over the raw body bytes, in constant
time, inside a 300-second window — and refuses to give you an event if any of
that fails. The verifier is checked against
sdks/signature-vectors.json,
the same vectors the server and the TypeScript client assert against, so the
three cannot quietly disagree.
Step 5 is the whole contract: a webhook tells you something changed; the GET tells you what is true.
Errors
Every refusal raises. The split is by what you can do next:
| Raised | Status | What to do |
|---|---|---|
BadRequestError |
400 | fix the request |
AuthenticationError |
401 | fix the key |
PermissionDeniedError |
403 | the key lacks the scope |
NotFoundError |
404 | no such deposit, withdrawal, user or asset |
ConflictError |
409 | an illegal transition, if it reached you |
ValidationError |
422 | a rule refused it; see .field_errors |
UpstreamRefusedError |
502 | BTCPay said no; the intent is dead |
ServiceUnavailableError |
503 | temporary; already retried, see .retry_after |
ServerError |
other 5xx | temporary |
TransportError |
— | never got an answer |
All of them derive from CryptoProcessingError.
Versions
client 0.N.x supports server 0.N.y. See
docs/reference/versioning.md.
What is generated and what is not
crypto_processing_client._generated comes from the server's committed
OpenAPI document and is regenerated in CI, which fails on any difference — so
it cannot describe a server that does not exist. Everything else in this
package is handwritten: the idempotency and retry behaviour, the error classes,
and webhook verification. Those are the things codegen cannot produce.
MIT licensed, like the service.
Release files for crypto-processing-client 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| crypto_processing_client-0.2.0.tar.gz | 50.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| crypto_processing_client-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:156.6 kB
Release files / crypto_processing_client-0.2.0.tar.gz
| Download URL | crypto_processing_client-0.2.0.tar.gz |
|---|---|
| Size | 50.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c9583c52a30099bceba8fcf75f48f234e43a95f70c319ac8dc321a3e855d646
|
|
BLAKE2b-256 checksum How to use checksums |
3075d3763662485441149d40529f21b189b0afa0c60f8a2ea2286a00bfbc9e70
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.
Transparency logRelease files / crypto_processing_client-0.2.0-py3-none-any.whl
| Download URL | crypto_processing_client-0.2.0-py3-none-any.whl |
|---|---|
| Size | 106.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bcfef25d2473391f1968abcc4be994a5e04b0c1e41aba937251f25361fac4948
|
|
BLAKE2b-256 checksum How to use checksums |
16bcdd748ad65e8f73d1158d64a033f61bb09bbd4b3fc6f6265a07f7414bace3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.
Transparency log