solverr
The Python server SDK for the Solverr crypto payment API.
Overview
Your server uses this SDK to create invoices, payment links, refunds, payouts, subscriptions and credit charges, to list the coins the checkout accepts, and to verify the signed webhooks Solverr sends.
- Checkout: your server calls
client.invoices.create-> the customer pays at the invoice'scheckout_url. - Webhooks: Solverr sends a signed
invoice.paidevent ->solverr.webhooks.verifychecks it and returns the event. - Paid content:
client.invoices.redeemmarks a paid invoice as used -> one payment grants access only once.
Install
The SDK needs Python 3.11 or later, uses only the standard library, and ships type hints.
pip install solverr
Quick Start
Create an invoice and send the customer to checkout:
import os
from solverr import Solverr
client = Solverr(
api_key=os.environ["SOLVERR_SECRET_KEY"], # sk_live_…
base_url=os.environ["SOLVERR_URL"], # the origin of your Solverr app
)
invoice = client.invoices.create(
amount="25.00",
currency="USD",
order_id="1042",
redirect_url="https://shop.example/thanks",
)
# Redirect the customer to invoice["checkout_url"]
base_url can be left out when the SOLVERR_URL environment variable is set. It must be an https URL. Plain http works only for localhost, 127.0.0.1 and [::1], so your API key never travels unencrypted. Arguments and response keys use the API's own field names (order_id, checkout_url), so the API reference applies as written. Responses are plain dictionaries, typed with the TypedDict classes in solverr.types.
Every key is a live key, so every invoice you create takes real payments. To see the checkout without moving real money, open the demo store at /demo on the Solverr app that base_url points to.
Webhooks
Confirm a payment with a webhook. Pass the raw request body exactly as it arrived: parsed and re-serialized JSON will not match the signature.
import os
from flask import Flask, request
import solverr
app = Flask(__name__)
@app.post("/webhooks/solverr")
def solverr_webhook():
try:
event = solverr.webhooks.verify(
request.get_data(),
request.headers,
os.environ["SOLVERR_WEBHOOK_SECRET"],
)
except solverr.WebhookVerificationError as error:
return str(error), 400
if event["type"] == "invoice.paid":
mark_order_paid(event["data"]["invoice"]["order_id"])
return "", 200
In Django, pass request.body and request.headers. In FastAPI or Starlette, pass await request.body() and request.headers.
How verify checks an event
verifychecks the Standard Webhooks signature. Thewebhook-signatureheader holdsv1,and the base64 HMAC-SHA256 ofid.timestamp.body(webhook-id,webhook-timestampand the raw body). The HMAC key is the part of the secret afterwhsec_, decoded from base64.- Any
v1entry may match, so events signed with both secrets during a rotation still pass. - It compares the exact standard base64 text with
hmac.compare_digest, so an unpadded or URL-safe spelling of a valid signature is refused. - It rejects events whose
webhook-timestampis more than 300 seconds from now. Change that window withtolerance=, in seconds. - Header names can use any casing.
- The same event can arrive more than once, so use
event["id"]to skip events you already handled.
Each business has one webhook URL and one signing secret.
Resources
| Call | Endpoint |
|---|---|
invoices.create(amount=, currency=, order_id=, description=, redirect_url=, customer_email=, metadata=, expires_in=, idempotency_key=) |
POST /api/v1/invoices |
invoices.retrieve(invoice_id) |
GET /api/v1/invoices/:id |
invoices.list(limit=, starting_after=, status=) |
GET /api/v1/invoices |
invoices.list_all(limit=, starting_after=, status=) |
Iterator over every matching invoice, fetching the next page as the loop goes on |
invoices.redeem(invoice_id) |
POST /api/v1/invoices/:id/redeem |
payment_links.create(title=, currency=, amount=, redirect_url=), payment_links.list() |
/api/v1/payment-links |
payment_links.deactivate(link_id) |
DELETE /api/v1/payment-links/:id |
refunds.create(invoice_id=, payment_id=, amount=), refunds.list() |
/api/v1/refunds |
payouts.create(recipient=, asset_id=, amount=), payouts.list() |
/api/v1/payouts |
payouts.create_batch(payouts=[{"recipient": …, "asset_id": …, "amount": …}, …]) |
POST /api/v1/payouts/batch |
refunds.retrieve(refund_id), payouts.retrieve(payout_id) |
GET /api/v1/refunds/:id, GET /api/v1/payouts/:id |
refunds.cancel(refund_id), payouts.cancel(payout_id) |
POST /api/v1/refunds/:id/cancel, POST /api/v1/payouts/:id/cancel |
subscriptions.create(customer_email=, amount=, currency=, interval=, description=), subscriptions.list() |
/api/v1/subscriptions |
subscriptions.cancel(subscription_id) |
DELETE /api/v1/subscriptions/:id |
credits.topup(amount=, currency=, credit_account_id=) |
POST /api/v1/credits/topup |
credits.charge(token=, amount=, currency=, description=) |
POST /api/v1/credits/charge |
credits.refund_charge(charge_id) |
POST /api/v1/credits/charges/:id/refund |
tokens.list() |
GET /api/v1/tokens |
webhooks.send_test() |
POST /api/v1/webhooks/test |
solverr.webhooks.verify(raw_body, headers, secret, tolerance=300) |
Local check, no request |
In the resource calls, every argument except an id is keyword-only. solverr.webhooks.verify takes its arguments by position. Every create, create_batch, topup, charge, refund_charge and redeem call, and refunds.cancel and payouts.cancel, also takes idempotency_key=.
Credit balances are kept in USD: credits.topup needs currency="USD", and credits.charge accepts only "USD" when you pass a currency.
Refunds and payouts
refunds.create refunds the payment that paid the invoice. To refund an extra payment on the same invoice, pass its id as payment_id=. Leave out amount= to refund what is left of that payment.
The asset_id= of a payout is the coin the recipient receives. Take it from tokens.list(), which returns every coin the checkout accepts, with its chain, decimals and USD price.
Lists and pages
Every list call except tokens.list() returns {"object": "list", "data": [...], "has_more": bool}. To get the next page, pass the last item's id as starting_after, or loop over list_all():
for invoice in client.invoices.list_all(status="paid"):
print(invoice["id"])
tokens.list() returns every coin in one {"object": "list", "data": [...]} response, without pages.
Grant access once per paid invoice
invoices.redeem(invoice_id) marks a paid invoice as used. Call it before you deliver a download or show paid content, so one payment cannot be used twice.
from solverr import SolverrError
try:
client.invoices.redeem(invoice_id)
# deliver the content
except SolverrError as error:
if error.status == 409:
... # error.type is "invalid_state" (not paid) or "already_redeemed"
else:
raise
Errors and Retries
API errors raise solverr.SolverrError with these attributes:
| Attribute | Meaning |
|---|---|
status |
The HTTP status, or 0 when the API cannot be reached. |
type |
The error type, or connection_error when the API cannot be reached. |
message |
The error message. |
param |
The invalid field, when a field is invalid. |
code |
A finer reason, when the API adds one. None of the endpoints this SDK calls send one today. |
retry_after |
The seconds the API asked you to wait, on a 429 for example. |
from solverr import SolverrError
try:
client.invoices.create(amount="0", currency="USD")
except SolverrError as error:
print(error.status, error.type, error.param, error.message)
# 400 invalid_request amount amount must be greater than 0.
Timeouts and retries
Each attempt allows 15 seconds to connect and 15 seconds for each wait on the network. The SDK stops reading the response body once 15 seconds have passed since the attempt began.
| Response | How the SDK retries |
|---|---|
| A timeout, a network error or a 5xx response | Twice, with a growing delay. |
A 429 (rate limit), or a 409 that says an earlier request with the same Idempotency-Key is still being processed |
Up to three times, after the wait in its Retry-After header. When Retry-After asks for more than 10 seconds, the error is raised right away. |
With those retries, one call ends after about 47 seconds when the server does not answer at all. A slow connection or slow response headers can make it longer, and Retry-After retries add to that.
Idempotency keys
Every POST carries an Idempotency-Key header. The SDK generates one when you do not pass one, and every retry sends the same key, so a retried POST never creates a second object. Pass your own key to make retries across process restarts safe too.
A key belongs to the business for 24 hours and covers every endpoint. Give each operation its own key, for example invoice-order-1042 for the invoice of order 1042 and refund-order-1042 for its refund, not the order id alone. If you send a key again with a different request (another body or another endpoint), the API answers 409 with type idempotency_conflict, and the SDK raises it without a retry.
invoice = client.invoices.create(
amount="25.00",
currency="USD",
order_id="1042",
idempotency_key="invoice-order-1042",
)
Tests
The tests use only the standard library (unittest) and need no install step: tests/__init__.py puts src on the import path. HTTP tests run against a local server on 127.0.0.1 and never reach a real host. Webhook tests generate their own secrets. From this folder, run:
python -I -m unittest
CI names the test folder explicitly: python -I -m unittest discover -s tests -t .. The -t . makes tests load as a package.
License
Released under the MIT License.
Metadata
Release files for solverr 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| solverr-0.1.1.tar.gz | 24.1 kB | Without provenance | Details for solverr-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| solverr-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for solverr-0.1.1-py3-none-any.whl |
Total release size: 47.3 kB
Release files / solverr-0.1.1.tar.gz
| Download URL | solverr-0.1.1.tar.gz |
|---|---|
| Size | 24.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d81e1a822b7ce93b5636347ab0664dd21d765e283a7a5269e26d4609cba14a7c
|
|
BLAKE2b-256 checksum How to use checksums |
910e2241632b24d5d6fb85e8f1cc252d462cc26e637757d3dd1f7ac5254177e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / solverr-0.1.1-py3-none-any.whl
| Download URL | solverr-0.1.1-py3-none-any.whl |
|---|---|
| Size | 23.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
845076f8b5f5f92151b36f602b54f97771749fb8b63926b9f58da4b5f6d85b61
|
|
BLAKE2b-256 checksum How to use checksums |
53b22d620d03690176af34021f754e0513dec728c23535c75b9fa4a2a876b0f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for solverr 0.1.1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.