Начало работы
Это краткое руководство поможет вам шаг за шагом: создать API-ключ и выполнить первые запросы к Pert API.
В итоге вы сможете:
- создать и подтвердить API-пользователя
- создать vault-аккаунт
- отправить свою первую транзакцию
Как работает аутентификация API
Pert использует асимметричную криптографию для защиты взаимодействия с API. Проще говоря, это работает так:
| Компонент | Описание |
|---|---|
| Секретный API-ключ (приватный ключ) |
Хранится только у вас и используется для подписи авторизационного запроса, чтобы подтвердить, что он отправлен именно вами |
| Публичный ключ | Находится у Pert и нужен для проверки, что запрос действительно подписан вашим приватным ключом |
Такая архитектура гарантирует, что ваш секретный API-ключ никогда не покидает ваше окружение — в Pert передаётся только публичный ключ. Мы не можем подписывать запросы за вас, и даже в случае компрометации системы Pert ваши учётные данные останутся в безопасности.
Шаг 1: Генерация пары ключей
В каждом запросе к API Pert должен передаваться авторизационный JWT-токен:
| Заголовок | Значение |
|---|---|
| Authorization | Bearer JWT_TOKEN |
Для получения авторизационного JWT-токена необходимо отправить запрос подписанный ассиметричным ключом шифрования.
Используйте OpenSSL для генерации пары ключей ed25519
# генерируем приватный ключ
openssl genpkey -algorithm ED25519 -out "pert_secret.pem"
# генерируем публичный ключ
openssl pkey -in "pert_secret.pem" -pubout -out "pert_public.pem"
В результате выполнения этой команды создаются:
- pert_secret.pem — ваш приватный Ed25519-ключ, который используется как API-секрет
- pert_public.pem — файл публичного ключа, который нужно будет загрузить при создании API-ключа
Обязательно храните файл с секретным ключом в надёжном и безопасном месте.
Шаг 2: Создание API-пользователя
Чтобы создать API-пользователя с правами подписи, выполните следующие шаги:
- В консоли Pert откройте раздел Пользователи → По API
- Нажмите Создать пользователя
- Укажите название и описание(не обязательно) пользователя, выберите нужную роль (например, SIGNER, если он должен подписывать транзакции) и загрузите файл публичного ключа, созданный на шаге 1
- Нажмите Сохранить
После одобрения заявки новый API-пользователь появится в списке. Вы получите API Key (ID пользователя API) — уникальный идентификатор, который используется вместе с вашим секретным ключом.
Шаг 3: Получение авторизационного токена
Чтобы получить JWT-токен, отправьте подписанный JSON-payload на эндпоинт POST /api/v1/auth/access-token.
Структура подписываемого payload:
| Поле | Тип | Описание |
|---|---|---|
client_type |
string | user для API-пользователя |
client_id |
string | UUID API-пользователя (выдан на шаге 2) |
timestamp |
int64 | Unix-время в секундах. Должно быть в окне ±5 минут от серверного времени |
nonce |
string | Случайная строка для защиты от replay-атак (16+ байт энтропии, base64 или hex) |
Тело запроса:
| Поле | Тип | Описание |
|---|---|---|
data |
string | base64-кодированный JSON payload |
signature |
string | base64-кодированная Ed25519-подпись сырых байт JSON (до base64-кодирования) |
Подпись считается над сырыми байтами JSON
Подписывается JSON до base64-кодирования. Не подписывайте base64-строку и не пересериализуйте JSON между подписью и отправкой — это сломает верификацию на стороне сервера.
Используйте серверное время
Если на вашем хосте сильный clock drift, запросы будут отвергаться с ошибкой
request timestamp too old / too far in future. Получите серверное время через
GET /api/v1/auth/time и используйте его при формировании timestamp.
Пример
import base64
import json
import secrets
import time
import requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
BASE_URL = "https://auth.pert.paranoid.security"
API_USER_ID = "<API_USER_UUID>"
SECRET_PEM = "pert_secret.pem"
with open(SECRET_PEM, "rb") as f:
private_key = load_pem_private_key(f.read(), password=None)
payload = {
"client_type": "user",
"client_id": API_USER_ID,
"timestamp": int(time.time()),
"nonce": secrets.token_urlsafe(16),
}
data_bytes = json.dumps(payload, separators=(",", ":")).encode()
signature = private_key.sign(data_bytes)
resp = requests.post(
f"{BASE_URL}/api/v1/auth/access-token",
json={
"data": base64.b64encode(data_bytes).decode(),
"signature": base64.b64encode(signature).decode(),
},
timeout=10,
)
resp.raise_for_status()
access_token = resp.json()["access_token"]
package main
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"net/http"
"os"
"time"
)
type signed struct {
ClientType string `json:"client_type"`
ClientID string `json:"client_id"`
Timestamp int64 `json:"timestamp"`
Nonce string `json:"nonce"`
}
func main() {
pemBytes, _ := os.ReadFile("pert_secret.pem")
block, _ := pem.Decode(pemBytes)
keyAny, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
priv := keyAny.(ed25519.PrivateKey)
nonce := make([]byte, 16)
_, _ = rand.Read(nonce)
data, _ := json.Marshal(signed{
ClientType: "user",
ClientID: "<API_USER_UUID>",
Timestamp: time.Now().Unix(),
Nonce: base64.RawURLEncoding.EncodeToString(nonce),
})
sig := ed25519.Sign(priv, data)
body, _ := json.Marshal(map[string]string{
"data": base64.StdEncoding.EncodeToString(data),
"signature": base64.StdEncoding.EncodeToString(sig),
})
http.Post("https://auth.pert.paranoid.security/api/v1/auth/access-token",
"application/json", bytes.NewReader(body))
}
# подписываемый payload (timestamp — секунды)
DATA_JSON=$(printf '{"client_type":"user","client_id":"%s","timestamp":%d,"nonce":"%s"}' \
"$API_USER_ID" "$(date +%s)" "$(openssl rand -base64 16 | tr -d '=\n')")
# подпись над сырым JSON
DATA_B64=$(printf '%s' "$DATA_JSON" | base64 | tr -d '\n')
SIG_B64=$(printf '%s' "$DATA_JSON" \
| openssl pkeyutl -sign -inkey pert_secret.pem -rawin \
| base64 | tr -d '\n')
curl -X POST https://auth.pert.paranoid.security/api/v1/auth/access-token \
-H "Content-Type: application/json" \
-d "{\"data\":\"$DATA_B64\",\"signature\":\"$SIG_B64\"}"
Ответ:
{
"access_token": "eyJhbGciOi...",
"expires_at": "1777300943056"
}
| Поле | Описание |
|---|---|
access_token |
Bearer-токен, передавайте в заголовке Authorization: Bearer <token> |
expires_at |
Время истечения токена в миллисекундах Unix (строкой) |
Кешируйте токен и обновляйте заранее
Не запрашивайте новый токен на каждый вызов API. Сохраните access_token
и обновите его за ~30 секунд до expires_at, повторив процедуру подписи.
Шаг 4: Создание vault-аккаунта
Все остальные запросы помимо Authorization требуют заголовок X-Workspace-Id —
UUID вашего workspace. Узнать его можно в консоли (URL …/workspaces/<UUID>/…)
или через GET /api/v1/workspace.
Создайте vault — логический контейнер для адресов в разных валютах:
curl -X POST https://wallet.pert.paranoid.security/api/v1/vault \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{"name": "My first vault"}'
Ответ содержит id созданного vault — он понадобится дальше.
Добавьте wallet нужной валюты — для каждой валюты в vault создаётся
отдельный wallet с депозитным адресом. Список доступных валют — GET /api/v1/currencies:
curl -X POST https://wallet.pert.paranoid.security/api/v1/wallet \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{"vault_id": "'"$VAULT_ID"'", "currency_id": "'"$CURRENCY_ID"'"}'
В ответе вернётся массив assets — каждый элемент содержит id wallet и его
депозитный address. id wallet — это source_wallet_id для исходящих транзакций.
Шаг 5: Отправка первой транзакции
Перед отправкой можно оценить комиссию через POST /api/v1/transactions/fee —
это не обязательно, но рекомендуется.
Создание транзакции:
curl -X POST https://wallet.pert.paranoid.security/api/v1/transaction \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"source_wallet_id": "'"$SOURCE_WALLET_ID"'",
"target_address": "'"$TARGET_ADDRESS"'",
"amount": "0.01"
}'
| Заголовок | Описание |
|---|---|
X-Idempotency-Key |
UUID запроса. Повторный запрос с тем же ключом не создаст вторую транзакцию (вернёт 409) |
Ответ 201 Created содержит созданную транзакцию с её id и начальным статусом.
Транзакция не отправляется в сеть мгновенно — она проходит через скрининг,
аппрувалы по политикам и MPC-подписание. Финальный статус (broadcasted,
confirmed, rejected и т.д.) приходит позже.
Подробнее про статусы и pipeline — в разделе Pert API.
Что дальше
- Авторизация — детали JWT-токенов, обновление, introspect.
- Webhooks — подпишитесь на события, чтобы получать обновления по транзакциям и депозитам без поллинга.
- Pert API — полный справочник эндпоинтов.
- Коды ошибок — что означают коды ответа и как их обрабатывать.