Crypto payments for custom backends
A backend-agnostic walkthrough of integrating Qvard into your own server: the order model, creating the payment, the webhook, reconciliation and a go-live checklist, with Python examples.

If your shop, service or platform runs on your own backend, you do not need a plugin: Qvard needs one call that creates a payment and one endpoint that receives the result. This guide walks through the integration in a way that fits any language and framework, with Python examples, and covers the parts that make it reliable: the order model, idempotency, reconciliation and a short go-live checklist.
What you need
- A Qvard account with a project, an API key and the callback URL of your webhook saved in the project.
- A public HTTPS endpoint on your server for the webhook.
- An orders table (or its equivalent) in which a payment can be matched to an order by your own identifier.
There is no SDK and no sandbox yet: the integration is plain HTTP, and testing is done with small real amounts. The reference for the request formats is crypto payment API for developers; this guide shows how they fit together.
A minimal order model
Your own data decides how clean the integration is. A minimal set of fields on an order:
id: your order or invoice number. This is theidyou send to Qvard and the key that comes back in the webhook.amount_usd: the amount in US dollars, the unit the payment is created in.state: for exampleawaiting_payment,paid,cancelled. Mark paid only from the webhook.paid_atand, if you like, the Qvard paymentidfrom the response for support.
Avoid putting the whole payment logic in the order class. A small module that creates the payment, and an endpoint that applies the webhook, keeps things testable.
Create the payment
When the customer chooses to pay, create the payment from your backend with one POST to https://api.qvard.com/api/v1/payment. Send the key in the Authorization: Bearer header and a JSON body with your order number as id (a string), the amount as price in US dollars and, optionally, a paymentSystemId to open the payment page with an asset and network selected.
The response is 201 with redirectUrl, the address of the hosted payment page. Redirect the customer there, or put the link in an email. Rules worth encoding in the client:
- Timeout and retry. Use a timeout and retry on a timeout or a
5xx. Sending the sameidfor the same payment method returns the existing payment, so a retry cannot create a second one. - Do not retry a
400("Error validation", with a message per field) or a401(wrong key) without changing something. - Amounts in dollars. If your shop prices in another currency, convert before the call.
import os
import requests
QVARD_URL = "https://api.qvard.com/api/v1/payment"
def create_payment(order_id: str, amount_usd: float) -> str:
"""Create a Qvard payment for an order and return the hosted payment page URL."""
response = requests.post(
QVARD_URL,
headers={"Authorization": "Bearer " + os.environ["QVARD_API_KEY"]},
json={"id": order_id, "price": amount_usd}, # id as a string, price in US dollars
timeout=15,
)
if response.status_code == 201:
return response.json()["redirectUrl"]
# 400 = "Error validation" with a per-field message, 401 = wrong API key
raise RuntimeError(f"Qvard {response.status_code}: {response.text}")Receive the webhook
When a payment is completed, Qvard sends a POST to your callback URL with the id and the status, signed in the X-Qvard-Signature header. Your endpoint should:
- Verify the signature: HMAC-SHA256 with your secret key over the timestamp and the raw body, compared in constant time; if it does not match, answer
400. - Act on the
completedstatus only. - Update the order conditionally, "paid if still awaiting payment", so repeats do nothing.
- Queue the slow work and answer exactly HTTP 200.
Qvard retries any other answer, a timeout or a redirect, five attempts over roughly six minutes, so repeated deliveries are normal. The details of retries are in webhook retry strategy for payment systems, and the security rules in how to secure crypto payment webhooks.
import os
from fastapi import FastAPI, Request, Response
from qvard import Qvard
app = FastAPI()
qvard = Qvard(os.environ["QVARD_API_KEY"])
@app.post("/webhooks/qvard")
async def qvard_webhook(request: Request):
try:
body = qvard.webhooks.construct_event(await request.body(), request.headers.get("X-Qvard-Signature"), os.environ["QVARD_API_KEY"])
except ValueError:
return Response(status_code=401)
if body.get("status") == "completed":
# Only changes an order that is not paid yet, so repeated deliveries are harmless.
changed = await db.execute(
"UPDATE orders SET state = 'paid', paid_at = now() WHERE id = :id AND state = 'awaiting_payment'",
{"id": body.get("id")},
)
if changed:
await queue.enqueue("fulfil_order", body["id"])
return Response(status_code=200)Partial payments, failures and the customer view
Decide what each payment status means for an order:
wait: awaiting payment.partial: less than the full amount arrived; no completion webhook is sent. Keep the order open, tell the customer how much is missing, or cancel and refund.completed: the full amount arrived; mark the order paid.rejected: offer another way to pay.
Return URLs after payment are not available yet, so the customer comes back on their own: give them an order page that shows the real status and refreshes while it is waiting. A refund is a transfer you send yourself, because a crypto payment cannot be reversed.
Reconciliation and a go-live checklist
Add a daily check that compares the orders you consider paid or unpaid with the payments and balance in your Qvard cabinet; a payment shown as completed while your order is unpaid is a missed notification, which you can resend from the cabinet.
Before going live:
- the API key is in an environment variable or a secrets manager, never in the repository;
- the callback URL is saved, public, HTTPS and reachable from outside your network;
- the endpoint answers exactly 200, quickly, and is safe to run twice;
- you paid a small real amount and saw the order become paid;
- you made the endpoint fail once and saw Qvard retry without a double fulfilment;
- you withdrew a small amount to your wallet. Withdrawal costs only the network fee; withdrawal to a bank account in fiat is not available.
Commercial terms are on the pricing page, and a demo invoice shows what your customer sees.
FAQ
Is there an SDK for my language?
No. The integration is plain HTTP: one POST to create a payment and one webhook to receive the result. The examples in this guide show the pattern in Python, and it ports to any language.
How do I match a payment to an order?
Send your own order number as the id when you create the payment. The same id comes back in the webhook.
Is there a sandbox for testing?
Not yet. Use small real amounts, and a demo invoice to see the payment page.
What if my server is down when a payment completes?
Qvard retries five times over about six minutes. After that, resend the notification for the payment from the cabinet; an idempotent handler makes that safe.
