Skip to content

Public key for the cryptogram

Returns the current KVELL public RSA key. This key is used on the merchant's side to encrypt bank card details before sending them to KVELL: instead of plaintext details, a cryptogram is sent.

Integration flow

  1. Request the public key with this method.
  2. Store public_key and key_id in your cache until refresh_after.
  3. Encrypt the card details with the obtained key to get the cryptogram.
  4. Pass the cryptogram to the API method that accepts it.
  5. Before using the key again, check refresh_after and request the key again if needed.

URL

GET https://api.pay.kvell.group/v1/cryptogram/public-key
GET https://api.pay.stage.kvell.group/v1/cryptogram/public-key

Request

Headers

Name Type Required Description
X-Api-Key string Yes Shop identifier.

Example

curl --request GET \
  --url 'https://api.pay.stage.kvell.group/v1/cryptogram/public-key' \
  --header 'X-Api-Key: 00000000-0000-4000-8000-000000000000'

Response

Select an HTTP code to see an example, response parameters, and recommended actions.

Example 200 (OK) response
{
  "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6tJntpB49kze14gfIkQ\nAQIDAQAB\n-----END PUBLIC KEY-----\n",
  "key_id": "068eb60b259651c4",
  "refresh_after": "2026-08-20T10:20:48.650203Z"
}

Response parameters

Parameter Type Description
public_key string RSA-2048 public key in PEM format.
key_id string Key identifier. Computed as SHA-256 of the DER representation of SubjectPublicKeyInfo, truncated to the first 16 characters, so you can recompute it yourself from public_key.
refresh_after string The UTC moment, in ISO 8601, until which the returned key is guaranteed to be valid. See «Caching and key rotation» for details.

What to do next

  1. Store public_key and key_id in your cache until refresh_after.
  2. Use the key to build the cryptogram.
Example 403 (Forbidden) response
{
  "errors": [
    {
      "code": 20037,
      "message": "Access denied"
    }
  ]
}

Response parameters

Parameter Type Description
errors array List of errors. Codes and recommendations are given in «HTTP response errors».
errors[].code integer Error code. In the example — 20037.
errors[].message string Description of the reason access was denied.
Example 404 (Not Found) response
{
  "errors": [
    {
      "code": 20006,
      "message": "Shop not found"
    }
  ]
}

Response parameters

Parameter Type Description
errors array List of errors. Codes and recommendations are given in «HTTP response errors».
errors[].code integer Error code. In the example — 20006.
errors[].message string Description of the resource that was not found.
Example 422 (Unprocessable Entity) response
{
  "errors": [
    {
      "code": 20098,
      "message": "x-api-key: Field required"
    }
  ]
}

Response parameters

Parameter Type Description
errors array List of validation errors. Codes and recommendations are given in «HTTP response errors».
errors[].code integer Error code. For a field error — 20098.
errors[].message string The field and reason for the validation error.
Example 503 (Service Unavailable) response
{
  "errors": [
    {
      "code": 20043,
      "message": "The cryptogram public key is temporarily unavailable"
    }
  ]
}

Response parameters

Parameter Type Description
errors array List of errors. Codes and recommendations are given in «HTTP response errors».
errors[].code integer Technical error code.
errors[].message string Description of the technical error.

What to do next

  1. Safely retry the same GET request with the same X-Api-Key.
  2. If the error keeps recurring, contact KVELL support with the URL, request time, HTTP code, code, and message.

How the cryptogram is built

Working with card data

Card details are encrypted on the merchant's side, and only the cryptogram is ever sent to KVELL. Do not store or log the card number and CVV, including intermediate values before encryption.

The cryptogram is built in two steps.

  1. Build a string with the card details. Fields are separated by two colons, in a fixed order:

    pan::cvv::holder::exp_date
    
    Field Required Description
    pan Yes Card number.
    cvv No Card verification code.
    holder No Cardholder name.
    exp_date No Card expiration date, exactly 5 characters in YY/MM format, e.g. 26/01.

    Optional fields can be omitted by trimming the string from the right: 4111111111111111::123 and 4111111111111111 are also valid. A field in the middle cannot be skipped — an empty value will be parsed as an empty string, not as absent.

  2. Encrypt the string with the public key and Base64-encode the result.

    Parameter Value
    Algorithm RSA-OAEP
    Hash function SHA-256
    Mask generation function MGF1 with SHA-256
    Label Not used
    Result encoding Base64 with the standard alphabet and padding

With a 2048-bit key and OAEP with SHA-256, the maximum length of the source string is 190 bytes. Full card details fit within this limit.

Examples

The examples use the test card 4111111111111111 with expiration date 2034-12, written in the cryptogram as 34/12.

Required package: pip install cryptography.

import base64

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key

# value of the public_key field from the method response
public_key_pem = '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n'

public_key = load_pem_public_key(public_key_pem.encode('utf-8'))

payload = '4111111111111111::123::TEST HOLDER::34/12'

cryptogram = base64.b64encode(
    public_key.encrypt(
        payload.encode('utf-8'),
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )
).decode('ascii')

print(cryptogram)

Uses the built-in node:crypto module.

const crypto = require('node:crypto');

// value of the public_key field from the method response
const publicKeyPem = '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n';

const payload = '4111111111111111::123::TEST HOLDER::34/12';

const cryptogram = crypto.publicEncrypt(
  {
    key: publicKeyPem,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: 'sha256',
  },
  Buffer.from(payload, 'utf8'),
).toString('base64');

console.log(cryptogram);
# 1. Save the public key to a file
curl -s --request GET \
  --url 'https://api.pay.stage.kvell.group/v1/cryptogram/public-key' \
  --header 'X-Api-Key: 00000000-0000-4000-8000-000000000000' \
  | jq -r '.public_key' > public_key.pem

# 2. Encrypt the card details and Base64-encode the result
printf '%s' '4111111111111111::123::TEST HOLDER::34/12' \
  | openssl pkeyutl -encrypt -pubin -inkey public_key.pem \
      -pkeyopt rsa_padding_mode:oaep \
      -pkeyopt rsa_oaep_md:sha256 \
      -pkeyopt rsa_mgf1_md:sha256 \
  | openssl base64 -A

Caching and key rotation

The key can be cached. The refresh_after field indicates the moment until which the returned value is guaranteed to be valid; after that moment, request the key again. There is no need to request the key before every operation.

Key rotation does not require a synchronous cutover: cryptograms encrypted with a key obtained before the rotation are still accepted for some time afterward. To avoid relying on this grace period, honor refresh_after.

The key_id value changes together with the key, so it's convenient for detecting that the key has been updated and for referencing it when contacting support.