Skip to content

Building a signature for payouts

This section describes how to build the X-Signature for making payouts:

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

  1. Generate a private key:

    openssl genrsa -out privatekey.pem 4096
    
  2. Obtain the public part of the key:

    openssl rsa -in privatekey.pem -pubout -out publickey.pem
    
  3. 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

  1. Get the exact JSON request body as a UTF-8 encoded string.
  2. Append the shop's secret_key to the end of the string with no separator, space, or line break.
  3. Sign the resulting bytes with the private RSA key using SHA-256.
  4. Base64-encode the signature and pass the string in the X-Signature header.

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:

{"recipient_pan":"4111111111111111","amount":15000,"transaction":"payout-card-20260810-0001","description":"Payment under contract 42","customer":"customer@example.com"}<secret-key>
{"phone":"79991234567","fio":"Иванов Иван Иванович","bank_id":"100000000008","amount":15000,"transaction":"payout-sbp-20260810-0001","description":"Payment under contract 42"}<secret-key>

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 appended secret_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:

  1. The exact bytes of the sent JSON, without re-serialization, were used for the signature.
  2. secret_key is appended to the end of the body with no separator and is not sent in the JSON itself.
  3. The private key used matches the public key provided to KVELL.
  4. SHA-256 and PKCS#1 v1.5 are used, and the result is Base64-encoded without extra line breaks.
  5. X-Signature carries the signature of exactly the current request, including its actual set and order of fields.