#API Smartica: ссылка для входа (SSO)

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

Запрос выполняет только доверенный бэкенд МИС. Из браузера этот 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 нет Массив { id, label } до 80 элементов. Работает только вместе с непустым template

#Про user_full_name

Формально поле условное, но на практике передавайте его всегда.

Если врач уже заходил в Smartica, ФИО не требуется. Но при самом первом входе мы заводим для него аккаунт, и без ФИО сделать это невозможно — вернётся 422. Передавая ФИО всегда, вы избавляете себя от необходимости отслеживать, кто входит впервые.

#Про encounter_id

Строкой, даже если в базе это число. "12345", а не 12345.

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

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

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

Иначе любой авторизованный пользователь сможет подставить чужой email и войти в Smartica под другим врачом.

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

Если передать template и непустой fields прямо в этом запросе, Smartica сохранит их и использует при подготовке протокола. Тогда обращаться к вашему GET мы не будем.

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

Это удобно в двух случаях:

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

Работает только когда переданы оба поля: template непустой и fields непустой. Если передать что-то одно, значение игнорируется и мы всё равно пойдём в ваш GET.

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

При SSO-запросе Smartica:

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

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

Заводить аккаунты заранее не нужно — достаточно того, что врач нажал кнопку.

Исключение: если email уже относится к другой организации в Smartica, автоматический перенос запрещён и вернётся 403 email_in_other_organization. Обычно это врач, который раньше регистрировался у нас самостоятельно. Такой случай решается через поддержку.

#Ответ 200

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

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

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

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

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

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

Код без привязки к фреймворку. 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 по логину и паролю можно предложить как временный обходной путь только при сетевой или временной серверной ошибке. Он не поможет при неверном ключе, конфликте организаций или невалидном encounter_id.

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

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

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

#Проверка через 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.

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

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

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

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

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

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

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