Yord Tech

Yord ID для разработчиков

Вход через Yord ID — стандартный OpenID Connect (Authorization Code + PKCE). Подключается любой OIDC-библиотекой по одному адресу.

1. Получить client_id

Напишите на info@yord.tech: название продукта и адреса возврата (redirect_uri). Пришлём client_id и client_secret. Мобильное или SPA-приложение — публичный клиент: без секрета, только PKCE; redirect_uri вида space.yord.app:/callback. Адрес возврата сверяется посимвольно. Адрес после выхода (post_logout_redirect_uri) тоже должен быть в этом списке.

2. Адреса

discovery  https://id.yord.space/.well-known/openid-configuration
authorize  https://id.yord.space/authorize
token      https://id.yord.space/token
userinfo   https://id.yord.space/userinfo
jwks       https://id.yord.space/jwks
logout     https://id.yord.space/logout

3. Отправить пользователя на вход

GET https://id.yord.space/authorize?response_type=code
  &client_id=<client_id>
  &redirect_uri=https://продукт/auth/yord/callback
  &scope=openid profile email phone
  &state=<случайная строка — сверить на возврате>
  &nonce=<необязательно; если передали — сверьте в id_token>
  &code_challenge=<BASE64URL(SHA256(code_verifier))>
  &code_challenge_method=S256
  [&prompt=login — спросить вход, даже если пользователь уже вошёл]
  [&login_methods=phone,vk,yandex — какие способы входа показать]
  [&require=phone — без подтверждённого телефона кода не будет]

Если пользователь уже вошёл в Yord ID в этом браузере, он сразу вернётся в продукт — без экрана входа. На возврате: ?code=…&state=…, при ошибке — ?error=…&error_description=…&state=….

login_methods — способы входа через запятую, которые увидит человек на экране входа Yord ID: phone (телефон через бота Telegram или MAX), email (код на почту), vk, ok, mail, yandex, telegram, google, apple. Неизвестные значения пропускаются; если не осталось ни одного или параметра нет — показываются все способы. Скрытым способом войти в рамках этого запроса нельзя, но человек, который уже вошёл в Yord ID, вернётся в продукт сразу: аккаунт один, способ входа — только ключ к нему (кроме prompt=login). На экране входа есть ссылка «Отменить вход» — продукт получит error=access_denied.

require — phone, email или оба через запятую; каждому нужен такой же scope (require=phone без scope phone — ошибка invalid_request). Если в аккаунте нет подтверждённого контакта, Yord ID попросит добавить его (телефон — через бота, почту — кодом) и только потом вернёт код. Отказался — вернётся error=access_denied. Если вход в Yord ID был больше 10 минут назад, перед добавлением контакта человек входит ещё раз — любым своим способом, login_methods здесь не действует.

GET https://id.yord.space/authorize?…&scope=openid phone&login_methods=phone,vk&require=phone

отмена:  https://продукт/auth/yord/callback?error=access_denied
           &error_description=Пользователь не добавил телефон&state=…

4. Обменять код на токены (с сервера продукта)

POST https://id.yord.space/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<тот же>&code_verifier=<verifier>
&client_id=<client_id>&client_secret=<secret>      (или Authorization: Basic)

200 OK
{"access_token": "eyJ…", "token_type": "Bearer", "expires_in": 900,
 "id_token": "eyJ…", "refresh_token": "…", "scope": "email openid profile"}

Код живёт 60 секунд и срабатывает один раз. id_token проверяйте по ключам из jwks (RS256, iss = https://id.yord.space, aud = ваш client_id, nonce).

5. Данные пользователя

В id_token и в GET https://id.yord.space/userinfo с заголовком Authorization: Bearer <access_token>:

sub                    постоянный ID пользователя во всех продуктах — храните его
name, picture          scope profile
given_name, family_name, middle_name, birthdate   scope profile (отчество и дата — если указаны; дата YYYY-MM-DD)
yord_identities        scope profile: ["vk", "telegram", …] — чем человек может входить
email, email_verified  scope email (почта всегда подтверждена)
phone_number, phone_number_verified   scope phone

6. Обновить токены

POST /token  grant_type=refresh_token&refresh_token=<…>&client_id=…&client_secret=…

refresh_token живёт 30 дней и одноразовый: в ответе — новый. Повтор старого отзывает всю цепочку — пользователю придётся войти заново.

7. Выход

GET https://id.yord.space/logout?client_id=<client_id>&post_logout_redirect_uri=<адрес из списка>&state=<…>

Если передаёте id_token_hint, отправляйте запрос методом POST (поля формы): так токен не попадёт в URL и логи.

8. Ошибки /token

400 invalid_grant           код или refresh_token недействителен, использован, истёк; не тот redirect_uri или verifier
400 unsupported_grant_type  только authorization_code и refresh_token
401 invalid_client          неизвестный client_id или неверный секрет
429 rate_limited            больше 30 запросов в минуту на клиента с одного IP

9. Примеры

Python, модуль oauth_ru (тот же, что для VK ID и Яндекс ID):

cfg = oauth_ru.Config(client_id=..., client_secret=..., scope='openid profile email phone',
                      redirect_uri='https://продукт/auth/yord/callback')
url = oauth_ru.authorize_url('yord', cfg, state, challenge)          # → редирект
token = await oauth_ru.exchange('yord', cfg, cb, verifier, client)   # на callback
profile = oauth_ru.normalize('yord', await oauth_ru.fetch_profile('yord', cfg, token, client))

oauth_ru пока не передаёт nonce: защиту от подмены даёт PKCE + state.

Node, Auth.js:

providers: [{ id: 'yord', name: 'Yord ID', type: 'oidc', issuer: 'https://id.yord.space',
              clientId: process.env.YORD_CLIENT_ID, clientSecret: process.env.YORD_CLIENT_SECRET,
              authorization: { params: { scope: 'openid profile email phone' } } }]

iOS / Android: AppAuth с discovery-адресом выше; вход открывается в системном браузере (ASWebAuthenticationSession / Custom Tabs), не во встроенном WebView.

10. Документы пользователя

Паспорт, СНИЛС, ИНН и другие документы, проверенные сотрудником Yord Tech. Только продуктам с секретом, которым Yord Tech подключил нужные документы (юрлицо, ИНН и цель указываются при подключении). scope: passport, passport_registration, foreign_passport, foreign_id, snils, inn, oms, driver_license, address. Человек видит экран согласия с вашим юрлицом, ИНН, перечнем и целью; согласие действует 1 год.

POST https://id.yord.space/api/documents
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

token=<access_token>

200 OK
{"passport": {"verified_at": "2026-10-14T09:00:00+00:00", "verified_by": "Yord Tech",
               "fields": {"last_name": "…", "first_name": "…", "number": "…", …}},
 "address": [{"verified_at": "…", "verified_by": "Yord Tech", "fields": {…}}]}

Отдаются только документы со статусом verified. ФИО — из самого документа, не из профиля. Согласованный, но непроверенный документ в ответ не попадает, поэтому ответ может быть и {}. Документов нет в id_token и /userinfo. Запросы — только с сервера, 60 в минуту на продукт.

Тело — строго application/x-www-form-urlencoded; JSON и multipart читаются как пустое тело и дают 401 invalid_token. Ошибки — JSON {"error": "…", "error_description": "…"}:

401 invalid_client     нет Basic, неверный секрет, продукт отключён или публичный
401 invalid_token      токен не at+jwt, выдан другому продукту, истёк, человек заблокирован
403 consent_required   нет действующего согласия хотя бы на один документный scope токена (или в токене их нет) — отказ на весь запрос, частичной выдачи нет; согласия нет, оно отозвано или изменились адреса возврата (redirect_uri), владелец, юрлицо, ИНН, цель — отправьте человека на /authorize
429 rate_limited       больше 60 запросов в минуту
503 temporarily_unavailable  документы временно выключены или недоступны (в том числе не читается сохранённый документ) — повторите позже

После отзыва согласия продукт обязан удалить полученные данные (условие соглашения с Yord Tech).

11. Кнопка

Войти через Yord ID
<a class="yord-id-btn" href="/auth/yord/start">
  <svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="7" fill="#fff"/>
    <path d="M7 6.5h2.6L12 10.6l2.4-4.1H17l-3.7 6v5h-2.6v-5L7 6.5Z" fill="#05607A"/></svg>
  <span>Войти через Yord ID</span></a>
<style>
.yord-id-btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; box-sizing: border-box;
  height: 48px; min-width: 240px; padding: 0 20px; border-radius: 12px; background: #05607A; color: #fff;
  font: 600 15px/1 -apple-system, "Segoe UI", Roboto, sans-serif; text-decoration: none; }
.yord-id-btn:hover { background: #034458; }
.yord-id-btn:focus-visible { outline: 3px solid #2BA3C4; outline-offset: 2px; }
</style>

Высота — не меньше 40 px, текст — «Войти через Yord ID», цвет и значок не менять. Ссылка ведёт на маршрут продукта, который начинает вход (шаг 3).

12. Yord Pay: оплата картой из Yord ID

Подключается по договору: Yord Tech включает продукту оплату и автоплатежи. Карту человек вводит только на странице ЮKassa; продукт видит только API Yord Pay на https://pay.yord.space.

POST   /api/v1/intents                  — намерение: token (access-токен человека со scope pay), amount, description,
                                          order_ref, items, return_url, [mandate] → 201 intent_id, confirm_url, expires_at
GET    /api/v1/payments/{id}            — статус: id, status (pending | succeeded | canceled), amount, refunded_amount, ...
POST   /api/v1/payments/{id}/close      — закрывающий чек по предоплате: items [{index, quantity}] → receipt_id, status
POST   /api/v1/mandates/{id}/notices    — предупреждение: amount, charge_at → notice_id, delivered
POST   /api/v1/charges                  — автоплатёж: mandate_id, notice_id, amount, description, items → 202 payment_id
DELETE /api/v1/mandates/{id}            — отменить подписку → id, revoked, inflight
POST   /api/v1/refunds                  — возврат: payment_id, amount, [items], reason → refund_id, status
GET    /api/v1/refunds/{id}             — статус возврата

Коды ошибок (поле error) и HTTP-статусы:

401 invalid_client          нет Basic или неверный секрет, продукт отключён или публичный
401 invalid_token           access_token человека недействителен или человек заблокирован
403 insufficient_scope      оплата или автоплатежи продукту не подключены, в токене нет scope pay / pay_recurring, Yord Pay пока открыт только сотрудникам Yord Tech
400 invalid_request         неверное тело, сумма, позиции, return_url, charge_at, нет Idempotency-Key
400 mandate_too_large       предел списания больше утверждённого тарифа продукта
404 not_found               платёж, согласие или возврат не найден (чужой объект тоже 404)
409 mandate_inactive        аккаунт заблокирован, получатель изменился или результат прошлого списания неизвестен
409 method_inactive         карта согласия больше не действует
409 notice_required         нет действующего предупреждения для этого списания
409 notice_undeliverable    предупреждение не доставлено: у человека нет российского канала
409 refund_blocked          возврат запрещён: спор, возврат с неизвестным результатом и т. п.
409 idempotency_conflict    этот Idempotency-Key уже использован с другим запросом
409 idempotency_in_progress запрос с этим ключом ещё выполняется
422 limit_exceeded          постоянный предел превышен (сумма выше предела продукта или согласия)
422 launch_limit_exceeded   первую неделю работы Yord Pay платёж не больше 10 000 ₽ (намерение, предупреждение, списание)
429 limit_exceeded          временный лимит (запросов, оплат за час или сутки, списаний за период)

Повторы и идемпотентность. Сохраняются только ответы 2xx и детерминированные 4xx: повтор с тем же ключом и тем же телом вернёт тот же ответ. Ответ 5xx и 429 не сохраняются — их можно повторить с тем же Idempotency-Key. Ответ 2xx или детерминированный 4xx (в том числе 409 notice_required, mandate_inactive) запоминается за ключом на 7 дней и повторяется как есть: исправив причину 4xx, отправляйте запрос с новым Idempotency-Key. idempotency_conflict — тот же ключ с другим запросом (не сохраняется); idempotency_in_progress — первый запрос с этим ключом ещё выполняется: повторите позже с тем же ключом.

429 и 422. 429 — временный лимит: повторите позже с нарастающей паузой. 422 — превышен постоянный предел (например, сумма выше предела продукта): повтор не поможет, измените запрос. launch_limit_exceeded — временный предел первой недели боевого режима: снимется сам через неделю после запуска; этот ответ не сохраняется, повтор с тем же ключом после снятия предела пройдёт.

expires_at ограничивает только начало оплаты: итог — только succeeded или canceled. Если заказ уже отменён, а платёж прошёл — сделайте возврат. Возвращению человека на return_url не верьте: статус узнавайте запросом GET /api/v1/payments/{id}.

Автоплатёж. Списание по согласию — только после предупреждения: charge_at не раньше чем через 24 часа, списывать можно не раньше charge_at и не позже чем через двое суток после него, на сумму не больше указанной в предупреждении, и не раньше чем через 24 часа после доставки предупреждения. delivered: true значит, что письмо или сообщение в MAX поставлено в очередь на отправку, а не что человек его прочитал; 24 часа считаются от фактической доставки. Если почта или MAX недоступны дольше 15 минут, сообщение снимается с очереди недоставленным: списание по такому предупреждению получит 409 notice_required — отправьте новое предупреждение (новый Idempotency-Key). Без российского канала у человека (почта на российском домене или MAX) предупреждение не доставляется — notice_undeliverable. Закрывающий чек по предоплате пробивается в день оказания услуги.

Уведомления продукту (если задан адрес): POST {"event": "payment.succeeded", "id": "…"} — также payment.canceled, refund.succeeded, refund.canceled, refund.review, payment.disputed, mandate.revoked, payment.close_overdue. Данных в теле нет: перезапросите объект через API.

13. Удаление аккаунта

Когда человек удаляет аккаунт Yord ID, продукты, куда он входил, получают об этом событие. Адрес events_uri (https, без данных в пути) продукт сообщает при подключении; без него события не отправляются.

POST <events_uri>
Content-Type: application/secevent+jwt

<JWT: iss=https://id.yord.space, aud=<ваш client_id>, iat, jti, sub=<идентификатор человека>,
      events={"https://id.yord.space/events/account-deleted": {}}>

Продукт проверяет: подпись по ключам https://id.yord.space/jwks, iss, aud равен своему client_id, заголовок typ = secevent+jwt, iat не старше 7 суток, в events есть событие удаления аккаунта. Затем удаляет все данные по sub и отвечает 202; при неверном токене — 400 без подробностей.

Любой ответ не из 2xx считается неудачей: Yord ID повторяет доставку с паузой 1, 2, 4… минут (до часа) в течение 7 суток. Поэтому обработка должна быть идемпотентной: событие по уже удалённому sub тоже получает 202.