#Ссылка для входа врача

SSO открывает smartica.ai из МИС без повторного ввода логина и пароля. Ваш бэкенд запрашивает одноразовую ссылку и открывает её врачу.

Запрос выполняет только доверенный бэкенд МИС. Не вызывайте этот endpoint из браузера.

Статья описывает сценарий Launch. Общее описание потока читайте в статье Запуск из МИС, протокол присылаем мы (Launch).

#Адрес endpoint

Канонический вид:

POST https://app.smartica.ai/api/v1/integrations/{platform}/sso/token

Безверсионный путь POST /api/integrations/{platform}/sso/token тоже работает. Это постоянный алиас для МИС, подключённых раньше. Для новой интеграции используйте вариант с /v1/.

Используйте точный адрес, который вам выдали при подключении. {platform} — идентификатор вашей клиники. Подстановка произвольной строки вернёт 404.

#Запрос

POST /api/v1/integrations/{platform}/sso/token
Authorization: Bearer <API-ключ клиники>
Content-Type: application/json
Accept: application/json

{
  "user_email": "doctor@clinic.ru",
  "user_full_name": "Иванов Иван Иванович",
  "encounter_id": "12345"
}
Поле Обязательное Ограничения
user_email да Валидный email до 255 символов. Регистр не учитывается
user_full_name при первом входе Непустая строка до 255 символов
encounter_id да Строка из цифр или UUID в форме 8-4-4-4-12
template нет Название бланка. Только вместе с fields
fields нет До 80 полей. Обязательны id и label. type необязателен. Передавайте fields только вместе с непустым template. Типы описаны в статье Типы полей бланка

#Поле user_full_name

Передавайте user_full_name в каждом запросе.

Если врач уже входил в smartica.ai, ФИО не требуется. При первом входе smartica.ai создаёт аккаунт врача. Без ФИО запрос возвращает 422.

#Поле encounter_id

Передавайте encounter_id строкой, даже если в вашей базе значение хранится числом. Пример: "12345".

Допустимы только цифры или UUID. Значения "enc-123" и "TEST-1" возвращают 422. Полные правила читайте в статье Глоссарий.

#Безопасность полей запроса

Не берите platform, email и ФИО из данных браузера. Возьмите platform из конфигурации бэкенда. Возьмите врача из текущей сессии. После проверки прав возьмите приём из своей базы.

Иначе авторизованный пользователь подставляет чужой email и входит в smartica.ai под другим врачом.

#Inline-поля вместо GET

Если вы передаёте template и непустой fields в этом запросе, smartica.ai сохраняет их и использует при подготовке протокола. В этом случае smartica.ai не вызывает ваш GET.

{
  "user_email": "doctor@clinic.ru",
  "user_full_name": "Иванов Иван Иванович",
  "encounter_id": "12345",
  "template": "Кардиолог",
  "fields": [
    { "id": "complaints", "label": "Жалобы" },
    { "id": "weight", "label": "Вес", "type": "integer", "unit": "кг" },
    { "id": "diagnosis_primary", "label": "Диагноз (основной)" }
  ]
}

Передавайте template и fields в SSO-запросе в двух случаях:

  • Вы не поднимаете отдельный endpoint для чтения контекста.
  • Ваша МИС забирает результат сама. Входящих запросов от smartica.ai быть не должно.

Inline-поля работают, только если template и fields оба непустые. При доставке push одно поле без второго игнорируется. smartica.ai всё равно вызывает ваш GET.

При доставке pull передайте оба поля. В режиме pull smartica.ai не вызывает ваш GET. Без бланка в SSO врач не сможет начать запись. Пошаговый сценарий читайте в статье Запуск из МИС, протокол забираете вы (Launch Pull).

#Автоматическое создание врача

При SSO-запросе smartica.ai:

  1. Ищет пользователя по email, приведённому к нижнему регистру.
  2. Использует существующий аккаунт, если аккаунт относится к вашей клинике.
  3. Присоединяет к вашей клинике врача, который зарегистрировался в smartica.ai сам и не состоит ни в одной клинике.
  4. Создаёт нового врача по user_email и user_full_name, если аккаунта нет.
  5. Восстанавливает ранее удалённый аккаунт по тем же данным.
  6. Проверяет, что интеграция включена для этой организации.

Пункты 3–5 работают, когда для клиники включено автосоздание врачей. По умолчанию автосоздание включено. Если автосоздание выключено, smartica.ai не создаёт нового врача (404). Врача, который регистрировался сам, smartica.ai не присоединяет (403 email_in_other_organization).

Новый врач сразу попадает в приложение. Отдельная регистрация и подтверждение email не требуются, потому что вход происходит по доверенному каналу от вашей МИС.

Не создавайте аккаунты заранее. Достаточно, чтобы врач нажал кнопку.

Если email принадлежит врачу другой клиники в smartica.ai, перенос запрещён. Запрос возвращает 403 email_in_other_organization. Такой случай решается через поддержку.

Кому принадлежит email в smartica.ai?user_email из SSO-запроса
Аккаунта нет или он удалён
Автосоздание включено, ФИО переданоСоздаём или восстанавливаем аккаунт, врач входит
ФИО не передано422errors.user_full_name
Автосоздание выключено404
Врачу, который регистрировался сам
Автосоздание включеноПрисоединяем к вашей клинике, врач входит
Автосоздание выключено403email_in_other_organization
Врачу вашей клиники
Аккаунт уже естьВрач входит
Врачу другой клиники
Перенос только через поддержку403email_in_other_organization
Во всех ветках, где врач входит, мы сначала проверяем, что интеграция включена для клиники. Если нет — 403 integration_not_enabled.

#Ответ 200

{
  "launch_url": "https://app.smartica.ai/launch?platform=your-clinic&encounter_id=12345&t=<одноразовый токен>",
  "expires_in": 60
}

expires_in — время жизни ссылки в секундах. Не зашивайте 60 в код. Берите expires_in из ответа.

Если вы передали бланк и в нём нашлись поля с одинаковым id, к ответу добавится массив warnings. Вход врача работает как обычно. Часть полей заполнена не будет. Подробности о дубликатах id читайте в статье Глоссарий.

Что делать с ответом:

  1. Сразу перенаправьте браузер на launch_url через 302. Либо откройте launch_url в новой вкладке.
  2. Передайте адрес без изменений.
  3. Не извлекайте параметр t. Не сохраняйте параметр t.
  4. Больше ничего не делайте. При открытии smartica.ai убирает токен из адресной строки редиректом.

#Пример запроса

Примеры не привязаны к одному фреймворку. Email и ФИО берутся из текущей сессии МИС. encounterId берётся из карточки приёма после проверки прав врача. Секреты читаются из окружения.

$response = $http->post(
    getenv('SMARTICA_SSO_ENDPOINT'),
    [
        'headers' => [
            'Authorization' => 'Bearer '.getenv('SMARTICA_SSO_KEY'),
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'user_email' => $doctor->email,
            'user_full_name' => $doctor->fullName, // передаём всегда
            'encounter_id' => (string) $encounterId,
        ],
        'timeout' => 10,
    ]
);

if ($response->getStatusCode() !== 200) {
    $error = json_decode((string) $response->getBody(), true);

    // 401 и 403 не повторяем: причина не исчезнет сама.
    throw new RuntimeException($error['message'] ?? 'Smartica недоступна');
}

$launchUrl = json_decode((string) $response->getBody(), true)['launch_url'];

return redirect($launchUrl); // 302, без изменения адреса
const response = await fetch(process.env.SMARTICA_SSO_ENDPOINT, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SMARTICA_SSO_KEY}`,
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({
    user_email: doctor.email,
    user_full_name: doctor.fullName, // передаём всегда
    encounter_id: String(encounterId),
  }),
});

if (!response.ok) {
  const error = await response.json().catch(() => ({}));

  // 401 и 403 не повторяем: причина не исчезнет сама.
  throw new Error(error.message || 'Smartica недоступна');
}

const { launch_url: launchUrl } = await response.json();

res.redirect(302, launchUrl); // без изменения адреса
response = httpx.post(
    os.environ["SMARTICA_SSO_ENDPOINT"],
    headers={
        "Authorization": f"Bearer {os.environ['SMARTICA_SSO_KEY']}",
        "Accept": "application/json",
    },
    json={
        "user_email": doctor.email,
        "user_full_name": doctor.full_name,  # передаём всегда
        "encounter_id": str(encounter_id),
    },
    timeout=10,
)

if response.status_code != 200:
    # 401 и 403 не повторяем: причина не исчезнет сама.
    raise RuntimeError(response.json().get("message", "Smartica недоступна"))

launch_url = response.json()["launch_url"]

return redirect(launch_url)  # 302, без изменения адреса
var payload = new
{
    user_email = doctor.Email,
    user_full_name = doctor.FullName, // передаём всегда
    encounter_id = encounterId.ToString()
};

using var request = new HttpRequestMessage(
    HttpMethod.Post,
    Environment.GetEnvironmentVariable("SMARTICA_SSO_ENDPOINT"));

request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer",
    Environment.GetEnvironmentVariable("SMARTICA_SSO_KEY"));
request.Content = JsonContent.Create(payload);

using var response = await httpClient.SendAsync(request);

if (!response.IsSuccessStatusCode)
{
    // 401 и 403 не повторяем: причина не исчезнет сама.
    throw new InvalidOperationException($"Smartica вернула {(int)response.StatusCode}");
}

var result = await response.Content.ReadFromJsonAsync<SsoResponse>();

return Results.Redirect(result!.LaunchUrl); // без изменения адреса
Map<String, String> payload = Map.of(
        "user_email", doctor.getEmail(),
        "user_full_name", doctor.getFullName(), // передаём всегда
        "encounter_id", String.valueOf(encounterId)
);

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(System.getenv("SMARTICA_SSO_ENDPOINT")))
        .header("Authorization", "Bearer " + System.getenv("SMARTICA_SSO_KEY"))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .timeout(Duration.ofSeconds(10))
        .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
        .build();

HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());

if (response.statusCode() != 200) {
    // 401 и 403 не повторяем: причина не исчезнет сама.
    throw new IllegalStateException("Smartica вернула " + response.statusCode());
}

String launchUrl = objectMapper.readTree(response.body()).get("launch_url").asText();

return ResponseEntity.status(HttpStatus.FOUND)
        .location(URI.create(launchUrl))
        .build();
Функция ПолучитьСсылкуДляВхода(Врач, ИдентификаторПриёма) Экспорт

    Тело = Новый Структура;
    Тело.Вставить("user_email", Врач.Email);
    Тело.Вставить("user_full_name", Врач.ФИО);
    Тело.Вставить("encounter_id", Строка(ИдентификаторПриёма));

    Запись = Новый ЗаписьJSON;
    Запись.УстановитьСтроку();
    ЗаписатьJSON(Запись, Тело);

    Заголовки = Новый Соответствие;
    Заголовки.Вставить("Authorization", "Bearer " + Константы.SmarticaКлючКлиники.Получить());
    Заголовки.Вставить("Content-Type", "application/json");
    Заголовки.Вставить("Accept", "application/json");

    Соединение = Новый HTTPСоединение(
        "app.smartica.ai", 443, , , , 30, Новый ЗащищенноеСоединениеOpenSSL);

    Запрос = Новый HTTPЗапрос(
        "/api/v1/integrations/" + Константы.SmarticaPlatform.Получить() + "/sso/token",
        Заголовки);
    Запрос.УстановитьТелоИзСтроки(Запись.Закрыть(), КодировкаТекста.UTF8);

    Ответ = Соединение.ОтправитьДляОбработки(Запрос);

    Если Ответ.КодСостояния <> 200 Тогда
        ВызватьИсключение "Smartica вернула " + Ответ.КодСостояния;
    КонецЕсли;

    Чтение = Новый ЧтениеJSON;
    Чтение.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
    Результат = ПрочитатьJSON(Чтение, Истина);
    Чтение.Закрыть();

    // Ссылку сразу отдаём врачу: она одноразовая и живёт секунды.
    Возврат Результат["launch_url"];

КонецФункции

#Свойства ссылки

  • Токен одноразовый. Второе открытие не сработает.
  • Время жизни токена задаёт expires_in.
  • Токен привязан к врачу, клинике и приёму.
  • Изменение encounter_id в адресе делает вход недействительным.
  • Использованная или просроченная ссылка открывает обычный экран входа.

Если ссылка истекла до открытия, запросите новую. Старая ссылка больше не действует.

Получайте ссылку в момент клика. Не запрашивайте ссылку заранее. Не складывайте ссылки в очередь. Не кладите ссылки в письма. Не кэшируйте ссылки.

#Ошибки SSO

Все ошибки приходят в JSON. При 422 дополнительно приходит объект errors с разбивкой по полям.

Код error Причина Что делать в МИС
401 — Ключ отсутствует или неверен Проверить секрет. Не повторять автоматически
403 email_in_other_organization Email относится к другой организации Показать message, обратиться в поддержку
403 integration_not_enabled Интеграция выключена для организации Обратиться в поддержку
404 — Подключение не зарегистрировано либо аккаунт недоступен для SSO Проверить адрес, затем обратиться в поддержку
422 — Ошибка в email, ФИО или формате encounter_id Исправить поля по объекту errors
429 — Больше 30 запросов в минуту Учесть Retry-After, не запускать цикл повторов
503 — SSO ещё не настроен на нашей стороне Показать временную ошибку, обратиться в поддержку

Пример 422 при первом входе без ФИО:

{
  "message": "Неверные данные.",
  "errors": {
    "user_full_name": [
      "Для создания врача нужно ФИО (user_full_name)."
    ]
  }
}

Пример конфликта организаций:

{
  "error": "email_in_other_organization",
  "message": "Этот email уже привязан к другой организации в Smartica. Обратитесь в поддержку Smartica для переноса аккаунта."
}

#Как показывать ошибки врачу

Не заменяйте конкретный message общей фразой «не авторизован». Врач и администратор клиники не поймут, что делать.

Для 401, 403 и 422 покажите понятное сообщение. Пока причина не устранена, не повторяйте тот же запрос. Повторы упрутся в лимит неудачных попыток авторизации.

При сетевом сбое и 5xx предложите врачу нажать кнопку ещё раз.

Только при сетевой или временной серверной ошибке предложите обычный вход в smartica.ai по логину и паролю как временный обходной путь. Обычный вход не поможет при неверном ключе, конфликте организаций или невалидном encounter_id.

#Безопасность

  • Вызывайте endpoint только по HTTPS и только с бэкенда.
  • Храните ключ в менеджере секретов или в переменных окружения сервера.
  • Используйте разные ключи для тестового и рабочего окружений. Адреса контуров читайте в статье Тестовая площадка.
  • Не помещайте ключ, токен и launch_url во фронтенд, в аналитику, в систему сбора ошибок и в access-логи.
  • Не допускайте утечки launch_url в заголовке Referer на сторонние домены.
  • Ротируйте ключ через согласованный с нами процесс. При перевыпуске старый ключ действует ещё 72 часа.

Этим же ключом ваш бэкенд авторизуется, когда забирает готовые протоколы в режиме pull.

#Проверка через curl

Подставьте точный адрес, полученный при подключении:

SSO_ENDPOINT='https://app.smartica.ai/api/v1/integrations/your-clinic/sso/token'
SSO_KEY='замените-на-api-ключ-клиники'

curl --fail-with-body --silent --show-error \
  --request POST "${SSO_ENDPOINT}" \
  --header "Authorization: Bearer ${SSO_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{
    "user_email": "doctor@clinic.ru",
    "user_full_name": "Иванов Иван Иванович",
    "encounter_id": "12345",
    "template": "Кардиолог",
    "fields": [
      { "id": "complaints", "label": "Жалобы" },
      { "id": "diagnosis_primary", "label": "Диагноз (основной)" }
    ]
  }'

template и fields в этом примере необязательны. Если вы реализуете GET, уберите template и fields.

Полученную ссылку можно открыть в браузере вручную. Ссылка одноразовая и живёт около минуты.

#Отладка без SSO

Только для локальной проверки, когда врач уже вошёл в smartica.ai вручную:

const url = `https://app.smartica.ai/launch?platform=${platform}&encounter_id=${encounterId}`;
window.open(url, '_blank', 'noopener,noreferrer');

Не используйте этот вариант на общем тестовом стенде и в рабочем окружении. Этот вариант обходит SSO.

#Связанные статьи

← Все статьи: Интеграция с МИС Поиск по базе знаний