Подписывайте запросы ed25519 своим ключом. Ниже готовые примеры на шести языках.
nspkID и ссылки оплаты. Ниже
описана подпись запросов своим ключом для продакшена.
Каждый непубличный запрос подписывается ключом ed25519. Заголовок
Authorization содержит JSON-объект:
{
"partnerID": "<ваш partnerID>",
"key": "ssh-ed25519 AAAA... (ваш публичный ключ)",
"sign": "<base64 ed25519-подписи>"
}
POST/PUT/PATCH: sign = base64(ed25519_sign(точные_байты_тела)).GET/DELETE: sign = base64(ed25519_sign(полный_URL_в_нижнем_регистре)).POST обязано содержать ваш partnerID.
Для POST /qr добавьте ещё partnerqrID, равный вашему partnerID.partnerID и передаёт Paylow свой публичный ключ ed25519. Приватный ключ (32-байтовый seed) остаётся только у вас.| Параметр | Что это |
|---|---|
PARTNER_ID | Ваш partnerID (например 4payXXXX-....). |
PUBLIC_KEY | Строка ssh-ed25519 AAAA..., которую вы передали Paylow. |
SEED_B64 | Ваш приватный ключ как base64 32-байтового seed ed25519. |
-----BEGIN OPENSSH PRIVATE KEY-----), извлеките из
него 32-байтовый seed. Готовый разбор есть в локальном тест-клиенте
examples/test_client.py (он принимает и base64-seed, и файл OpenSSH).
POST /qr)/*
* Paylow SBP QR & Card API. Подпись запроса на Node.js.
* Установка: npm i tweetnacl
* Подпись ed25519; для POST подписывается тело; результат в base64.
*/
const https = require("https");
const nacl = require("tweetnacl");
// --- Ваши учётные данные ---
const PARTNER_ID = "<ВАШ partnerID>";
const PUBLIC_KEY = "ssh-ed25519 AAAA... (ВАШ публичный ключ)";
const SEED_B64 = "<ВАШ приватный ключ: base64 32-байтового seed>";
const seed = Uint8Array.from(Buffer.from(SEED_B64, "base64")); // 32 байта
const kp = nacl.sign.keyPair.fromSeed(seed);
// Тело обязано содержать partnerID; для /qr также partnerqrID === partnerID.
const body = JSON.stringify({
amount: 100, currency: "RUB", qrType: "QRDynamic",
qrDescription: "QR для заказа 126", order: "order-126",
callbackUrl: "https://partner.example/callback",
partnerID: PARTNER_ID, partnerqrID: PARTNER_ID,
});
const sign = Buffer.from(nacl.sign.detached(Buffer.from(body, "utf8"), kp.secretKey)).toString("base64");
const authorization = JSON.stringify({ partnerID: PARTNER_ID, key: PUBLIC_KEY, sign });
const req = https.request({
method: "POST", host: "qr.paylow.online", path: "/api/qr",
headers: { "Authorization": authorization, "Content-Type": "application/json" },
}, (res) => { let d = ""; res.on("data", c => d += c); res.on("end", () => console.log("HTTP", res.statusCode, d)); });
req.write(body); req.end();
"""
Paylow SBP QR & Card API. Подпись запроса на Python.
Установка: pip install cryptography requests
"""
import base64, json, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
# --- Ваши учётные данные ---
PARTNER_ID = "<ВАШ partnerID>"
PUBLIC_KEY = "ssh-ed25519 AAAA... (ВАШ публичный ключ)"
SEED_B64 = "<ВАШ приватный ключ: base64 32-байтового seed>"
key = Ed25519PrivateKey.from_private_bytes(base64.b64decode(SEED_B64)) # 32 байта
# Тело обязано содержать partnerID; для /qr также partnerqrID == partnerID.
body = json.dumps({
"amount": 100, "currency": "RUB", "qrType": "QRDynamic",
"qrDescription": "QR для заказа 126", "order": "order-126",
"callbackUrl": "https://partner.example/callback",
"partnerID": PARTNER_ID, "partnerqrID": PARTNER_ID,
}, separators=(",", ":"))
sign = base64.b64encode(key.sign(body.encode("utf-8"))).decode("ascii")
authorization = json.dumps({"partnerID": PARTNER_ID, "key": PUBLIC_KEY, "sign": sign})
resp = requests.post("https://qr.paylow.online/api/qr",
data=body.encode("utf-8"),
headers={"Authorization": authorization, "Content-Type": "application/json"},
timeout=20)
print("HTTP", resp.status_code, resp.text)
// Paylow SBP QR & Card API. Подпись запроса на Go (только stdlib).
package main
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// --- Ваши учётные данные ---
const partnerID = "<ВАШ partnerID>"
const publicKey = "ssh-ed25519 AAAA... (ВАШ публичный ключ)"
const seedB64 = "<ВАШ приватный ключ: base64 32-байтового seed>"
func main() {
seed, _ := base64.StdEncoding.DecodeString(seedB64) // 32 байта
priv := ed25519.NewKeyFromSeed(seed)
// Тело обязано содержать partnerID; для /qr также partnerqrID == partnerID.
body := `{"amount":100,"currency":"RUB","qrType":"QRDynamic","qrDescription":"QR для заказа 126","order":"order-126","callbackUrl":"https://partner.example/callback","partnerID":"` + partnerID + `","partnerqrID":"` + partnerID + `"}`
sign := base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(body)))
auth, _ := json.Marshal(map[string]string{"partnerID": partnerID, "key": publicKey, "sign": sign})
req, _ := http.NewRequest("POST", "https://qr.paylow.online/api/qr", strings.NewReader(body))
req.Header.Set("Authorization", string(auth))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { fmt.Println("Ошибка:", err); return }
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
fmt.Println("HTTP", resp.StatusCode, string(b))
}
// Paylow SBP QR & Card API. Подпись запроса на Rust.
// Cargo.toml:
// ed25519-dalek = "2"
// base64 = "0.22"
// reqwest = { version = "0.12", features = ["blocking"] }
use base64::Engine;
use ed25519_dalek::{Signer, SigningKey};
// --- Ваши учётные данные ---
const PARTNER_ID: &str = "<ВАШ partnerID>";
const PUBLIC_KEY: &str = "ssh-ed25519 AAAA... (ВАШ публичный ключ)";
const SEED_B64: &str = "<ВАШ приватный ключ: base64 32-байтового seed>";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let seed_vec = base64::engine::general_purpose::STANDARD.decode(SEED_B64)?;
let seed: [u8; 32] = seed_vec.as_slice().try_into()?;
let signing = SigningKey::from_bytes(&seed);
// Тело обязано содержать partnerID; для /qr также partnerqrID == partnerID.
let body = format!(
r#"{{"amount":100,"currency":"RUB","qrType":"QRDynamic","qrDescription":"QR для заказа 126","order":"order-126","callbackUrl":"https://partner.example/callback","partnerID":"{p}","partnerqrID":"{p}"}}"#,
p = PARTNER_ID
);
let sig = signing.sign(body.as_bytes());
let sign = base64::engine::general_purpose::STANDARD.encode(sig.to_bytes());
let authorization = format!(r#"{{"partnerID":"{}","key":"{}","sign":"{}"}}"#, PARTNER_ID, PUBLIC_KEY, sign);
let client = reqwest::blocking::Client::new();
let resp = client.post("https://qr.paylow.online/api/qr")
.header("Authorization", authorization)
.header("Content-Type", "application/json")
.body(body).send()?;
println!("HTTP {} {}", resp.status(), resp.text()?);
Ok(())
}
// Paylow SBP QR & Card API. Подпись запроса на Java (11+).
// Зависимость: BouncyCastle (org.bouncycastle:bcprov-jdk18on:1.78+).
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
public class SignQr {
// --- Ваши учётные данные ---
static final String PARTNER_ID = "<ВАШ partnerID>";
static final String PUBLIC_KEY = "ssh-ed25519 AAAA... (ВАШ публичный ключ)";
static final String SEED_B64 = "<ВАШ приватный ключ: base64 32-байтового seed>";
public static void main(String[] args) throws Exception {
byte[] seed = Base64.getDecoder().decode(SEED_B64); // 32 байта
Ed25519PrivateKeyParameters priv = new Ed25519PrivateKeyParameters(seed, 0);
// Тело обязано содержать partnerID; для /qr также partnerqrID == partnerID.
String body = String.format(
"{\"amount\":100,\"currency\":\"RUB\",\"qrType\":\"QRDynamic\","
+ "\"qrDescription\":\"QR для заказа 126\",\"order\":\"order-126\","
+ "\"callbackUrl\":\"https://partner.example/callback\","
+ "\"partnerID\":\"%s\",\"partnerqrID\":\"%s\"}", PARTNER_ID, PARTNER_ID);
byte[] msg = body.getBytes(StandardCharsets.UTF_8);
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, priv);
signer.update(msg, 0, msg.length);
String sign = Base64.getEncoder().encodeToString(signer.generateSignature());
String authorization = String.format(
"{\"partnerID\":\"%s\",\"key\":\"%s\",\"sign\":\"%s\"}", PARTNER_ID, PUBLIC_KEY, sign);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://qr.paylow.online/api/qr"))
.header("Authorization", authorization)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build();
HttpResponse<String> resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP " + resp.statusCode() + " " + resp.body());
}
}
// Paylow SBP QR & Card API. Подпись запроса на C# (.NET 6+).
// NuGet: dotnet add package BouncyCastle.Cryptography
using System;
using System.Net.Http;
using System.Text;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
class SignQr
{
// --- Ваши учётные данные ---
const string PartnerId = "<ВАШ partnerID>";
const string PublicKey = "ssh-ed25519 AAAA... (ВАШ публичный ключ)";
const string SeedB64 = "<ВАШ приватный ключ: base64 32-байтового seed>";
static async System.Threading.Tasks.Task Main()
{
byte[] seed = Convert.FromBase64String(SeedB64); // 32 байта
var priv = new Ed25519PrivateKeyParameters(seed, 0);
// Тело обязано содержать partnerID; для /qr также partnerqrID == partnerID.
string body = $"{{\"amount\":100,\"currency\":\"RUB\",\"qrType\":\"QRDynamic\","
+ "\"qrDescription\":\"QR для заказа 126\",\"order\":\"order-126\","
+ "\"callbackUrl\":\"https://partner.example/callback\","
+ $"\"partnerID\":\"{PartnerId}\",\"partnerqrID\":\"{PartnerId}\"}}";
byte[] msg = Encoding.UTF8.GetBytes(body);
var signer = new Ed25519Signer();
signer.Init(true, priv);
signer.BlockUpdate(msg, 0, msg.Length);
string sign = Convert.ToBase64String(signer.GenerateSignature());
string authorization =
$"{{\"partnerID\":\"{PartnerId}\",\"key\":\"{PublicKey}\",\"sign\":\"{sign}\"}}";
using var client = new HttpClient();
var content = new StringContent(body, Encoding.UTF8, "application/json");
client.DefaultRequestHeaders.Add("Authorization", authorization);
var resp = await client.PostAsync("https://qr.paylow.online/api/qr", content);
Console.WriteLine($"HTTP {(int)resp.StatusCode} {await resp.Content.ReadAsStringAsync()}");
}
}
Для GET/DELETE подпись считается по полному URL в нижнем регистре.
Пример (Python): статус динамического QR по nspkID.
url = f"https://qr.paylow.online/api/qr/dynamic/{PARTNER_ID}/{nspkID}"
sign = base64.b64encode(key.sign(url.lower().encode())).decode()
auth = json.dumps({"partnerID": PARTNER_ID, "key": PUBLIC_KEY, "sign": sign})
requests.get(url, headers={"Authorization": auth})
Для первой оплаты по подписке создайте карточный платёж через POST /card
с полем binding: true. После успешной оплаты сохраните
bindingID из ответа, статуса платежа или webhook.
{
"amount": 100,
"currency": "RUB",
"orderID": "sub-first-001",
"description": "Первый платёж по подписке",
"callbackUrl": "https://partner.example/card-callback",
"returnUrl": "https://partner.example/return",
"binding": true,
"partnerID": "<ваш partnerID>"
}
Для следующего регулярного списания создайте новый платёж тем же методом
POST /card, но передайте сохранённый bindingID и новый
orderID.
{
"amount": 100,
"currency": "RUB",
"orderID": "sub-renewal-002",
"description": "Регулярное списание по подписке",
"callbackUrl": "https://partner.example/card-callback",
"binding": true,
"bindingID": "<bindingID сохранённой карты>",
"partnerID": "<ваш partnerID>"
}
Проверить интеграцию можно консольным клиентом
examples/test_client.py. Он подписывает запросы вашим ключом
локально (ключ никуда не отправляется) и обращается к реальному API:
# переменные окружения (или vn.config.json рядом со скриптом)
set VN_PARTNER_ID=<ВАШ partnerID>
set VN_PUBLIC_KEY=ssh-ed25519 AAAA...
set VN_PRIVATE_SEED=<base64 seed>
python examples/test_client.py partner
python examples/test_client.py create-qr --amount 100 --callback https://partner.example/cb
python examples/test_client.py qr-status <nspkID>
python examples/test_client.py create-card --amount 100 --callback https://partner.example/cb
python examples/test_client.py create-card --amount 100 --callback https://partner.example/cb --binding
python examples/test_client.py create-card --amount 100 --callback https://partner.example/cb --binding-id <bindingID>