Skip to content

For developersGuide

How to accept crypto payments in Python

Accept Bitcoin and USDT in a Python app: sign API requests with hmac, create a payment with requests, redirect to the payment page and verify the webhook in Flask.

A Python application accepts crypto with CryptoLike in three steps: sign each API request with the standard hmac and hashlib modules, create a payment for the order, and verify the signed webhook before marking the order paid. The examples use requests and Flask; the same logic fits Django, FastAPI and other frameworks.

A shop on WooCommerce, OpenCart, PrestaShop or Magento 2 needs no code — see the integrations. This guide is for your own backend.

What you need

A CryptoLike account, Python 3.10 or newer, and a public https:// URL for webhooks. Keep the secrets in the environment: the examples read CRYPTOLIKE_API_KEY_ID, CRYPTOLIKE_API_SECRET and CRYPTOLIKE_WEBHOOK_SECRET, plus CRYPTOLIKE_API_URL for the test API URL (unset means production). Start with test keys and test networks.

1. Create a test API key

In the cabinet open Developers → API keys → Create key with the permissions create_invoice and read. A ck_test_ key works with test networks, where coins have no value. Test keys come from the test environment: CryptoLike support gives you access to its cabinet, where you create the same key, and its API URL (CRYPTOLIKE_API_URL in the examples). The production cabinet issues live keys only, and on mainnet a test key is refused with 401 auth.test_key. The secret is shown once; put it into your environment or secret manager right away.

2. Sign API requests with hmac

The signature is an HMAC-SHA256 of four lines — the Unix timestamp in seconds, the method, the path with the query, and the SHA-256 of the body — keyed with the API secret:

python
import hashlib, hmac, json, os, time
import requests

def cryptolike(method: str, path: str, payload: dict | None = None, idempotency_key: str | None = None) -> dict:
    body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode()
    ts = str(int(time.time()))
    canonical = "\n".join([ts, method.upper(), path, hashlib.sha256(body).hexdigest()])
    headers = {
        "X-API-Key": os.environ["CRYPTOLIKE_API_KEY_ID"],
        "X-Timestamp": ts,
        "X-Signature": hmac.new(os.environ["CRYPTOLIKE_API_SECRET"].encode(), canonical.encode(), hashlib.sha256).hexdigest(),
        "Content-Type": "application/json",
    }
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    r = requests.request(method, os.environ.get("CRYPTOLIKE_API_URL", "https://api.cryptolike.net") + path, data=body or None, headers=headers, timeout=30)
    data = r.json()
    if r.status_code >= 400:
        raise RuntimeError(f"{data.get('error', {}).get('code', r.status_code)}: {data.get('error', {}).get('message', '')}")
    return data

Send the same bytes you hashed — pass data=body, not json=payload, which would serialise again. Check the function against the reference vector in API authentication.

3. Create the payment for an order

Create the payment on the server when the order is placed. Price it in USD or EUR and let the customer pick the coin, or pass a coin_id from GET /v1/currencies. Without coin_id the page lists the coins you offer for invoices, each with an approximate amount marked ≈; add allowed_coin_ids (ids with invoice_enabled: true in the signed GET /v1/currencies) to offer only some of them. The order ID as the idempotency key makes a retry return the same payment:

python
invoice = cryptolike("POST", "/v1/invoices", {
    "amount": "49.90",
    "currency": "USD",
    "order_id": order.id,
    "idempotency_by_order_id": True,
    "return_url": f"https://example.com/orders/{order.id}",
}, idempotency_key=f"order-{order.id}")

order.invoice_id = invoice["id"]

Keep amounts as strings or Decimal — never float.

4. Send the customer to the payment page

Redirect to invoice["checkout_url"] (redirect(invoice["checkout_url"], code=303) in Flask). The page shows the amount, the network, the address with a QR code, the time left and the live status, then returns the customer to return_url. To keep the customer on your site, embed the page with the checkout widget.

5. Verify the webhook in Flask

Add a webhook endpoint in Developers → Webhooks with the invoice.* events. Verify over the raw body from request.get_data(), deduplicate by event_id and answer quickly:

python
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/cryptolike")
def cryptolike_webhook():
    raw = request.get_data()
    ts = request.headers.get("X-Timestamp", "")
    got = request.headers.get("X-Signature", "")
    secret = os.environ.get("CRYPTOLIKE_WEBHOOK_SECRET", "")
    want = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not secret or not ts.isdigit() or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(got, want):
        return "", 401
    event = json.loads(raw)
    if first_time(event["event_id"]) and event["type"] == "invoice.paid":
        mark_order_paid(event["data"]["invoice"]["order_id"])
    return "", 200

Deliveries are at least once and may arrive out of order: rely on the status inside data. The complete procedure is in verify crypto payment webhooks.

6. Test on a test network, then go live

Create a payment with the test key and pay it with test coins on the network shown on the page: invoice.pending arrives first, then invoice.paid. Handle invoice.underpaid, invoice.expired and invoice.reverted as well, then switch to a live key and follow the go-live checklist in the developer guide.

Common errors and fixes

Error What happened What to do
401 auth.invalid_signature The request body differs from what was signed Send the hashed bytes with data=, not json=
401 auth.expired_timestamp The clock is more than 5 minutes off Sync the server clock with NTP
401 on your webhook The body was parsed or decoded before the check Verify over request.get_data()
400 validation_error A field is missing or malformed Read details.field; send amounts as strings
409 conflict The order already has a payment Use details.invoice_id instead of creating another
422 no_allowed_coins None of allowed_coin_ids is offered for invoices in this currency Send other ids, or leave the field out to offer every coin

Prefer an SDK?

The official Python SDK for Python 3.10+ wraps these calls: signed requests, retries on 429 and 5xx, an Idempotency-Key on every POST, pagination and webhook verification. The code above is what it does inside, so you can switch later without changing the flow.

FAQ

Why does my signature fail when I use requests' json= argument?

json=payload serialises the body again, with its own spacing, so the bytes sent differ from the bytes you hashed. Serialise once, hash those bytes, and pass the same bytes with data=. The same applies on the webhook side: verify over request.get_data() before you parse anything.

Can I use Django or FastAPI instead of Flask?

Yes. The logic is the same in every framework: read the raw body, compute sha256= plus the HMAC of the timestamp, a dot and the body, compare with hmac.compare_digest, reject timestamps more than 300 seconds away from your clock, then parse. In Django the raw body is request.body; in FastAPI, await request.body().

How do I test without spending real crypto?

Use a ck_test_ key with the test API URL from CryptoLike support. Test keys work with test networks, where test coins have no value, and the whole flow — payment page, confirmations, signed webhooks — behaves as in production. Keep test and live keys in separate configurations so the two never mix.

Terms used here

Read next

Start with a test payment

Create an account, ask support for the test environment and see a payment go from pending to paid on a test network.