v4.lifeID

Developer documentation

OAuth 2.0 / OIDC · API keys · Outgoing webhooks с HMAC-подписью. Всё что нужно чтобы интегрироваться с единой идентификацией v4.

Swagger UI →ReDocRaw OpenAPI 3.0

Аутентификация

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_… or Authorization: ApiKey v4_…
  • Bearer JWT — short-lived access (1h) + 30-day refresh. Standard login/refresh flow for first-party clients
curl
# 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>"
python
import requests
r = requests.get(
    "https://id.v4.life/api/users/me/",
    headers={"X-Api-Key": "v4_xxx..."},
)
print(r.json())
javascript
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: приложение принадлежит человеку, который его создал, и их не больше десяти на аккаунт.

curl
# 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: обмен пароля на токен и неявный поток существуют ради старого веба и сегодня небезопасны. Экран согласия не отключается — пропуск есть только у наших продуктов и включается руками, это решение о доверии, а не настройка.

Два токена и разница между ними

В обмен на код приходят два токена сразу. Их путают чаще всего остального, поэтому прямо:

HTTP
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_tokenid_token — удостоверение. JWT с подписью RS256, ключи лежат в jwks_uri. Его читает Ваш сервер, чтобы узнать, кто вошёл. Проверяйте подпись, iss, aud и exp: непроверенный id_token не значит ничего — подделать текст внутри может кто угодно.
  • access_tokenaccess_token — ключ к данным. Его не разбирают и не читают, а кладут в заголовок Authorization при запросах к нашим API. Живёт час, продлевается refresh_token — тридцать дней.

Частая ошибка — отправить id_token в заголовке Authorization. Он не будет принят: это разные вещи по назначению, а не два имени одного токена.

Выход тоже общий. end_session_endpoint гасит сессию в самом Identity, а не только у Вас; без него человек выходит у Вас, а следующий вход проходит молча — и он считает, что не вышел.

OAuth scopes (21)

При запросе авторизации указывайте scopes в порядке нужды. Юзер увидит consent-экран и сможет выключить опциональные.

ScopeЧто даёт
openidИдентификатор v4 ID (обязателен)
profileИмя, фамилия, страна, язык
emailEmail
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 и протокол. Собственная инфраструктура ЭЦП не нужна.

  1. Создаёте конверт с файлом и списком подписантов (email или телефон).
  2. Отправляете его — подписанты получают ссылку.
  3. Человек открывает документ и подписывает — сам, в v4.life. За него это не делает ни Ваш сервис, ни мы.
  4. Вам приходит 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Подпись отозвана
headers
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

HTTP
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/json

Payload

JSON
{
  "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 даст другую подпись:

python
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 False

Retry policy

При 5xx или сетевой ошибке Celery повторяет доставку с exp.backoff: 30s · 2m · 10m · 1h · 6h (max 5 попыток). Идемпотентность — клиент должен дедуплицировать по X-V4-Event-Id.

Поддержка

Вопросы интеграции — admin@v4.life. Известные ограничения и changelog — публикуем в changelog (планируется).