Аутентификация
v4 ID поддерживает три способа авторизации запросов:
- OAuth 2.0 / OIDC — v4.business, docs.v4.business, pay.v4. Authorization code flow + PKCE. Discovery:
https://id.v4.life/.well-known/openid-configuration - API keys — long-lived tokens for integrations (n8n / Zapier / scripts). Created in /profile, max 10 per user. Header:
X-Api-Key: v4_…orAuthorization: ApiKey v4_… - Bearer JWT — short-lived access (1h) + 30-day refresh. Standard login/refresh flow for first-party clients
# API-key auth
curl https://id.v4.life/api/users/me/ \
-H "Authorization: ApiKey v4_<your-key>"
# OAuth2 access token
curl https://id.v4.life/api/users/me/ \
-H "Authorization: Bearer <oauth-access-token>"import requests
r = requests.get(
"https://id.v4.life/api/users/me/",
headers={"X-Api-Key": "v4_xxx..."},
)
print(r.json())const r = await fetch("https://id.v4.life/api/users/me/", {
headers: { "X-Api-Key": "v4_xxx..." },
});
const me = await r.json();Регистрация приложения
Приложение заводите сами, администратор Identity для этого не нужен. Нужен только вход в v4 ID: приложение принадлежит человеку, который его создал, и их не больше десяти на аккаунт.
# 1. Завести приложение (нужен свой токен v4 ID)
curl -X POST https://id.v4.life/api/oauth/apps/ \
-H "Authorization: Bearer <ваш-access-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Моё приложение",
"redirect_uris": ["https://example.kz/callback"],
"homepage_url": "https://example.kz"
}'
# Ответ содержит client_id и client_secret. Секрет — только здесь и только сейчас.
# 2. Свои приложения, новый секрет, удаление
curl https://id.v4.life/api/oauth/apps/ -H "Authorization: Bearer <token>"
curl -X POST https://id.v4.life/api/oauth/apps/<client_id>/secret/ -H "Authorization: Bearer <token>"
curl -X DELETE https://id.v4.life/api/oauth/apps/<client_id>/ -H "Authorization: Bearer <token>"Секрет показывается один раз — при создании и при перевыпуске. Хранится он хешем: достать повторно не можем ни мы, ни Вы, только выпустить новый. Новый гасит прежний сразу, потому что перевыпуск затевают, когда старый утёк.
Адрес возврата — только https; http допустим единственно на localhost, «#» в адресе запрещён стандартом. Схему нативного приложения (myapp://) выдаём отдельно — напишите нам.
Имя приложения человек читает на экране согласия и по нему решает, отдавать ли доступ к своим документам. Поэтому имена наших продуктов заняты: v4, v4.life, v4.business, v4.docs, Atrium и подобные.
Что не настраивается и почему. Способ выдачи — только код авторизации с PKCE: обмен пароля на токен и неявный поток существуют ради старого веба и сегодня небезопасны. Экран согласия не отключается — пропуск есть только у наших продуктов и включается руками, это решение о доверии, а не настройка.
Два токена и разница между ними
В обмен на код приходят два токена сразу. Их путают чаще всего остального, поэтому прямо:
POST https://id.v4.life/oauth/token/
grant_type=authorization_code&code=…&code_verifier=…&client_id=…
{
"access_token": "…", // в Authorization: Bearer — для запросов к API
"id_token": "eyJhbGciOiJSUzI1NiIs…", // кто вошёл; проверять по jwks_uri
"refresh_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}id_token— id_token — удостоверение. JWT с подписью RS256, ключи лежат в jwks_uri. Его читает Ваш сервер, чтобы узнать, кто вошёл. Проверяйте подпись, iss, aud и exp: непроверенный id_token не значит ничего — подделать текст внутри может кто угодно.access_token— access_token — ключ к данным. Его не разбирают и не читают, а кладут в заголовок Authorization при запросах к нашим API. Живёт час, продлевается refresh_token — тридцать дней.
Частая ошибка — отправить id_token в заголовке Authorization. Он не будет принят: это разные вещи по назначению, а не два имени одного токена.
Выход тоже общий. end_session_endpoint гасит сессию в самом Identity, а не только у Вас; без него человек выходит у Вас, а следующий вход проходит молча — и он считает, что не вышел.
OAuth scopes (21)
При запросе авторизации указывайте scopes в порядке нужды. Юзер увидит consent-экран и сможет выключить опциональные.
| Scope | Что даёт |
|---|---|
| openid | Идентификатор v4 ID (обязателен) |
| profile | Имя, фамилия, страна, язык |
| phone | Номер телефона |
| iin | ИИН (конфиденциально)по разговору |
| wallet:read | Просмотр баланса кошелькапо разговору |
| wallet:write | Инициировать платежипо разговору |
| docs:read | Читать документы юзера |
| docs:sign | Отправлять документы на подпись (подписывает сам человек в v4.life)по разговору |
| organizations:read | Список организаций |
| organizations:write | Управлять организациями |
| tasks:read | Читать задачи |
| tasks:write | Создавать и менять задачи |
| contacts:read | Читать контакты |
| contacts:write | Менять контакты |
| notes:read | Читать заметки |
| notes:write | Менять заметки |
| messages:read | Читать сообщения |
| calendar:read | Смотреть календарь |
| calendar:write | Ставить и переносить встречи от имени человека |
| calendar:freebusy | Видеть занятость без содержимого встреч |
Права с пометкой «по разговору» — ИИН, кошелёк, отправка на подпись и запись в календарь — включаются приложению вручную. Просто запросить их в authorize нельзя: невыданное право не появится на экране согласия.
Ещё одно право, introspection, в таблице не показано. Это служебная проверка чужих токенов, и она выдаётся только сервисам экосистемы.
Перечень не переписан сюда руками — страница спрашивает его у Identity при сборке: GET /api/oauth/scopes/
Про подписание: документы подписываются на стороне v4.life. Ваш сервис отправляет документ, человек подписывает его своей подписью v4 ID и Вы получаете результат. Приложение никогда не ставит подпись за человека — поэтому подключить подписание может любой сервис, своей инфраструктуры ЭЦП для этого не нужно.
Подписание документов
Движок подписания — v4.Docs (docs.v4.business), аккаунт общий с v4 ID. Ваш сервис отправляет документ, человек подписывает его в v4.life своей подписью, Вы получаете подписанный PDF и протокол. Собственная инфраструктура ЭЦП не нужна.
- Создаёте конверт с файлом и списком подписантов (email или телефон).
- Отправляете его — подписанты получают ссылку.
- Человек открывает документ и подписывает — сам, в v4.life. За него это не делает ни Ваш сервис, ни мы.
- Вам приходит webhook, забираете подписанный PDF и PDF протокола (кто, когда, каким способом, с какого адреса).
Способы подписи: код из SMS, ЭЦП НЦА (NCALayer), eGov Mobile, WebAuthn (Face ID / отпечаток). Какой доступен — зависит от страны и от того, что Вы разрешили в конверте.
| Запрос | Что даёт |
|---|---|
| POST /api/sign/envelopes/ | Создать конверт: файл, подписанты, порядок подписания |
| POST /api/sign/envelopes/<id>/send/ | Отправить подписантам — уходят приглашения |
| GET /api/sign/envelopes/<id>/ | Статус: кто открыл, кто подписал, кто отказался |
| GET /api/sign/envelopes/<id>/signed/ | Скачать подписанный PDF |
| GET /api/sign/envelopes/<id>/audit/pdf/ | Скачать протокол подписания (PDF) |
| POST /api/sign/envelopes/<id>/cancel/ | Отозвать конверт до завершения |
События подписания идут той же единой шиной, что и остальные: те же заголовки, та же подпись и та же политика повторов.
| Event | Когда |
|---|---|
| sign.envelope.sent | Конверт отправлен подписантам |
| sign.signer.viewed | Подписант открыл документ |
| sign.signer.signed | Подписант подписал |
| sign.signer.declined | Подписант отказался (с причиной) |
| sign.envelope.completed | Все подписанты подписали — конверт завершён |
| sign.envelope.cancelled | Конверт отозван отправителем |
| sign.signature.revoked | Подпись отозвана |
X-V4-Event: sign.envelope.completed
X-V4-Event-Id: evt_…
X-V4-Timestamp: 1755075695
X-V4-Signature: sha256=<hex>
Content-Type: application/jsonЧеловек видит все свои подписи в v4.life — раздел «Где я расписался»: документ, способ, момент, от кого пришло. Это же делает Вашу интеграцию заметной для него.
Webhook events (8)
Создайте webhook в /profile → выберите события → получите signing secret один раз. На каждое событие v4 ID шлёт POST на ваш URL с HMAC-SHA256 подписью.
| Event | Когда |
|---|---|
| user.created | Юзер зарегистрировался |
| user.login_success | Успешный логин |
| user.login_new_device | Логин с нового устройства |
| user.deleted | Юзер запросил удаление (soft-delete с 30-day grace) |
| user.restored | Восстановление soft-deleted аккаунта |
| password.changed | Юзер сменил пароль |
| 2fa.enabled | Включён TOTP 2FA |
| 2fa.disabled | Отключён TOTP 2FA |
Headers
X-V4-Event: sign.signer.signed
X-V4-Event-Id: evt_88fd021f… # ключ дедупликации, при повторах тот же
X-V4-Delivery: dlv_72228e87… # своя на каждую попытку
X-V4-Attempt: 1
X-V4-Timestamp: 1755075695 # unix-секунды; участвуют в подписи
X-V4-Signature: sha256=<hex>
Content-Type: application/jsonPayload
{
"id": "evt_88fd021fa7b14c5a92b38c69782baebf",
"type": "sign.signer.signed",
"version": 1,
"source": "v4.docs",
"occurred_at": "2026-08-13T09:08:15Z",
"actor": { "user_id": "4a2ab206-61fb-4ba8-a605-447797a134d6" },
"subject": { "type": "envelope", "id": "f29087ed-c21f-4024-b94b-6e479f54ee15" },
"audience": [ { "user_id": "4a2ab206-…", "email": "signer@example.kz" } ],
"delivery": { "id": "dlv_72228e87a9ea4c3b83f523ab7728b6ad", "attempt": 1 },
"data": {
"title": "Договор № 17",
"method": "ncalayer",
"signer_email": "signer@example.kz",
"envelope_status": "completed",
"signed_at": "2026-08-13T09:08:15Z"
}
}Verify signature
Сравните X-V4-Signature с HMAC-SHA256 от строки «timestamp.тело» по вашему secret. Тело берите сырым — пересобранный JSON даст другую подпись:
import hmac, hashlib, time
MAX_SKEW = 300 # 5 минут: доставку с более старой меткой не принимаем
def verify(secrets: list, body: bytes, ts: str, sig_header: str) -> bool:
"""secrets — список: на время ротации ключа действуют два секрета сразу."""
try:
if abs(time.time() - int(ts)) > MAX_SKEW: # X-V4-Timestamp
return False
except (TypeError, ValueError):
return False
# Подписывается строка "timestamp.тело", и тело — СЫРОЕ.
# Пересериализованный JSON даст другой порядок ключей и другую подпись.
payload = ts.encode() + b"." + body
for secret in secrets:
expected = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, sig_header): # не ==, чтобы не утекало время
return True
return FalseRetry policy
При 5xx или сетевой ошибке Celery повторяет доставку с exp.backoff: 30s · 2m · 10m · 1h · 6h (max 5 попыток). Идемпотентность — клиент должен дедуплицировать по X-V4-Event-Id.
Поддержка
Вопросы интеграции — admin@v4.life. Известные ограничения и changelog — публикуем в changelog (планируется).