Где хранить ключ, если вы не программист

Базовый рецепт. Форма записи на сайте, связка с таблицей, бот — всё строится поверх него. Сначала находим ключу правильное место.

20 минутНачните отсюда

Почему ключ нельзя вставить в сайт

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

Код, вставленный в конструктор сайта, виден любому посетителю: правая кнопка мыши, «Исходный код страницы» — и ключ у него. Поэтому Планёрка и не принимает обращения напрямую из браузера: даже если вы вставите ключ на страницу, ничего не заработает.

Ключу нужно место, где он лежит закрыто, а наружу выдаётся только результат. Такое место называется прослойкой. Соберём её на Google Apps Script — это бесплатный сервис, он идёт вместе с обычным Google-аккаунтом. Ни сервера, ни хостинга, ни карты не нужно.

Ваш сайтформа, бот, таблица
Google Apps Scriptздесь заперт ключ
Планёркарасписание и записи
Ключ живёт в середине цепочки. На сайт попадает только ссылка, по которой можно узнать свободное время и записать человека.
  1. 01Ключ
  2. 02Скрипт
  3. 03Ключ внутрь
  4. 04Публикация
  5. 05Проверка
1

Создать ключ

Откройте Ключи API и создайте ключ. Дайте ему понятную метку, например «Форма на сайте»: если однажды придётся его отозвать, вы будете знать, что именно сломается.

Ключ показывается один раз

Скопируйте его целиком, вместе с началом cal_. Если закрыли окно и не сохранили — просто создайте новый, старый отзовите.

2

Создать прослойку

Откройте script.google.com и нажмите «Новый проект». Всё, что уже написано в файле, удалите и вставьте код ниже целиком. Разбираться в нём не нужно — он вставляется один раз и больше не меняется.

var API = "https://planerka.app/rest/v2";

function key() {
  return PropertiesService.getScriptProperties().getProperty("PLANERKA_API_KEY");
}

function call(method, path, version, body) {
  return UrlFetchApp.fetch(API + path, {
    method: method,
    contentType: "application/json",
    headers: { Authorization: "Bearer " + key(), "cal-api-version": version },
    payload: body ? JSON.stringify(body) : undefined,
    muteHttpExceptions: true
  });
}

function json(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}

function doGet(e) {
  var p = e.parameter || {};

  // ?action=types — открытые типы встреч владельца ключа
  if (p.action === "types") {
    var rt = call("GET", "/event-types", "2024-08-13");
    var st = JSON.parse(rt.getContentText());
    if (rt.getResponseCode() !== 200) return json({ error: st.error || st });

    var data = st.data || {};
    var list = [];
    Object.keys(data).forEach(function (k) {
      var t = data[k];
      if (t && !t.hidden) {
        list.push({
          id: t.id,
          title: t.title,
          slug: t.slug,
          description: t.description || "",
          minutes: t.lengthInMinutes || t.length,
          price: t.price || 0,
          currency: t.currency || "RUB"
        });
      }
    });
    return json(list);
  }

  // свободное время на один день: ?eventTypeId=…&date=2026-07-30&timeZone=…
  var id = Number(p.eventTypeId);
  if (!id) return json({ error: { code: "NO_EVENT_TYPE", message: "нужен eventTypeId" } });

  var tz = p.timeZone || "Europe/Moscow";
  var start = p.date || Utilities.formatDate(new Date(), tz, "yyyy-MM-dd");
  var end = Utilities.formatDate(
    new Date(new Date(start + "T00:00:00Z").getTime() + 86400000), "UTC", "yyyy-MM-dd"
  );

  var cache = CacheService.getScriptCache();
  var ck = "slots|" + id + "|" + start + "|" + tz;
  var hit = cache.get(ck);
  if (hit) return ContentService.createTextOutput(hit).setMimeType(ContentService.MimeType.JSON);

  var url = "/slots?eventTypeId=" + id + "&start=" + start + "&end=" + end +
            "&timeZone=" + encodeURIComponent(tz);

  var r = call("GET", url, "2024-09-04");
  var s = JSON.parse(r.getContentText());
  if (r.getResponseCode() !== 200) return json({ error: s.error || s });

  var out = JSON.stringify(s.data || {});
  cache.put(ck, out, 60);
  return ContentService.createTextOutput(out).setMimeType(ContentService.MimeType.JSON);
}

function doPost(e) {
  var b = JSON.parse(e.postData.contents);

  if (b.trap) return json({ ok: true });
  if (!b.eventTypeId || !b.start || !b.name || !b.email) {
    return json({ ok: false, error: { code: "BAD_REQUEST" } });
  }

  var r = call("POST", "/bookings", "2024-08-13", {
    eventTypeId: Number(b.eventTypeId),
    start: b.start,
    attendee: {
      name: b.name,
      email: b.email,
      phoneNumber: b.phone || null,
      timeZone: b.timeZone || "Europe/Moscow",
      language: "ru"
    },
    metadata: { source: "site" }
  });

  var code = r.getResponseCode();
  if (code === 201) return json({ ok: true, data: JSON.parse(r.getContentText()).data });
  if (code === 409) return json({ ok: false, reason: "taken" });
  return json({ ok: false, error: JSON.parse(r.getContentText()) });
}
Код прослойки. Ключа в нём нет — его положим отдельно на следующем шаге.

Сохраните файл

Нажмите значок дискеты или Ctrl+S. Публикуется всегда последняя сохранённая версия, а не то, что вы видите на экране.

3

Положить ключ внутрь

Слева нажмите шестерёнку «Настройки проекта», пролистайте до «Свойства скрипта» и добавьте одно свойство.

Настройки проекта · Свойства скрипта
СвойствоЗначение
PLANERKA_API_KEYcal_7f3a…
Имя свойства должно совпадать буква в букву. Ключ вставляется целиком, без пробелов по краям.

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

4

Опубликовать

Кнопка «Развернуть» в правом верхнем углу → «Новое развёртывание» → тип «Веб-приложение». Google попросит разрешение — согласитесь.

Новое развёртывание
Тип
Веб-приложение
Запуск от имени
Меня
Доступ
Все
Доступ обязательно «Все» — иначе ваш сайт не сможет обратиться к прослойке.

Скопируйте полученную ссылку — она заканчивается на /exec. Это и есть адрес вашей прослойки, дальше он понадобится везде.

5

Проверить

Откройте в браузере ту же ссылку, дописав в конец ?action=types:

https://script.google.com/macros/s/…/exec?action=types

Должен появиться список ваших открытых типов встреч:

[{"id":42165,"title":"Консультация","minutes":50,"price":0,"currency":"RUB"}]
Число в поле id — номер типа встречи, он пригодится во втором рецепте.

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

Что умеет прослойка

Три обращения — из них собирается любая форма записи.

…/exec?action=typesоткрытые типы встреч с длительностью и ценой
…/exec?eventTypeId=42165&date=2026-07-30свободное время на эту дату
POST на …/execсоздаёт запись

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

Если что-то пошло не так

Откройте ссылку с ?action=types и смотрите, что она отвечает.

{"error":{"code":"UNAUTHORIZED"…}}Ключ не принят. Чаще всего скопирован не полностью, попал с пробелом по краям, создан на другом аккаунте или свойство названо иначе, чем PLANERKA_API_KEY.
Красная надпись со словом TypeErrorКод вставлен не целиком. Удалите всё содержимое файла и вставьте заново.
{"error":{"code":"NO_EVENT_TYPE"…}}Так и должно быть, если запрашивать время без номера типа встречи. Проверяйте адресом с ?action=types.
{}Ключ и тип верные, но в этот день всё занято либо на него не заведено расписание.

Две ловушки, на которых теряют больше всего времени

Код не сохранён. Публикуется последняя сохранённая версия. Всегда нажимайте Ctrl+S перед развёртыванием.

«Новое развёртывание» выдаёт новую ссылку, а старая продолжает работать по прежнему коду. Если правки «не применились» — вы почти наверняка проверяете старый адрес. Чтобы сохранить прежний, идите в «Управление развёртываниями», нажмите карандаш у активного и выберите «Версия: Новая».

А вот менять сам ключ можно свободно: свойства читаются в момент обращения, переразвёртывание для этого не нужно.

Правила обращения с ключом

  • На каждую задачу — свой ключ со своей меткой. Тогда отзыв одного не ломает остальные.
  • Ключ живёт только в свойствах скрипта. Не в коде, не в переписке, не в заметках.
  • Отдавать ключ исполнителю можно, но после сдачи работы отзовите его и создайте новый.
  • Если ключ где-то засветился — отзовите его в разделе «Ключи API», создайте новый и впишите в свойства. Минутное дело.
  • Не заставляйте прослойку пересылать наружу список встреч: там контакты всех ваших клиентов. Она должна отдавать только типы встреч, свободное время и подтверждение записи.
Следующий рецептСвоя форма записи на ТильдеПять экранов на вашей странице: формат, дата, время, контакты и подтверждение.