Универсальный SDK
Для любого SPA с npm, yarn или pnpm. Вы сами указываете контейнер кнопки и панели.
yarn add @appnotes/sdkПрактическое руководство для тех, кто встраивает панель в своё приложение и отвечает за доступ к внутренним заметкам.
AppNotes работает внутри браузерного SPA. Начните с проекта в кабинете, затем выберите обычный SDK, React-обёртку или готовую CDN-сборку.
В кабинете создайте публичный ключ. Его можно хранить в клиентском коде — это идентификатор, а не пароль.
Добавьте точные адреса приложения: например, https://app.example.com и http://localhost:5173 для разработки.
projectKey выбирает проект, roomId — аккаунт клиента, roomName — его отображаемое название, а url — текущую страницу внутри аккаунта.
Заметки появятся только после входа участника AppNotes, чей аккаунт добавлен в этот проект.
Для любого SPA с npm, yarn или pnpm. Вы сами указываете контейнер кнопки и панели.
yarn add @appnotes/sdkДля React 19. Контейнеры создаются автоматически, а roomId, roomName и url обновляются через props.
yarn add @appnotes/reactimport { initAppNotes } from "@appnotes/sdk";
const appNotes = initAppNotes({
projectKey: "appnotes_pk_...",
roomId: currentAccount.id,
roomName: currentAccount.name,
url: window.location.href,
toggleDomElement: "#appnotes-toggle",
rootDomElement: document.body,
theme: "system",
});
// Если пользователь переключил аккаунт внутри SPA:
appNotes.setRoom(nextAccount.id, nextAccount.name);"use client";
import { AppNotes } from "@appnotes/react";
export function InternalTools({ account, employee }) {
if (!employee.canUseAppNotes) return null;
return (
<AppNotes
projectKey="appnotes_pk_..."
roomId={account.id}
roomName={account.name}
theme="system"
drawerWidth={420}
/>
);
}Зафиксируйте версию в URL, разместите скрипт после контейнера кнопки и вызывайте глобальный метод window.AppNotes.init.
<div id="appnotes-toggle"></div>
<script src="https://js.appnotes.tech/appnotes.global.js"></script>
<script>
const appNotes = window.AppNotes.init({
projectKey: "appnotes_pk_...",
roomId: window.currentAccount.id,
roomName: window.currentAccount.name,
toggleDomElement: "#appnotes-toggle",
rootDomElement: document.body,
});
</script>Без учётной записи и членства клиент не прочитает заметки, но может увидеть кнопку и форму входа, если вы смонтировали SDK для всех. Подключайте компонент только для сотрудников или за внутренним feature flag — как в React-примере выше.
Обязательные поля отвечают за проект, рабочий контекст и место монтажа. Остальные позволяют настроить внешний вид, API и обработку ошибок.
Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.
| Параметр | Тип | Обязательный | По умолчанию | Назначение |
|---|---|---|---|---|
projectKey | string | Да | — | Публичный ключ проекта из раздела «Интеграция» в кабинете. Идентифицирует проект, но не даёт доступ к заметкам. |
roomId | string | Да | — | Стабильный идентификатор аккаунта, организации или рабочего пространства в вашем продукте. Не используйте email и секретные данные. |
roomName | string | Нет | roomId | Человекочитаемое название комнаты. Показывается в заголовке панели и фильтре комнат, но не заменяет стабильный roomId. |
toggleDomElement | HTMLElement | string | Да | — | Элемент или CSS-селектор, в который SDK добавит кнопку открытия. В React-обёртке создаётся автоматически. |
rootDomElement | HTMLElement | string | Да | — | Элемент или CSS-селектор для панели. Обычно document.body. В React-обёртке создаётся автоматически. |
url | string | URL | Нет | window.location.href | Начальная страница для контекстных заметок. AppNotes сохраняет origin и pathname; query-параметры, hash и завершающий слеш не учитываются. |
theme | "light" | "dark" | "system" | Нет | "system" | Цветовая тема панели. Стили изолированы от приложения с помощью Shadow DOM. |
drawerWidth | number | string | Нет | "420px" | Ширина панели на экранах от 641 px. Число трактуется как пиксели; можно передать CSS-значение. |
endpoints | { apiUrl: string; realtimeUrl: string } | Нет | AppNotes Cloud | Явные HTTP- и WebSocket-адреса для самостоятельного размещения. В SaaS используются api.appnotes.tech и ws.appnotes.tech. |
apiUrl | string (deprecated) | Нет | — | Совместимость со старыми интеграциями. Для новых подключений используйте endpoints с двумя независимыми адресами. |
onError | (error: unknown) => void | Нет | — | Обработчик ошибок SDK для вашей системы мониторинга и пользовательских сценариев. |
storage | TokenStorage | null | Нет | память вкладки | Пользовательское хранилище короткоживущего access token. Рекомендуем не передавать: refresh credential всё равно остаётся только в HttpOnly cookie. |
storageKey | string | Нет | appnotes:{projectKey}:token | Ключ для access token, если вы явно подключили пользовательское storage. |
fetch | typeof fetch | Нет | globalThis.fetch | Своя реализация fetch — например, для диагностики запросов или тестовой среды. |
Эти параметры дополняют основные настройки SDK и управляют React-контейнерами и доступом к экземпляру панели.
Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.
| Параметр | Тип | Обязательный | По умолчанию | Назначение |
|---|---|---|---|---|
className | string | Нет | — | Класс внешнего контейнера React-компонента. |
toggleClassName | string | Нет | — | Класс контейнера, в который добавляется кнопка открытия панели. |
rootClassName | string | Нет | — | Класс контейнера, в который добавляется сама панель. |
onReady | (instance: AppNotesInstance) => void | Нет | — | Вызывается после запуска SDK и передаёт экземпляр для программного управления. |
ref | Ref<AppNotesInstance> | Нет | — | React-ссылка на экземпляр SDK с методами open, close, setRoom и другими. |
containerProps | HTMLAttributes<HTMLDivElement> | Нет | — | Дополнительные HTML-атрибуты внешнего контейнера компонента. |
initAppNotes возвращает экземпляр SDK. React-компонент предоставляет те же методы через ref и onReady.
Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.
| Метод | Когда использовать | Результат |
|---|---|---|
open() | Своя кнопка или горячая клавиша | Открывает панель. |
close() | Завершение сценария или смена экрана | Закрывает панель. |
toggle() | Одна кнопка для открытия и закрытия | Переключает текущее состояние панели. |
setRoom(roomId, roomName?) | Пользователь выбрал другой аккаунт | Меняет рабочее пространство и его отображаемое название без пересоздания SDK. |
setPage(url) | Нестандартный переход внутри приложения | Меняет страницу, к которой привязаны контекстные заметки. |
refresh() | Нужно принудительно сверить данные | Повторно загружает актуальные заметки и счётчики. |
destroy() | Приложение удаляет интеграцию со страницы | Удаляет AppNotes, обработчики и Shadow DOM. |
Переходы через History API отслеживаются автоматически. Вызывайте setPage(), если ваш роутер меняет экран без стандартных pushState, replaceState или popstate.
function showAccount(account: { id: string; name: string }, pageUrl: string) {
appNotes.setRoom(account.id, account.name);
appNotes.setPage(pageUrl);
appNotes.open();
}
// Например, после выбора другого клиента:
showAccount(
{ id: "account-43", name: "Codeception" },
"https://app.example.com/orders",
);"use client";
import { AppNotes, type AppNotesInstance } from "@appnotes/react";
import { useRef } from "react";
export function NotesPanel({ account }: { account: { id: string; name: string } }) {
const appNotesRef = useRef<AppNotesInstance>(null);
return (
<>
<button onClick={() => appNotesRef.current?.open()}>
Открыть заметки
</button>
<AppNotes
ref={appNotesRef}
projectKey="appnotes_pk_..."
roomId={account.id}
roomName={account.name}
/>
</>
);
}Создайте персональный ключ в разделе «Профиль» и передавайте его в Bearer-заголовке серверных запросов. Полное значение показывается только один раз.
Автором заметки или комментария становится владелец ключа, а не технический аккаунт проекта.
API при каждом запросе проверяет роль, членство в проекте и доступ к указанной комнате.
Храните ключ в менеджере секретов внешнего сервиса. Не передавайте его в браузер и мобильное приложение.
| Метод | Путь | Действие |
|---|---|---|
| POST | /api/v1/notes | Создать заметку |
| PATCH | /api/v1/notes/:noteId | Изменить заголовок или текст |
| POST | /api/v1/notes/:noteId/comments | Добавить комментарий |
| PATCH | /api/v1/notes/:noteId/comments/:commentId | Изменить комментарий |
| POST | /api/v1/notes/:noteId/attachments | Прикрепить файл к заметке |
| POST | /api/v1/notes/:noteId/comments/:commentId/attachments | Прикрепить файл к комментарию |
| GET | /api/v1/attachments/:attachmentId/content | Скачать приватное вложение |
| DELETE | /api/v1/attachments/:attachmentId | Удалить вложение |
# Создать заметку
curl --request POST https://api.appnotes.tech/api/v1/notes \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"projectKey": "appnotes_pk_...",
"roomId": "customer_128",
"roomName": "Acme",
"pageId": "https://app.example.com/customers/128",
"title": "Нужно сверить договор",
"body": "Клиент прислал новую редакцию"
}'
# Изменить заметку
curl --request PATCH https://api.appnotes.tech/api/v1/notes/NOTE_ID \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--header "Content-Type: application/json" \
--data '{"body":"Договор проверен"}'
# Добавить комментарий
curl --request POST https://api.appnotes.tech/api/v1/notes/NOTE_ID/comments \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--header "Content-Type: application/json" \
--data '{"body":"Передано юристам"}'
# Изменить комментарий
curl --request PATCH \
https://api.appnotes.tech/api/v1/notes/NOTE_ID/comments/COMMENT_ID \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--header "Content-Type: application/json" \
--data '{"body":"Согласовано с юристами"}'Передавайте один файл в multipart-полеfile. Ответ загрузки содержит метаданные attachment и актуальную заметку thread. Содержимое остаётся приватным: для скачивания нужен тот же персональный API-ключ и действующий доступ к комнате.
# Прикрепить файл к заметке
curl --request POST \
https://api.appnotes.tech/api/v1/notes/NOTE_ID/attachments \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--form "file=@./contract.pdf"
# Прикрепить файл к комментарию
curl --request POST \
https://api.appnotes.tech/api/v1/notes/NOTE_ID/comments/COMMENT_ID/attachments \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--form "file=@./approval.png"
# Скачать приватное вложение
curl --fail \
https://api.appnotes.tech/api/v1/attachments/ATTACHMENT_ID/content \
--header "Authorization: Bearer $APPNOTES_API_KEY" \
--output contract.pdf
# Удалить вложение
curl --request DELETE \
https://api.appnotes.tech/api/v1/attachments/ATTACHMENT_ID \
--header "Authorization: Bearer $APPNOTES_API_KEY"К заметке или комментарию можно прикрепить до 10 файлов размером до 20 МиБ каждый. Поддерживаются JPEG, PNG, GIF, WebP, PDF, TXT, CSV, Markdown и LOG. AppNotes проверяет содержимое файла, а не только имя и Content-Type.
Owner и admin могут редактировать любые заметки и комментарии. Member создаёт записи, но редактирует только свои. Viewer получает 403 на любые операции записи. Ограничения комнат действуют для всех ролей. Отзыв ключа, смена роли или удаление пользователя из проекта применяются к следующему запросу без задержки.
Владелец или администратор проекта может добавить HTTPS-адрес, выбрать нужные события и проверить интеграцию тестовой доставкой в разделе «Вебхуки» личного кабинета.
Создавайте отдельные подписки для CRM, аналитики и внутренних автоматизаций.
Каждая доставка подписана отдельным секретом, который можно безопасно заменить.
AppNotes делает до шести попыток доставки; их статус виден в журнале.
Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.
| Событие | Когда отправляется |
|---|---|
note.created | Создана новая заметка |
note.updated | Изменены текст, заголовок, статус, закрепление или вложения |
note.archived | Заметка перенесена в архив |
note.restored | Заметка восстановлена из архива |
note.deleted | Заметка окончательно удалена |
comment.created | К заметке добавлен комментарий |
comment.updated | Изменены текст или вложения комментария |
comment.archived | Родительская заметка с комментарием архивирована |
comment.restored | Родительская заметка с комментарием восстановлена |
comment.deleted | Комментарий удалён вместе с родительской заметкой |
Комментарий не архивируется отдельно от заметки. При архивировании, восстановлении или окончательном удалении заметки AppNotes отправляет соответствующее событие по каждому её комментарию. Получатель может обрабатывать события идемпотентно по полюid.
Тело отправляется как JSON. Для событий комментария поле data содержит и комментарий, и актуальный снимок родительской заметки. Массивы attachments содержат метаданные файлов заметки и комментария; само содержимое не встраивается в вебхук и скачивается авторизованным запросом по ID вложения.
{
"id": "evt_019fd1...",
"apiVersion": "2026-08-01",
"type": "comment.updated",
"createdAt": "2026-08-05T12:10:30.000Z",
"projectId": "project_42",
"actor": {
"id": "user_17",
"email": "agent@example.com",
"name": "Анна"
},
"data": {
"note": {
"id": "note_91",
"roomId": "customer_128",
"title": "Нужно сверить договор",
"attachments": [{
"id": "attachment_18",
"threadId": "note_91",
"commentId": null,
"fileName": "contract.pdf",
"mediaType": "application/pdf",
"sizeBytes": 483102,
"kind": "file",
"uploadedBy": {
"id": "user_17",
"email": "agent@example.com",
"name": "Анна"
},
"createdAt": "2026-08-05T12:08:00.000Z"
}]
},
"comment": {
"id": "comment_33",
"body": "Согласовано",
"attachments": [{
"id": "attachment_19",
"threadId": "note_91",
"commentId": "comment_33",
"fileName": "approval.png",
"mediaType": "image/png",
"sizeBytes": 204800,
"kind": "image",
"uploadedBy": {
"id": "user_17",
"email": "agent@example.com",
"name": "Анна"
},
"createdAt": "2026-08-05T12:10:30.000Z"
}]
}
}
}Вычислите HMAC-SHA256 от строки timestamp.rawBody и сравните результат с X-AppNotes-Signature. Используйте исходные байты тела до JSON.parse и отклоняйте слишком старый X-AppNotes-Timestamp, чтобы защититься от повторной отправки.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyAppNotesWebhook(rawBody: string, headers: Headers, secret: string) {
const timestamp = headers.get("x-appnotes-timestamp") ?? "";
const received = headers.get("x-appnotes-signature") ?? "";
const timestampMs = Number(timestamp) * 1_000;
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > 5 * 60_000) {
return false;
}
const expected = "v1=" + createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const receivedBytes = Buffer.from(received);
const expectedBytes = Buffer.from(expected);
return receivedBytes.length === expectedBytes.length &&
timingSafeEqual(receivedBytes, expectedBytes);
}Роль определяет доступные действия, а настройки комнат — доступные данные. Выбирайте минимальный уровень доступа, которого достаточно человеку для работы.
Полный контроль над проектом
Управляет работой команды
Работает с заметками
Только чтение
В разделе «Команда» владелец или администратор может настроить доступ для каждого участника: оставить все комнаты, разрешить только выбранные или закрыть отдельные. Ограничение применяется к просмотру и изменению заметок, а также к обновлениям в реальном времени.
Панель и API не доверяют публичному ключу или roomId. Для доступа нужен вход в AppNotes, подтверждённое участие в проекте и разрешённый origin приложения.
Перед чтением или изменением данных API проверяет сессию и участие пользователя в нужном проекте. Публичного ключа недостаточно.
Встроенная панель получает сессию, ограниченную одним проектом. Её нельзя применить к другому проекту или к операциям управления аккаунтом.
Владелец или администратор может разрешить участнику только выбранные комнаты либо закрыть для него отдельные комнаты. Сервер проверяет это правило при каждом обращении.
Каждый пользователь может включить в профиле подтверждение входа одноразовым кодом. Рекомендуем настроить его всем участникам проекта.
Он хранится в отдельной project-specific HttpOnly cookie, вращается при обновлении и не возвращается в JavaScript. Access token по умолчанию живёт только в памяти.
SDK-вход, REST-запросы и WebSocket-соединение сверяются со списком разрешённых origin проекта. В production cookie передаётся только по HTTPS.
Access token короткоживущий, refresh credential одноразово ротируется. Выход, смена или сброс пароля отзывают действующие сессии.
Пароли хешируются с индивидуальной солью, email нужно подтвердить, а токены подтверждения, восстановления и приглашений хранятся в виде хешей и имеют срок действия.
По умолчанию участнику доступны все комнаты проекта. Владелец или администратор может изменить это правило в разделе «Команда»: разрешить только выбранные комнаты или запретить отдельные. Ограничение проверяется на сервере, поэтому знания идентификатора комнаты недостаточно для доступа к её заметкам.
AppNotes не использует сквозное шифрование: сервис хранит заметки, чтобы синхронизировать их между участниками. Инфраструктурный доступ отделён и защищён дополнительной аутентификацией, но технически уполномоченный оператор сервиса может получить доступ к хранимым данным. Не размещайте в заметках пароли, платёжные данные и другие секреты.
Готовы подключить AppNotes?