Песочница Yord ID — тестовые данные. Не вводите настоящие телефоны, почты и карты

Yord Tech

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

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

Песочница: тестовые данные, адреса ниже — песочницы. Код подтверждения — 000000.

1. Получить client_id

В песочнице клиента создаёте вы сами. Войдите: код подтверждения всегда 000000, писем и СМС нет; или нажмите «Создать тестового пользователя» на экране входа. Затем откройте Разработчику и введите код приглашения от Yord Tech (info@yord.tech). До 10 клиентов; секрет показывается один раз. Адреса возврата — https или http://localhost:<порт>/… и http://127.0.0.1:<порт>/… (только здесь). Клиенты и аккаунты, которыми не пользовались 30 дней, удаляются. Токены песочницы на боевом Yord ID недействительны; перенос клиента в боевой — по письму на info@yord.tech.

2. Адреса

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

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

GET https://id-sandbox.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-sandbox.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-sandbox.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-sandbox.yord.space, aud = ваш client_id, nonce).

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

В id_token и в GET https://id-sandbox.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-sandbox.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-sandbox.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. Документы

В песочнице документов нет: документные scope не выдаются. Описание — на боевом Yord ID.

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-sandbox.yord.space.

В песочнице оплата — заглушка без денег: confirm_url ведёт на страницу с кнопками «Оплатить», «Отклонить» и «Оплатить с сохранением карты» (последняя — если человеку можно сохранить карту). Статусы, уведомления продукту и чеки («чек пробит») идут тем же путём, что в бою. Включите scope pay у своего серверного клиента в «Разработчику».

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          постоянный предел превышен (сумма выше предела продукта или согласия)
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 — превышен постоянный предел (например, сумма выше предела продукта): повтор не поможет, измените запрос.

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

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

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