#Ссылка для входа врача
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:
- Ищет пользователя по email, приведённому к нижнему регистру.
- Использует существующий аккаунт, если аккаунт относится к вашей клинике.
- Присоединяет к вашей клинике врача, который зарегистрировался в smartica.ai сам и не состоит ни в одной клинике.
- Создаёт нового врача по
user_emailиuser_full_name, если аккаунта нет. - Восстанавливает ранее удалённый аккаунт по тем же данным.
- Проверяет, что интеграция включена для этой организации.
Пункты 3–5 работают, когда для клиники включено автосоздание врачей. По умолчанию автосоздание включено. Если автосоздание выключено, smartica.ai не создаёт нового врача (404). Врача, который регистрировался сам, smartica.ai не присоединяет (403 email_in_other_organization).
Новый врач сразу попадает в приложение. Отдельная регистрация и подтверждение email не требуются, потому что вход происходит по доверенному каналу от вашей МИС.
Не создавайте аккаунты заранее. Достаточно, чтобы врач нажал кнопку.
Если email принадлежит врачу другой клиники в smartica.ai, перенос запрещён. Запрос возвращает 403 email_in_other_organization. Такой случай решается через поддержку.
errors.user_full_nameemail_in_other_organizationemail_in_other_organization403 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 читайте в статье Глоссарий.
Что делать с ответом:
- Сразу перенаправьте браузер на
launch_urlчерез302. Либо откройтеlaunch_urlв новой вкладке. - Передайте адрес без изменений.
- Не извлекайте параметр
t. Не сохраняйте параметрt. - Больше ничего не делайте. При открытии 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.