Building a signature for payouts
This section describes how to build the X-Signature for making payouts:
- to a bank card;
- via SBP;
- to a bank account from Sberbank's nominal account;
- via SBP from Sberbank's nominal account;
- to a bank account via Alfa-Bank's nominal account.
All payout methods and all stages use the same algorithm: RSA/SHA-256.
The signature is passed in the X-Signature HTTP request header.
Digital signature key
-
Generate a private key:
-
Obtain the public part of the key:
-
Send the public part of the key to KVELL technical support to be uploaded into the system. The private key must remain on the merchant's side.
Attention
The signature is verified against the actual request body. Sign exactly the JSON you send to the API: don't change the order of fields and don't re-serialize the data after building the signature. There's no need to sort the parameters.
Signature-building algorithm
- Get the exact JSON request body as a UTF-8 encoded string.
- Append the shop's
secret_keyto the end of the string with no separator, space, or line break. - Sign the resulting bytes with the private RSA key using SHA-256.
- Base64-encode the signature and pass the string in the
X-Signatureheader.
Code examples
In each example, the body is serialized once: the same string is used both for signing and for sending to the API.
Python
First define a shared function for building the signature and sending the request:
import base64
import json
import requests
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
x_api_key = "<api-key>"
secret_key = "<secret-key>"
def send_payout(url: str, payload: dict):
data = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
)
with open("privatekey.pem", "rb") as private_key_file:
key = RSA.importKey(private_key_file.read())
digest = SHA256.new(f"{data}{secret_key}".encode("utf-8"))
signature = PKCS1_v1_5.new(key).sign(digest)
x_signature = base64.b64encode(signature).decode("utf-8")
return requests.post(
url,
data=data.encode("utf-8"),
headers={
"Content-Type": "application/json; charset=UTF-8",
"X-Api-Key": x_api_key,
"X-Signature": x_signature,
},
)
Then choose the payout type:
payload = {
"recipient_pan": "4111111111111111",
"amount": 15000,
"transaction": "payout-card-20260810-0001",
"description": "Payment under contract 42",
"customer": "customer@example.com",
}
response = send_payout(
"https://api.pay.kvell.group/v1/orders/account2card",
payload,
)
print(response.status_code)
print(response.text)
payload = {
"phone": "79991234567",
"fio": "Иванов Иван Иванович",
"bank_id": "100000000008",
"amount": 15000,
"transaction": "payout-sbp-20260810-0001",
"description": "Payment under contract 42",
}
response = send_payout(
"https://api.pay.kvell.group/v1/orders/payout/sbp",
payload,
)
print(response.status_code)
print(response.text)
PHP
First define a shared function for building the signature:
<?php
function buildSignedRequest(array $payload, string $secretKey): array
{
$data = json_encode(
$payload,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if ($data === false) {
throw new RuntimeException('Failed to serialize the request body');
}
$key = openssl_pkey_get_private(
file_get_contents('privatekey.pem')
);
$signature = '';
openssl_sign(
$data . $secretKey,
$signature,
$key,
'sha256WithRSAEncryption'
);
return [$data, base64_encode($signature)];
}
$secretKey = '<secret-key>';
Then choose the payout type:
<?php
$url = 'https://api.pay.kvell.group/v1/orders/account2card';
$payload = [
'recipient_pan' => '4111111111111111',
'amount' => 15000,
'transaction' => 'payout-card-20260810-0001',
'description' => 'Payment under contract 42',
'customer' => 'customer@example.com',
];
[$data, $xSignature] = buildSignedRequest($payload, $secretKey);
print('URL: ' . $url . PHP_EOL);
print('X-Signature: ' . $xSignature . PHP_EOL);
print('Body: ' . $data . PHP_EOL);
<?php
$url = 'https://api.pay.kvell.group/v1/orders/payout/sbp';
$payload = [
'phone' => '79991234567',
'fio' => 'Иванов Иван Иванович',
'bank_id' => '100000000008',
'amount' => 15000,
'transaction' => 'payout-sbp-20260810-0001',
'description' => 'Payment under contract 42',
];
[$data, $xSignature] = buildSignedRequest($payload, $secretKey);
print('URL: ' . $url . PHP_EOL);
print('X-Signature: ' . $xSignature . PHP_EOL);
print('Body: ' . $data . PHP_EOL);
OpenSSL
Save the exact POST request body to a file called data_to_be_signed and append secret_key to the end with no
space or line break:
Build the signature:
openssl dgst \
-sha256 \
-sign /path/to/privatekey.pem \
/path/to/data_to_be_signed \
| base64 \
| tr -d '\n'
Where:
/path/to/privatekey.pem— path to the private key;/path/to/data_to_be_signed— path to the file with the request body and the appendedsecret_key.
Pass the resulting Base64 string in the X-Signature header. Send only the original JSON body to the API — without
the appended secret_key.
Signature error
If the API returns error 20002, check the following in order:
- The exact bytes of the sent JSON, without re-serialization, were used for the signature.
secret_keyis appended to the end of the body with no separator and is not sent in the JSON itself.- The private key used matches the public key provided to KVELL.
- SHA-256 and
PKCS#1 v1.5are used, and the result is Base64-encoded without extra line breaks. X-Signaturecarries the signature of exactly the current request, including its actual set and order of fields.