Документация для команды продукта

Подключите AppNotes и настройте доступ команды

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

Интеграция

Четыре шага до первой заметки

AppNotes работает внутри браузерного SPA. Начните с проекта в кабинете, затем выберите обычный SDK, React-обёртку или готовую CDN-сборку.

  1. 1

    Создайте проект

    В кабинете создайте публичный ключ. Его можно хранить в клиентском коде — это идентификатор, а не пароль.

  2. 2

    Разрешите origin

    Добавьте точные адреса приложения: например, https://app.example.com и http://localhost:5173 для разработки.

  3. 3

    Передайте контекст

    projectKey выбирает проект, roomId — аккаунт клиента, roomName — его отображаемое название, а url — текущую страницу внутри аккаунта.

  4. 4

    Пригласите команду

    Заметки появятся только после входа участника AppNotes, чей аккаунт добавлен в этот проект.

Универсальный SDK

Для любого SPA с npm, yarn или pnpm. Вы сами указываете контейнер кнопки и панели.

yarn add @appnotes/sdk

React-обёртка

Для React 19. Контейнеры создаются автоматически, а roomId, roomName и url обновляются через props.

yarn add @appnotes/react
appnotes.ts
import { 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);
internal-tools.tsx
"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}
    />
  );
}
Подключение без пакетного менеджера через CDN

Зафиксируйте версию в URL, разместите скрипт после контейнера кнопки и вызывайте глобальный метод window.AppNotes.init.

index.html
<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 SDK

Параметры инициализации

Обязательные поля отвечают за проект, рабочий контекст и место монтажа. Остальные позволяют настроить внешний вид, API и обработку ошибок.

Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.

ПараметрТипОбязательныйПо умолчаниюНазначение
projectKeystring ДаПубличный ключ проекта из раздела «Интеграция» в кабинете. Идентифицирует проект, но не даёт доступ к заметкам.
roomIdstring ДаСтабильный идентификатор аккаунта, организации или рабочего пространства в вашем продукте. Не используйте email и секретные данные.
roomNamestringНетroomIdЧеловекочитаемое название комнаты. Показывается в заголовке панели и фильтре комнат, но не заменяет стабильный roomId.
toggleDomElementHTMLElement | string ДаЭлемент или CSS-селектор, в который SDK добавит кнопку открытия. В React-обёртке создаётся автоматически.
rootDomElementHTMLElement | string ДаЭлемент или CSS-селектор для панели. Обычно document.body. В React-обёртке создаётся автоматически.
urlstring | URLНетwindow.location.hrefНачальная страница для контекстных заметок. AppNotes сохраняет origin и pathname; query-параметры, hash и завершающий слеш не учитываются.
theme"light" | "dark" | "system"Нет"system"Цветовая тема панели. Стили изолированы от приложения с помощью Shadow DOM.
drawerWidthnumber | stringНет"420px"Ширина панели на экранах от 641 px. Число трактуется как пиксели; можно передать CSS-значение.
endpoints{ apiUrl: string; realtimeUrl: string }НетAppNotes CloudЯвные HTTP- и WebSocket-адреса для самостоятельного размещения. В SaaS используются api.appnotes.tech и ws.appnotes.tech.
apiUrlstring (deprecated)НетСовместимость со старыми интеграциями. Для новых подключений используйте endpoints с двумя независимыми адресами.
onError(error: unknown) => voidНетОбработчик ошибок SDK для вашей системы мониторинга и пользовательских сценариев.
storageTokenStorage | nullНетпамять вкладкиПользовательское хранилище короткоживущего access token. Рекомендуем не передавать: refresh credential всё равно остаётся только в HttpOnly cookie.
storageKeystringНетappnotes:{projectKey}:tokenКлюч для access token, если вы явно подключили пользовательское storage.
fetchtypeof fetchНетglobalThis.fetchСвоя реализация fetch — например, для диагностики запросов или тестовой среды.

Дополнительные props React-компонента

Эти параметры дополняют основные настройки SDK и управляют React-контейнерами и доступом к экземпляру панели.

Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.

ПараметрТипОбязательныйПо умолчаниюНазначение
classNamestringНетКласс внешнего контейнера React-компонента.
toggleClassNamestringНетКласс контейнера, в который добавляется кнопка открытия панели.
rootClassNamestringНетКласс контейнера, в который добавляется сама панель.
onReady(instance: AppNotesInstance) => voidНетВызывается после запуска SDK и передаёт экземпляр для программного управления.
refRef<AppNotesInstance>НетReact-ссылка на экземпляр SDK с методами open, close, setRoom и другими.
containerPropsHTMLAttributes<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.

account-switcher.ts
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",
);
notes-panel.tsx
"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Удалить вложение
external-api.sh
# Создать заметку
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-ключ и действующий доступ к комнате.

external-api-attachments.sh
# Прикрепить файл к заметке
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 на любые операции записи. Ограничения комнат действуют для всех ролей. Отзыв ключа, смена роли или удаление пользователя из проекта применяются к следующему запросу без задержки.

CRM и автоматизация

Получайте события через вебхуки

Владелец или администратор проекта может добавить HTTPS-адрес, выбрать нужные события и проверить интеграцию тестовой доставкой в разделе «Вебхуки» личного кабинета.

Один адрес — нужные события

Создавайте отдельные подписки для CRM, аналитики и внутренних автоматизаций.

HMAC-подпись

Каждая доставка подписана отдельным секретом, который можно безопасно заменить.

Повторные попытки

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 вложения.

comment.updated.json
{
  "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, чтобы защититься от повторной отправки.

verify-appnotes-webhook.ts
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);
}
Успешным считается любой ответ 2xx, полученный за 10 секунд. AppNotes не следует за перенаправлениями. Повторные попытки выполняются примерно через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. В рабочем окружении адрес должен использовать HTTPS и не может указывать на локальную или приватную сеть.
Доступ команды

Чем отличаются роли участников

Роль определяет доступные действия, а настройки комнат — доступные данные. Выбирайте минимальный уровень доступа, которого достаточно человеку для работы.

Владелец

Полный контроль над проектом

owner
  • Читает и создаёт заметки, отвечает и меняет статусы
  • Редактирует, закрепляет и архивирует любые заметки
  • Управляет участниками, приглашениями, ключами и разрешёнными доменами
  • Настраивает вебхуки и просматривает журнал доставок
  • Передаёт владение, архивирует, восстанавливает и удаляет проект

Администратор

Управляет работой команды

admin
  • Читает и создаёт заметки, отвечает и меняет статусы
  • Редактирует, закрепляет и архивирует любые заметки
  • Управляет участниками, приглашениями, ключами и разрешёнными доменами
  • Настраивает вебхуки и просматривает журнал доставок
  • Настраивает для каждого участника доступ только к нужным комнатам
  • Не может распоряжаться ролью владельца или удалить проект

Участник

Работает с заметками

member
  • Читает заметки в доступных комнатах и создаёт новые
  • Отвечает в любых активных ветках и меняет их статус
  • Редактирует, закрепляет, архивирует и восстанавливает только свои заметки
  • Не управляет проектом и составом команды

Наблюдатель

Только чтение

viewer
  • Читает заметки и ответы в доступных ему комнатах
  • Видит статусы, архив и свои счётчики непрочитанного
  • Не создаёт, не редактирует и не меняет заметки
  • Не управляет проектом и составом команды

Доступ к отдельным комнатам

В разделе «Команда» владелец или администратор может настроить доступ для каждого участника: оставить все комнаты, разрешить только выбранные или закрыть отдельные. Ограничение применяется к просмотру и изменению заметок, а также к обновлениям в реальном времени.

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

Почему клиент не может прочитать заметки

Панель и API не доверяют публичному ключу или roomId. Для доступа нужен вход в AppNotes, подтверждённое участие в проекте и разрешённый origin приложения.

Доступ только по членству

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

Изолированная SDK-сессия

Встроенная панель получает сессию, ограниченную одним проектом. Её нельзя применить к другому проекту или к операциям управления аккаунтом.

Ограничение доступа к комнатам

Владелец или администратор может разрешить участнику только выбранные комнаты либо закрыть для него отдельные комнаты. Сервер проверяет это правило при каждом обращении.

Двухфакторная защита аккаунта

Каждый пользователь может включить в профиле подтверждение входа одноразовым кодом. Рекомендуем настроить его всем участникам проекта.

Refresh credential недоступен приложению

Он хранится в отдельной project-specific HttpOnly cookie, вращается при обновлении и не возвращается в JavaScript. Access token по умолчанию живёт только в памяти.

Проверка origin

SDK-вход, REST-запросы и WebSocket-соединение сверяются со списком разрешённых origin проекта. В production cookie передаётся только по HTTPS.

Короткие и отзывные сессии

Access token короткоживущий, refresh credential одноразово ротируется. Выход, смена или сброс пароля отзывают действующие сессии.

Защищённые учётные данные

Пароли хешируются с индивидуальной солью, email нужно подтвердить, а токены подтверждения, восстановления и приглашений хранятся в виде хешей и имеют срок действия.

Как работает приватность комнат

По умолчанию участнику доступны все комнаты проекта. Владелец или администратор может изменить это правило в разделе «Команда»: разрешить только выбранные комнаты или запретить отдельные. Ограничение проверяется на сервере, поэтому знания идентификатора комнаты недостаточно для доступа к её заметкам.

AppNotes не использует сквозное шифрование: сервис хранит заметки, чтобы синхронизировать их между участниками. Инфраструктурный доступ отделён и защищён дополнительной аутентификацией, но технически уполномоченный оператор сервиса может получить доступ к хранимым данным. Не размещайте в заметках пароли, платёжные данные и другие секреты.

Проверка перед безопасным запуском

  1. 1Укажите точные адреса рабочего и тестового приложения. Не разрешайте обращения со всех сайтов символом *.
  2. 2Показывайте панель AppNotes только сотрудникам, которым нужны внутренние заметки.
  3. 3Для идентификатора комнаты используйте постоянный внутренний код без адресов электронной почты, ключей доступа и персональных данных.
  4. 4Без необходимости не подключайте постоянное хранилище браузера: безопаснее держать короткоживущий ключ доступа только в памяти.
  5. 5Назначайте роль «Наблюдатель» или «Участник», если человеку не нужны настройки проекта.
  6. 6Настройте каждому участнику доступ только к тем комнатам, которые нужны ему для работы.
  7. 7Попросите всех участников включить двухфакторный вход в настройках профиля.
  8. 8Удаляйте бывших сотрудников из проекта и отзывайте неиспользуемые ключи проекта.
  9. 9Рабочее приложение должно открываться по защищённому соединению HTTPS.
  10. 10Не храните в заметках секреты, которые не должны быть доступны команде проекта.

Готовы подключить AppNotes?

Создайте проект и ключ SDK

Перейти в кабинет