Integrate

Python SDK

Use the solverr package to create invoices, links, refunds and payouts from a Python server, verify webhooks, and handle errors, retries and idempotency.

On this page

The solverr package is the Solverr API client for Python servers. It covers invoices, payment links, refunds, payouts, subscriptions, credits and the list of accepted coins, and verifies webhooks. Arguments and response keys keep the API's own field names, such as order_id and checkout_url, so the API reference applies as written.

Install

pip install solverr

The package needs Python 3.11 or later, uses only the standard library and ships type hints. Responses are plain dictionaries, described by the TypedDict classes in solverr.types.

Create an invoice

import os

from solverr import Solverr

client = Solverr(
    api_key=os.environ["SOLVERR_SECRET_KEY"],  # sk_live_…
    base_url=os.environ["SOLVERR_URL"],  # https://your-solverr-host
)

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"]

Keep the API key on your server only. Leave out base_url when the SOLVERR_URL environment variable is set. The base URL must use https; plain http is accepted only for localhost, 127.0.0.1 and [::1].

invoices.create also accepts four optional arguments: description=; customer_email=, the address that gets the receipt; metadata=, up to 20 string keys and values returned on the invoice; and expires_in=, the invoice lifetime in seconds, from 900 to 86400 (default 3600).

Confirm payment with a webhook

Pass the raw request body to verify exactly as it arrived. JSON that was parsed and serialized again no longer matches 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

SOLVERR_WEBHOOK_SECRET holds the signing secret from Dashboard → Developers. solverr.webhooks.verify rejects events more than 300 seconds older or newer than now. tolerance= changes that window, in seconds.

In Django, pass request.body and request.headers. In FastAPI or Starlette, pass await request.body() and request.headers. Verify signatures explains what verify checks.

Grant access once per paid invoice

Call invoices.redeem(invoice_id) before you deliver a download or open paid content. It marks the paid invoice as used, 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

Resources

CallEndpoint
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.retrieve(id), refunds.list(), refunds.cancel(id)/api/v1/refunds
payouts.create(recipient=, asset_id=, amount=), payouts.retrieve(id), payouts.list(), payouts.cancel(id)/api/v1/payouts
payouts.create_batch(payouts=[{"recipient": …, "asset_id": …, "amount": …}, …])POST /api/v1/payouts/batch
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.

By default, 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.

Pagination

Every list call except tokens.list() returns a page as {"object": "list", "data": [...], "has_more": bool}. For the next page, pass the last item's id as starting_after, or let list_all() loop over every page:

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.

Errors and retries

API errors raise solverr.SolverrError with status, type, message, param (the invalid field), code (a finer reason when the API adds one; none of the endpoints this SDK calls send one today) and retry_after (the seconds the API asked you to wait, for example on a 429). When the API cannot be reached, status is 0 and type is connection_error.

Each attempt allows 15 seconds to connect and 15 seconds for each wait on the network. The response body is cut off once 15 seconds have passed since the attempt began. The SDK retries timeouts, network errors and 5xx responses twice with a growing delay. When the server does not answer at all, one call ends after about 47 seconds; a slow connection or slow response headers can make it longer, and Retry-After waits add to that. A 429 (rate limit), or a 409 saying that an earlier request with the same Idempotency-Key is still being processed, is retried up to three times after the wait in its Retry-After header. When Retry-After asks for more than 10 seconds, the SDK raises the error at once instead.

Idempotency keys

Every POST carries an Idempotency-Key header, generated when you do not pass one. Every retry sends the same key, so a retried POST never creates a second object. Pass your own key to make retries safe across process restarts too. A key belongs to your business across every endpoint for 24 hours, so 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 retrying.

invoice = client.invoices.create(
    amount="25.00",
    currency="USD",
    order_id="1042",
    idempotency_key="invoice-order-1042",
)