Интеграция с Google Calendar с помощью виджета TapKit и App Script


В данной статье рассмотрим, как можно создать, обновить, удалить или найти актуальные данные события в Google Calendar с помощью функциональных кнопок в сделке и использования механизма скриптов App Script

Задача

Настроить функциональные кнопки. Каждая кнопка будет осуществлять определенное действие:

  • Создавать событие в Google Calendar
  • Обновлять событие в Google Calendar
  • Искать событие в Google Calendar
  • Удалять событие в Google Calendar

Подключение AmoCRM

Создание полей

Перед подключением AmoCRM в MoreKIT — необходимо создать поля, с которыми будет осуществляться взаимодействие между MoreKIT и событиями Google Calendar

Будут использоваться следующие поля:

  • ID события — текст
  • Дата начала встречи — дата и время
  • Дата конца встречи — дата и время
  • Название — текст
  • Описание — текст
  • Локация — текст
  • Гость может приглашать других — флаг
  • Гость может редактировать событие — флаг
  • Гость может видеть других гостей — флаг

Подключение сервиса

В MoreKit в разделе Сервисы — ищем в списке сервисов amoCRM, нажимаем на кнопку Добавить, произойдет перенаправление на страницу переадресации amoCRM, где необходимо выбрать нужную CRM, которую надо подключить в MoreKit

Подключение amoCRM
Выбор аккаунта в аamoCRM

Подключение виджета TapKit в amoCRM

В amoCRm переходим в amoМаркет, вводим в поиске виджетов Tap Kit, устанавливаем виджет

Настройка App Script

Следующим шагом — создадим проект в App Script и настроим скрипты для управления событиями:

  • Зайдите на страницу https://script.google.com/home
  • Нажмите на кнопку Создать Проект

В App Script у вас будет окно с текущими скриптами, а также возможность подключать дополнительные сервисы

Нам необходимо установить Google Calendar API

Google App Script предоставляет довольно мощный функционал для работы со скриптами, на случай если понадобится изменить логику отправки и обработки отправляемых данных скриптом. В случае такой необходимости — советуем обратиться к официальной документации Google appScript, где описаны возможные методы и принцип работы App Script

Структура проекта

В данном кейсе — будет использоваться 7 скриптов

  • Config.gs
  • CreateEvent.gs
  • FindEvent.gs
  • UpdateEvent.gs
  • DeleteEvent.gs
  • Code.gs

И вспомогательный одноразовый скрипт Выгрузка календарей.gs

Устанавливаем скрипты

function listCalendars() {

  const calendars = CalendarApp.getAllCalendars();

  Logger.log("Найдено календарей: " + calendars.length);

  calendars.forEach(function(calendar, index) {

    Logger.log("Календарь #" + (index + 1));
    Logger.log("Название    : " + calendar.getName());
    Logger.log("ID          : " + calendar.getId());
    Logger.log("Описание    : " + calendar.getDescription());
    Logger.log("Часовой пояс: " + calendar.getTimeZone());
    Logger.log("Цвет        : " + calendar.getColor());
    Logger.log("Скрыт       : " + calendar.isHidden());

  });

  Logger.log("Календарь по умолчанию:");
  Logger.log(CalendarApp.getDefaultCalendar().getName());
  Logger.log("ID: " + CalendarApp.getDefaultCalendar().getId());

}
const DEFAULT_CALENDAR = "primary";
function json(obj) {
  return ContentService
    .createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}
function createEvent(data) {
  // ID календаря
  let calendarId = DEFAULT_CALENDAR;

  if (data.calendarId) {
    calendarId = data.calendarId;
  } else if (data.calendarName) {
    const calendars = CalendarApp.getCalendarsByName(data.calendarName);
    if (calendars.length === 0)
      throw new Error("Календарь не найден");
    calendarId = calendars[0].getId();
  }

  // Валидация
  if (!data.summary)
    throw new Error("summary не заполнено");
  if (!data.startDate)
    throw new Error("startDate не заполнено");
  if (!data.endDate)
    throw new Error("endDate не заполнено");

  // Формируем ресурс события
  const resource = {
    summary: data.summary,
    description: data.description || "",
    location: data.location || ""
  };

  // Дата/время
  const timeZone = data.timeZone || Session.getScriptTimeZone();

  if (data.allDay) {
    resource.start = { date: new Date(data.startDate).toISOString().split("T")[0] };
    resource.end   = { date: new Date(data.endDate).toISOString().split("T")[0] };
  } else {
    resource.start = {
      dateTime: new Date(data.startDate).toISOString(),
      timeZone: timeZone
    };
    resource.end = {
      dateTime: new Date(data.endDate).toISOString(),
      timeZone: timeZone
    };
  }

  // Гости
  if (Array.isArray(data.guests)) {
    resource.attendees = data.guests.map(function(email) {
      if (typeof email === "string")
        return { email: email };
      return email; // объект вида { email: "x", optional: true }
    });
  }

  // Видимость
  if (data.visibility) {
    resource.visibility = data.visibility; // "default" | "public" | "private" | "confidential"
  }

  // Настройки гостей
  if (typeof data.guestsCanInviteOthers === "boolean")
    resource.guestsCanInviteOthers = data.guestsCanInviteOthers;
  if (typeof data.guestsCanModify === "boolean")
    resource.guestsCanModify = data.guestsCanModify;
  if (typeof data.guestsCanSeeGuests === "boolean")
    resource.guestsCanSeeGuests = data.guestsCanSeeGuests;

  // Цвет (Calendar API использует colorId — строка от "1" до "11")
  if (data.color) {
    resource.colorId = String(data.color);
  }

  // Напоминания
  if (Array.isArray(data.reminders)) {
    resource.reminders = {
      useDefault: false,
      overrides: data.reminders.map(function(r) {
        return {
          method: r.method,  // "popup" или "email"
          minutes: r.minutes
        };
      })
    };
  } else {
    resource.reminders = { useDefault: true, overrides: [] };
  }

  const event = Calendar.Events.insert(resource, calendarId);

  // Ответ
  return json({
    success: true,
    eventId: event.id,
    iCalUID: event.iCalUID,
    calendarId: calendarId
  });
}
function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);

    switch ((data.action || "").toLowerCase()) {
      case "create":
        return createEvent(data);
      case "find":
        return findEvent(data);
      case "update":
        return updateEvent(data);
      case "delete":
        return deleteEvent(data);
      default:
        throw new Error("Неизвестное действие: " + data.action);
    }
  } catch (err) {
    return json({ success: false, error: String(err) });
  }
}
function findEvent(data) {
  
  // Календарь
  let calendarId = DEFAULT_CALENDAR;

  if (data.calendarId) {
    calendarId = data.calendarId;
  } else if (data.calendarName) {
    const calendars = CalendarApp.getCalendarsByName(data.calendarName);
    if (calendars.length === 0)
      throw new Error("Календарь не найден");
    calendarId = calendars[0].getId();
  }

  // Валидация
  if (!data.eventId)
    throw new Error("eventId не заполнено");


  // Получаем событие через Calendar API
  let event;
  try {
    event = Calendar.Events.get(calendarId, data.eventId);
  } catch (err) {
    const msg = String(err);
    if (msg.indexOf("404") >= 0 || msg.indexOf("Not Found") >= 0) {
      return json({ success: true, found: false });
    }
    throw err;
  }


  // Если событие удалено — not found
  if (event.status === "cancelled") {
    return json({ success: true, found: false });
  }

  // Формируем ответ
  return json({
    success: true,
    found: true,
    event: {
      eventId: event.id,
      summary: event.summary || "",
      description: event.description || "",
      location: event.location || "",
      startDate: event.start.dateTime || event.start.date,
      endDate: event.end.dateTime || event.end.date,
      allDay: !!event.start.date,
      lastUpdated: event.updated,
      status: event.status,
      htmlLink: event.htmlLink,
      guests: (event.attendees || []).map(function(g) {
        return {
          email: g.email,
          responseStatus: g.responseStatus,
          optional: !!g.optional
        };
      })
    }
  });
}
function updateEvent(data) {
  // Календарь
  let calendarId = DEFAULT_CALENDAR;
  if (data.calendarId) {
    calendarId = data.calendarId;
  } else if (data.calendarName) {
    const calendars = CalendarApp.getCalendarsByName(data.calendarName);
    if (calendars.length === 0)
      throw new Error("Календарь не найден");
    calendarId = calendars[0].getId();
  }

  // Валидация
  if (!data.eventId)
    throw new Error("eventId не заполнено");

  // Получаем событие через Calendar API
  let event;
  try {
    event = Calendar.Events.get(calendarId, data.eventId);
  } catch (err) {
    const msg = String(err);
    if (msg.indexOf("404") >= 0 || msg.indexOf("Not Found") >= 0) {
      return json({ success: false, error: "Событие не найдено" });
    }
    throw err;
  }

  // Обновляем поля
  const resource = {};

  if (data.summary)        resource.summary = data.summary;
  if (data.description)    resource.description = data.description;
  if (data.location)       resource.location = data.location;

  if (data.startDate || data.endDate) {
    resource.start = {};
    resource.end   = {};

    if (data.startDate) {
      if (data.allDay) {
        resource.start.date = new Date(data.startDate).toISOString().split("T")[0];
        resource.end.date   = new Date(data.endDate || data.startDate).toISOString().split("T")[0];
      } else {
        resource.start.dateTime = new Date(data.startDate).toISOString();
        resource.end.dateTime   = new Date(data.endDate || data.startDate).toISOString();
      }

      // Часовой пояс - либо из запроса, либо из календаря
      if (data.timeZone) {
        resource.start.timeZone = data.timeZone;
        resource.end.timeZone   = data.timeZone;
      }
    }
  }

  if (Array.isArray(data.guests)) {
    resource.attendees = data.guests.map(function(email) {
      return { email: email };
    });
  }

  if (data.visibility) {
    resource.visibility = data.visibility;
  }

  Calendar.Events.patch(resource, calendarId, data.eventId);

  // Возвращаем обновлённое событие
  const updated = Calendar.Events.get(calendarId, data.eventId);

  return json({
    success: true,
    event: {
      eventId: updated.id,
      summary: updated.summary || "",
      description: updated.description || "",
      location: updated.location || "",
      startDate: updated.start.dateTime || updated.start.date,
      endDate: updated.end.dateTime || updated.end.date,
      allDay: !!updated.start.date,
      lastUpdated: updated.updated,
      status: updated.status,
      htmlLink: updated.htmlLink
    }
  });
}
function deleteEvent(data) {
  // Календарь
  let calendarId = DEFAULT_CALENDAR;
  if (data.calendarId) {
    calendarId = data.calendarId;
  } else if (data.calendarName) {
    const calendars = CalendarApp.getCalendarsByName(data.calendarName);
    if (calendars.length === 0)
      throw new Error("Календарь не найден");
    calendarId = calendars[0].getId();
  }

  // Валидация
  if (!data.eventId)
    throw new Error("eventId не заполнено");

  // Удаление
  try {
    Calendar.Events.remove(calendarId, data.eventId);
  } catch (err) {
    const msg = String(err);
    if (msg.indexOf("404") >= 0 || msg.indexOf("Not Found") >= 0) {
      return json({ success: false, error: "Событие не найдено" });
    }
    throw err;
  }

  return json({ success: true });
}

Развертывание скрипта

Следующим шагом — выполним развертывание скрипта, чтобы он начал работать с календарем

Если скрипт попросит выдаст права на выполнение — подтверждаем

Оставьте данное окно, позже потребуется ссылка на Веб-приложение

Создание процессов MoreKIT

В MoreKIT нам потребуется создать 4 процесса:

  • Процесс создания события
  • Процесс поиска события
  • Процесс удаления события
  • Процесс обновления события

Для создания процесса нажмите на кнопку Создать процесс, тип процесса выбираем Другое, даем процессу название и сохраняем

Внутри каждого процесса есть принимающая webhook ссылка, она потребуется нам для настройки виджета Tap KIT

Следующим шагом — перейдем к настройке виджета Tap KIT

Настройка Tap KIT

В настройках виджета TapKIT — создадим 4 кнопки

Пример настройки кнопки:

  • Тип карточки: Сделки — сущность, в которой будет отображаться кнопка
  • Расположение: ID события — где будут располагаться кнопки, в данном примере они будут располагаться около кнопки ID события
  • Содержимое: Сделки — набор данных, которые будут отправляться в MoreKIT. В данном случае — сделки
  • Текст кнопки — текст кнопки, отображающий процесс кнопки, который она выполняет
  • Адрес отправки: здесь необходимо вставить ссылку из первого блока соответствующего процесса из MoreKIT
  • Цвет кнопки: цвет кнопки
  • Цвет текста: цвет текста
  • Воронки и статусы: где будет отображаться кнопка
  • Группы пользователей: для кого кнопка должна показываться, а для кого нет

В результате получим функциональные кнопки, которые будут управлять процессами

Процессы в MoreKIT

После настройки кнопок — настроим процессы в MoreKIT

Процесс создания события

Схема процесса по созданию события

Описание блоков схемы

#2 Найти сделки — находим полные данные о сделке

#3 Найти контакты — Ищем контакт из сделки, он будет нашим гостем в событии

#8 Конвертировать дату — преобразуем дату из формата timestamp, который отдает нам amoCRM для формата App Script
Формат для App Script, который используется: %Y-%m-%d %H:%M:%S +0700. Часовой пояс здесь +0700 нивелирует разницу между часовым поясом amoCRM и аккаунтом Google. Если время в событиях не будет совпадать с датами, указанными в сделке amoCRM — измените эту цифру

#9 Добавить в список — данные блок собирает список гостей, в значение кладем Email контакта amoCRM

Данный блок будет отправлять запрос на App Script

URL: Скопируйте ссылку из развертывания (во всех процессах она будет одинаковая)

Метод: POST

Доступные параметры:

{
  //Обязательные поля
  "action": "create",             // Действие для скрипта (обязательно)
  "summary": "string",           // Заголовок события (обязательно)
  "startDate": "string",         // Начало: "2026-07-07T10:00:00+05:00" или "2026-07-07" при allDay (обязательно)
  "endDate": "string",           // Конец: "2026-07-07T11:00:00+05:00" или "2026-07-07" при allDay (обязательно)

  // Календарь (опционально, по умолч. primary)
  "calendarId": "string",        // ID календаря, e.g. "mycalendar@gmail.com"
  // "calendarName": "string",   // Или название календаря (если нет calendarId)

  //Описание события (опционально) 
  "description": "string",       // Описание, e.g. "Обсуждение договора"
  "location": "string",          // Место, e.g. "Офис, 3 этаж"

  //Дата/время (опционально) 
  "allDay": false,               // true — событие на весь день
  "timeZone": "string",          // Часовой пояс, e.g. "Asia/Yekaterinburg" (по умолч. ScriptTimeZone)

  //Гости (опционально)
  "guests": [                    // Массив строк (email)
    "ivanov@example.com",
    "petrov@example.com"
  ],

  // Настройки приватности (опционально)
  "visibility": "default",       // "default" | "public" | "private" | "confidential"
  "color": 1,                    // Цвет события: число от 1 до 11

  // Права гостей (опционально) ---
  "guestsCanInviteOthers": false, // Гости могут приглашать других
  "guestsCanModify": false,       // Гости могут редактировать событие
  "guestsCanSeeGuests": true,     // Гости видят список остальных гостей

  //Напоминания (опционально)
  "reminders": [                 // Массив напоминаний
    { "method": "popup", "minutes": 30 },   // Всплывающее уведомление
    { "method": "email", "minutes": 60 }    // Уведомление на email
  ]
}

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

Скрипт выгрузит все календари, разрешенные на вашем аккаунте

И оставшаяся часть блоков:

#11 JSON декодировать — данный блок достает ответ от HTTP блока запроса
В нем будет содержаться следующая структура:

{
  "item":{
  "success":true, //результат, создано событие или нет
  "eventId":"3j290fodjk1qrtjeb4mfrdo1p4@google.com", //ID события
  "iCalUID":"3j290fodjk1qrtjeb4mfrdo1p4@google.com",//iCalUID
  "calendarId":"mycalendar@gmail.com"//ID календаря
  }
}

#12 Регулярные выражения — скрипт отдает ID события в формате <ID>@google.com, но @google.com не считается частью ID события, данный блок обрежет эту часть строки, оставляя только ID события, т.е. на выходе получится: 3j290fodjk1qrtjeb4mfrdo1p4

#10 Обновить сделку — данный блок записывает в поле ID события в amoCRM ID созданного события

Процесс обновления события

Процесс обновления события актуальными данными из сделки будет выглядеть следующим образом:

Описание блоков схемы

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

Параметры, доступные для обновления события аналогичны параметрам создания события, но дополнительно добавляется eventId — в него передается ID события из сделки amoCRM

Также не забудьте поправить action на update, чтобы скрипт понимал, что это запрос на обновление события

#8 JSON декодировать — раскладывает ответ от HTTP запрос в JSON формат
В ответе будут актуальные данные обновленного события и ключ success с результатом обновления

{
"item":{
    "success":true,
            "event":{
            "eventId":"hteu82jslj4iron567umbml8tk",
            "summary":"Событие из амо",
            "description":"Здесь находится описание события",
            "location":"",
            "startDate":"2026-07-06T16:15:00+05:00",
            "endDate":"2026-07-06T18:15:00+05:00",
            "allDay":false,
            "lastUpdated":"2026-07-06T10:51:02.641Z",
            "status":"confirmed",
            "htmlLink":"https://www.google.com/calendar/event?eid=aHRldTgyanNsajRpcm9uNTY3dW1ibWw4dGsgYW5kcmV5bW9yZWtpdEBt"
            }
    }
}

#11 Проверка — проверяет успешность выполнения. В зависимости от проверки — будет прикрепляться свое примечание в сделку amoCRM

#10 Создать примечание — отправляем в примечание сделки amoCRM текст об успешном создании события, прикрепляя ссылку на событие

#12 Создать примечание — отправляем в примечание сделки amoCRM текст об ошибке, прикрепляя данные ошибки

Процесс удаления события

Процесс удаления события будет выглядеть следующим образом:

Данный процесс будет удалять событие, очищать поля сделки от данных по событию после удаления и прикреплять примечание об удаленном событии

Описание блоков схемы

Блоки #2 и #3 также аналогичны процессам по созданию или обновления события

#4 HTTP запрос — данный блок отправляет "action": "delete" и ID события, чтобы скрипт удалил событие

#5 JSON декодировать — получаем ответ от HTTP запроса

#6 Проверка — проверяет, удалилось событие или нет

#7 Создать примечание — если событие удалилось, прикрепляет в сделку примечание об успешном удалении

#9 Обновить сделку — очищает поля в сделке amoCRM, связанные с событием

#8 Создать примечание — если событие не удалилось, прикрепляет примечание в сделку с текстом ошибки

Процесс поиска события

Процесс поиска события будет выглядеть следующим образом:

Блоки #209 и #210 аналогичны процессам обновления и создания события

#211 HTTP запрос — отправляет на скрипт ID события из сделки, а также событие "action": "find"

#212 JSON-декодировать — получает ответ от HTTP запроса с данными

#214 Проверка — проверяет, нашлось событие или нет

#215 Создать примечание — создает примечание к сделке с данными о событии

#216 Создать примечание — создает примечание к сделке с сообщением о том, что событие не найдено

Тестирование

Создание событие

Перейдем в тестовую сделку amoCRM и заполним данные, по которым будет создаваться событие

Нажимаем на кнопку Tap KIT Создать событие и проверяем календарь

Как видим — событие успешно создалось

В MoreKIT также можно увидеть событие, если перейти в События из процесса по созданию событий

В сделку должен был записаться ID созданного события

Если ID есть — значит все настроено верно

Обновление события

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

Проверим, сместилось ли оно в календаре

Событие обновилось

Найти событие

Следующим шагом — получим актуальные данные события. Нажмем на кнопку Найти событие

Поиск события

В текущем примере — данные создаются в примечании сделки, при желании — вы можете либо заполнять поля актуальными данными, либо настроить автоматизированный запуск процессом с типом По времени

Удаление события

Следующим шагом — удалим событие через кнопку и проверим, чтобы оно удалилось на стороне календаря

После нажатия — поля в сделке должны очиститься

На стороне календаря также видим, что событие удалилось

Подведем итоги

В данной статье рассмотрели пример интеграции с Google Calendar с помощью виджета TapKIT. Логика интеграции завязана на кнопках

Было сделано:

  • создание события
  • обновление события
  • получение данных о событии
  • удаление события

Данный скрипт дает большой простор для интеграции, вы можете подстроить его под свой бизнес процесс, например — настроить автоматическую синхронизацию с помощью процесса По времени, либо автоматически создавать события по встроенным триггерам в amoCRM