QryptoPay setup guide
QryptoPay is a self-hosted crypto acquiring solution that allows you to automatically accept cryptocurrency payments on your website. The module is easy to integrate: you can generate payment links on the fly, share them with your customers, and receive funds directly to your own crypto wallets.
The entire solution is deployed on your own server, giving you full control over your funds and a higher level of security.
What you need to get started
QryptoPay is a module of the BeAdmin control panel. To use it properly, you will need:
- a server with the
BeAdminpanel installed (see the installation guide); - a domain name pointing to this server (for example,
qpay.mysite.com).
⚠️ Warning
The domain name is used as the address of the payment page to which your customers will be redirected to complete the payment. This page runs on the same server where QryptoPay is installed. If you don’t have a domain yet, you can add it later at any time.
Step 1. Module installation
To ensure QryptoPay works correctly, you need to install the following modules:
- DOCKER — required, as QryptoPay runs inside Docker containers;
- NGINX — can be installed later, but without it the payment page cannot be launched.
To install each module, open the corresponding section in the sidebar and click Install. Modules must be installed sequentially, one after another.
After all dependencies are installed, return to the QryptoPay module and start the installation of the module itself.
Step 2. Creating a store
Once QryptoPay is successfully installed, create a store. Click the Create button and enter any name you prefer (it can be changed later if needed).
If the NGINX module is already installed and you have a domain for the payment page, you can specify it in the additional settings. In this case, the payment page will be created automatically together with the store.
After the store is created, you can proceed to its configuration.
Each store includes the following components:
- two terminals: primary and test — used to configure and verify the integration with your project;
- Customers section — used to track payers (data depends on the selected terminal);
- Payments section — used to view and analyze payments (data depends on the selected terminal);
- Wallets section — used to configure payment methods and receive funds (available only for the primary terminal).
ℹ️ Info
The test terminal does not require wallets to operate, so wallet creation is not available for it.
For the store to work properly, you need to connect your project to a terminal and add at least one wallet.
Step 3. Integration setup
The key element of the integration is the terminal. Each terminal has the following parameters:
- ID — a unique identifier;
- Token pair — public and private tokens used to identify payments;
- Webhook — the URL of your project where QryptoPay sends payment status notifications;
- Webhook key — used to authorize webhook notifications.
⚠️ Warning
The private token must be stored securely. If it is compromised, attackers may interfere with your payment processing logic. For this reason, after generation QryptoPay does not store the private token on its side.
If the token is lost or compromised, you can always generate a new token pair — just make sure to update the values in your project.
The webhook key is less critical, but it is still recommended to keep it private to prevent forged webhook notifications.
We recommend starting the integration with the test terminal. Unlike the primary terminal, it allows you to verify that the integration works correctly without actually transferring cryptocurrency.
First, generate a token pair for the test terminal and securely store the private token.
In your project settings, save the terminal parameters as environment variables. For example, your .env file may look like this:
TERMINAL_ID: <terminal ID>
PRIVATE_TOKEN: <generated private token>
WEBHOOK_KEY: <webhook key from settings>
PAYMENT_URL: <payment page domain>Next, you need to implement a method that generates a payment token used to create a payment link on the QryptoPay side.
Below is an example of such a method implemented in pseudocode:
function generate_payment_token(private_key_b64, terminal_uuid) -> string
# decode the private key from base64/base64url into a seed (raw bytes)
seed := decode_base64_any(private_key_b64)
# generate a unique nonce (typically uuid4)
nonce := uuid_v4()
# build the payload (required fields + payment data)
payload := {
timestamp: current_unix_time_seconds(),
nonce: nonce,
terminal_uuid: terminal_uuid, // terminal ID in QryptoPay
amount_fiat: transaction.amount, // amount in USD, format 00.00, decimal, > 0
payment_mid: transaction.uuid, // transaction ID in your project, string
back_to_store_link: link, // link back to your site after payment (you may add query params), string
customer: {
id: customer.uuid, // customer ID in your system (required), string
email: customer.email or "" // customer email in your system (optional), string
},
metadata: { // any additional parameters you want to pass through
key: value // in the form metadata.key=value
}
}
# serialize the payload into a deterministic (canonical) byte representation
payload_bytes := canonical_encode(payload)
# encode the payload into a transport-safe format
payload_part := base64url_no_padding(payload_bytes)
# compute the cryptographic part of the token using the private key and payload_part
proof_bytes := sign(seed, bytes(payload_part, ASCII))
# encode the cryptographic part of the token
proof_part := base64url_no_padding(proof_bytes)
# final token format: "<payload_b64u>.<proof_b64u>"
return payload_part + "." + proof_part
end⚠️ Warning
To generate a payment token, in addition to the transaction data, you must provide three required parameters: the current timestamp, a nonce (we recommend uuid4), and the terminal ID.
The timestamp is used to calculate the link’s lifetime (slightly over 1 hour). The nonce is a security measure that prevents creating multiple payment intents from the same link (spam protection). Without the terminal ID, QryptoPay won’t be able to process the payment.
Using the generated token, send a POST request to QryptoPay to obtain a payment link. For example:
POST /public/api/payments/intents/create/
Host: qpay.yoursite.com
Content-Type: application/json
{
"key": "payment token"
}⚠️ Warning
By the time you make this request, the payment page must already be created on the server where QryptoPay is installed.
If the request is successful, you’ll receive a response containing a payment link. You should pass this link to the customer so they can proceed to payment.
{
"service_id": "be535ba0-7f84-4cd3-9454-b26c4a938479",
"url": "https://qpay.yoursite.com/?payment=be535ba0-7f84-4cd3-9454-b26c4a979225",
"expires_at": "2026-01-30T08:21:56.526112Z"
}If an error occurs, the server may return one of the following codes:
- 400 — invalid request (malformed payload or missing required parameters).
- 403 — invalid request signature or the payment token has expired.
- 409 — one-time token reuse detected. The provided
noncehas already been used for this terminal. - 444 — payment intent creation is temporarily disallowed (for example, due to licence limits or rate limiting).
- 445 — payment intent creation is disallowed (licence exhausted or access permanently restricted).
- 500 — internal server error.
⚠️ Warning
If the customer opens the payment link, selects a currency, and starts the payment flow (clicks Continue), they won’t be able to change the selected currency. In this case, the customer must return to your site and generate a new payment link. This behavior is implemented for security reasons.
⚠️ Warning
If you include the customer’s email in the payload sent to QryptoPay and the email later changes, QryptoPay will identify the customer by ID and overwrite the stored email if it differs. The updated email will also be applied to previously created payments.
Here’s a clean English version, rephrased for documentation clarity while preserving the structure and meaning:
The overall flow for generating a payment link is as follows:
- a payment intent (for example, a transaction) is created in your project;
- the payment amount is converted to USD (QryptoPay accepts incoming amounts in USD only);
- a payment token with the required parameters is generated and sent to your server with QryptoPay;
- QryptoPay returns a payment link, which you pass to the customer so they can proceed to payment.
After that, the customer completes the payment on the payment page and may return to the store if needed. A notification about a successful or failed payment will be sent to your project with a delay, since blockchain transactions must wait for network confirmations. This usually takes from 1–2 minutes (Tron, USDT TRC-20) up to 10–30 minutes (Bitcoin).
You can now generate a payment link, open it, select a currency, and click the payment button. If everything is configured correctly, the payment will appear in your QryptoPay store under the test terminal.
Step 4. Webhook setup
After the customer completes the payment, QryptoPay starts scanning the blockchains to find the corresponding transaction. If the payment is made strictly according to the instructions on the payment page, a notification about the successful payment will be sent to your webhook.
The notification is delivered as an HTTP request with the following parameters:
POST /your/webhook/url
Content-Type: application/json
X-Term-UUID: 2671f44b-a025-44d3-b2f1-a0ea07b8acb7
X-Timestamp: 1738150000
X-Body-SHA256: 9c4d2b0a2f5b7d6c8a... # 64 hex characters
X-Signature: 7f1c3d5e9a... # 64 hex characters
{
... JSON webhook payload ...
}To process webhook notifications, you need a method that validates the webhook signature and ensures the request wasn’t intercepted or modified in transit. Below is an example of such a verification in pseudocode:
function validate_webhook_response() -> bool
# build the HMAC message: "<terminal_uuid>:<ts>:<body_sha256_hex>"
message_str := string(term_uuid_header) + ":" + string(ts) + ":" + computed_body_hash
message_bytes := utf8_bytes(message_str)
# compute the expected signature: HMAC-SHA256(secret, message), hex
expected_sig := hmac_sha256_hex(key = utf8_bytes(secret), msg = message_bytes)
# compare signatures in constant time
if not constant_time_equals(expected_sig, sig_header) then
return false
end
return true
end⚠️ Warning
We recommend treating the webhook request as invalid if any of the following conditions are met:
- any required header is missing;
X-Term-UUIDdoes not match the terminal ID used when generating the payment token for this transaction;X-Timestampis not a number (int), or more than 300 seconds have passed since the provided timestamp (recommended value).
If the validation succeeds, you can deserialize the request body. You will get an object roughly like the following:
{
"payment_result": "payment_result", // payment result: success, mismatch, unexpected
"amount_coins": "0.00", // crypto amount, format 00.00
"amount_fiat": "0.00", // amount in USD, format 00.00
"fiat_code": "USD", // fiat currency code
"coins_asset": "USDC", // crypto asset: BTC, ETH, USDT, USDC, etc.
"coins_chain": "ETH", // network/chain: BTC, ETH, TRX, etc.
"service_id": "qryptopay_pi_uuid", // internal payment identifier in QryptoPay
"payment_mid": "string", // transaction ID in your project; null if status is unexpected
"customer": {
"id": "your_id", // customer ID in your system
"email": "string" // customer email; null if not provided
},
"metadata": { // null if not provided originally or if status is unexpected
"key1": "value1"
},
"transaction_ids": [ // related blockchain transactions (one or few)
"686...fbe",
"8be...6ab"
]
}⚠️ Warning
If validation succeeds, respond with 200 OK. Otherwise, QryptoPay will keep retrying the payment notification until it receives a 200 response.
You can use the received data for post-processing the payment. Some fields are returned exactly as provided — for example, all metadata values passed through the payment link.
⚠️ Warning
If the customer’s email is already stored in QryptoPay, but the email is not sent in the current payment payload, the webhook notification will include the email from the QryptoPay database.
💡 Tip
We recommend performing an additional validation step by matching the webhook data against the original transaction parameters (for example, amount, payment ID, and customer ID).
Next, on your project side, you need to implement a method that properly handles incoming webhook notifications. There are three possible scenarios:
success— the payment was completed successfully. In this case, we recommend verifying theamount_fiatvalue from the webhook against your internal amount before finalizing the payment;mismatch— the customer paid less or more than the expected amount;unexpected— the customer sent funds to the wallet without creating a payment link first.
How you handle each scenario is up to you. For example, for mismatch you can compare the received amount with the expected amount and, if the payment is incomplete, notify the customer that an additional payment is required. For unexpected, one possible approach is to automatically top up the customer’s balance and then notify them about the payment.
After implementing the handler, set the webhook URL in the test terminal settings and repeat the payment flow: generate a new payment link and open it. For the test terminal, the payment is processed immediately, and the notification is sent to your webhook right away.
If your project successfully receives the notification, you can consider the test integration completed.
Step 5. Wallet setup
To accept payments, you need to add wallets for each cryptocurrency you plan to support. Wallets are created in external services, and then their details are configured in the QryptoPay module.
A detailed guide on adding and configuring wallets is available at this link.
Step 6. Primary terminal setup
After adding wallets, you can switch your site to use the primary terminal. To do this, follow these steps:
- generate a new token pair for the primary terminal;
- update the terminal settings and specify the webhook URL;
- replace the
ID,PRIVATE_TOKEN, andWEBHOOK_KEYvalues in your project’s.envfile with the credentials from the primary terminal.
After that, you can verify the terminal by creating a test payment. Keep in mind that at this stage you must perform a real cryptocurrency transfer for the wallet you added, so it’s recommended to start with small amounts.
If everything is configured correctly, the payment will be successfully credited in your project.