#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:
- ищет пользователя по email, приведённому к нижнему регистру;
- использует существующий аккаунт, если он относится к вашей клинике;
- создаёт нового врача по
user_emailиuser_full_name, если аккаунта нет; - восстанавливает ранее удалённый аккаунт по тем же данным;
- проверяет, что интеграция включена для этой организации.
Новый врач сразу попадает в приложение: отдельная регистрация и подтверждение 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 в бланке.
Что делать с ответом:
- Сразу перенаправьте браузер на
launch_urlчерез302или откройте его в новой вкладке. - Передайте адрес без единого изменения.
- Не извлекайте и не сохраняйте параметр
t. - Ничего больше не делайте: при открытии 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');
Не используйте этот вариант на общем тестовом стенде и в рабочем окружении: он обходит именно ту часть, которую нужно протестировать.