# 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.

Source: https://cryptolike.net/guides/accept-crypto-payments-python
Published: 2026-09-24 · Updated: 2026-10-04 · Author: CryptoLike team

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](https://cryptolike.net/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](https://cryptolike.net/docs/auth).

## 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](https://cryptolike.net/docs/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](https://cryptolike.net/guides/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](https://cryptolike.net/docs).

## 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](https://cryptolike.net/integrations/sdk-python) 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.
