Fetch · Заявка на услугу

Заявка на услугу через Fetch

Услуга, контакты и описание задачи: передача текстовых полей через Fetch в Прилетела.

Когда подходит

Для ремонта, консультаций и профессиональных услуг. Поле «Услуга» помогает отличать направления, а описание задачи даёт контекст до первого контакта. Подходит, если вы редактируете HTML и JavaScript и хотите управлять отправкой самостоятельно.

Весь обработчик включён в пример. Не подключайте к этой же форме embed/v1.js или второй обработчик: они могут отправить заявку дважды.

Понадобятся аккаунт Прилетела, адрес приёма заявок выбранной формы и возможность редактировать HTML своего сайта.

Поля: ваше имя, телефон, услуга, описание задачи. Звёздочкой отмечены обязательные поля примера. Проверка обязательности в браузере не заменяет серверную валидацию.

Форма и готовый код

Демонстрация

Данные никуда не отправляются и не сохраняются. Используйте вымышленные контакты.

HTML + Fetch
<style>

.priletela-embedded{max-width:560px;width:100%;margin:0 auto;padding:16px;box-sizing:border-box;font:16px/1.5 Arial,sans-serif;color:#111412;letter-spacing:0}
.priletela-embedded form{display:grid;gap:16px;margin:0;padding:0}
.priletela-embedded label{display:block;margin:0;font:inherit}
.priletela-embedded input,.priletela-embedded textarea,.priletela-embedded button{box-sizing:border-box;max-width:100%;min-width:0;font:inherit;border-radius:6px}
.priletela-embedded input:not([type=checkbox]),.priletela-embedded textarea{display:block;width:100%;margin:6px 0 0;padding:12px;border:1px solid #99aaa1;background:#fff;color:#111412}
.priletela-embedded textarea{min-height:120px;resize:vertical}
.priletela-embedded input[type=checkbox]{width:18px;height:18px;margin:0 8px 0 0;vertical-align:middle;accent-color:#0b6b50}
.priletela-embedded input[hidden]{display:none}
.priletela-embedded button{justify-self:start;min-height:44px;padding:10px 20px;border:0;background:#0b6b50;color:#fff;cursor:pointer}
.priletela-embedded button:disabled{opacity:.6;cursor:wait}
.priletela-embedded a{color:#0b6b50;text-decoration:underline}
.priletela-embedded :focus-visible{outline:2px solid #0b6b50;outline-offset:3px}
.priletela-embedded [data-priletela-status]{margin:0;overflow-wrap:anywhere}

</style>
<div class="priletela-embedded">
<form action="YOUR_FORM_ENDPOINT" method="POST">
  <fieldset disabled style="border:0;padding:0;margin:0;min-width:0;display:grid;gap:16px">
    <label>Ваше имя <input type="text" name="name" maxlength="20000"></label>
    <label>Телефон <input type="tel" name="phone" maxlength="20000" required></label>
    <label>Услуга <input type="text" name="service" maxlength="20000" required></label>
    <label>Описание задачи <textarea name="message" maxlength="20000"></textarea></label>
    <input type="text" name="website" hidden tabindex="-1" autocomplete="off" aria-hidden="true">
    <label><input type="checkbox" name="consent" value="yes" required> Согласен на обработку данных на условиях <a href="YOUR_POLICY_URL" target="_blank" rel="noopener noreferrer">политики владельца сайта</a></label>
    <button type="submit">Отправить</button>
  </fieldset>
  <p data-priletela-status role="status" aria-live="polite" aria-atomic="true">Отправка пока недоступна. Проверьте подключение JavaScript.</p>
</form>
<script>
(() => {
  const form = document.currentScript.previousElementSibling;
  const fields = form.querySelector('fieldset');
  const button = form.querySelector('button[type=submit]');
  const status = form.querySelector('[role=status]');
  function createSubmissionClient(endpoint, onBusy, onStatus) {
  let attempt = null, pending = false, disposed = false, controller = null, timer = null;
  return {
    async submit(form) {
      if (disposed || pending || !form.reportValidity()) return;
      const body = new URLSearchParams(new FormData(form)).toString();
      for (const [name, value] of new URLSearchParams(body)) {
        if (name !== 'website' && value.trim().length > 20000) {
          onStatus('Поле «' + name + '» превышает 20000 символов. Сократите текст.');
          return;
        }
      }
      if (new TextEncoder().encode(body).byteLength > 512 * 1024) {
        onStatus('Объём заявки превышает 512 КБ. Сократите текст. Поля сохранены.');
        return;
      }
      if (attempt && attempt.body !== body && !window.confirm(
        'Данные изменены. Предыдущая заявка могла сохраниться. Отправить новое обращение?'
      )) return;
      try {
        if (!attempt || attempt.body !== body) {
          const bytes = new Uint8Array(16);
          window.crypto.getRandomValues(bytes);
          attempt = {body, key: Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')};
        }
      } catch {
        onStatus('Браузер не поддерживает безопасную отправку. Обновите браузер.');
        return;
      }
      pending = true;
      onBusy(true);
      onStatus('Отправляем заявку…');
      controller = new AbortController();
      timer = setTimeout(() => controller.abort(), 25000);
      try {
        const response = await fetch(endpoint, {
          method: 'POST', credentials: 'omit', signal: controller.signal,
          headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Idempotency-Key': attempt.key},
          body: attempt.body
        });
        if (response.status === 429) throw new Error('Слишком много попыток. Подождите минуту и повторите отправку.');
        if (response.status === 409) throw new Error('Ключ отправки связан с другими данными. Свяжитесь с владельцем сайта.');
        if (response.status === 404) throw new Error('Форма недоступна. Свяжитесь с владельцем сайта.');
        if (response.status === 413) throw new Error('Объём заявки превышает 512 КБ. Сократите текст. Поля сохранены.');
        const result = await response.json();
        if (response.status === 400 && typeof result?.error === 'string') throw new Error(result.error);
        if (!response.ok || result?.ok !== true) throw new Error('Не удалось отправить заявку. Поля сохранены; повторите попытку.');
        if (disposed) return;
        form.reset();
        attempt = null;
        onStatus(result.test ? 'Пробная заявка принята. Вернитесь в мастер подключения.' : 'Спасибо! Ваша заявка принята.');
      } catch (error) {
        if (!disposed) onStatus(error instanceof Error && error.name === 'Error'
          ? error.message
          : 'Подтверждение не получено. Поля сохранены. Повторите без изменения данных: будет использован тот же ключ.');
      } finally {
        clearTimeout(timer);
        controller = null;
        pending = false;
        if (!disposed) onBusy(false);
      }
    },
    dispose() { disposed = true; clearTimeout(timer); controller?.abort(); }
  };
}
  const client = createSubmissionClient(form.action, busy => {
    fields.disabled = busy;
    form.setAttribute('aria-busy', String(busy));
    button.textContent = busy ? 'Отправляем…' : 'Отправить';
  }, message => { status.textContent = message; });
  form.addEventListener('submit', event => { event.preventDefault(); client.submit(form); });
  fields.disabled = false;
  status.textContent = '';
})();
</script>
</div>

Замените YOUR_FORM_ENDPOINT на адрес формы из кабинета, а YOUR_POLICY_URL — на адрес собственных юридических документов. Скопируйте пример целиком. Он содержит собственный обработчик и не использует embed/v1.js.

Как подключить форму через Fetch

Это HTML-форма со своим JavaScript-обработчиком Fetch API. Подходит для сайта, где вы можете редактировать и запускать HTML и JavaScript. Подключать embed/v1.js к этой форме не нужно: второй обработчик может создать отдельную отправку.

  1. Создайте аккаунт или откройте мастер подключения. Создайте проект, назовите форму и скопируйте её адрес приёма из настроек.
  2. Скопируйте весь пример на страницу сайта. Скрипт должен оставаться сразу после закрывающего тега формы внутри общего блока: так каждый экземпляр получает свой обработчик.
  3. Замените YOUR_FORM_ENDPOINT на полный адрес выбранной формы, а YOUR_POLICY_URL — на ссылку на документы владельца сайта. Адаптируйте текст согласия. Пароли, токены Google и другие серверные секреты в код не добавляйте.
  4. Опубликуйте страницу. Если политика CSP запрещает встроенные скрипты или стили, согласуйте их подключение через nonce либо вынесите код в разрешённые файлы. При выносе JavaScript в отдельный файл передавайте ему ссылку на нужную форму вместо document.currentScript. Не отключайте CSP целиком.
  5. Откройте опубликованную страницу. До запуска JavaScript поля отключены; после загрузки можно вводить данные. При отправке повторный клик блокируется, а подтверждение показывается только после ответа Прилетела.

Как проходит отправка

Обработчик делает POST с текстовыми полями в формате application/x-www-form-urlencoded, без cookie. Заголовок Idempotency-Key защищает повтор неизменённой попытки. CORS текущего API разрешает этот заголовок; не используйте режим no-cors, иначе нельзя прочитать подтверждение. Для своего сайта разрешите адрес Прилетела в connect-src.

При ошибке или тайм-ауте через 25 секунд поля и ключ остаются в памяти страницы. Повторите отправку без изменения данных. Если поля изменены после неудачной попытки, пример попросит подтвердить новую заявку: предыдущая могла уже сохраниться. После успеха поля сбрасываются и следующая отправка получает новый ключ.

Каждая форма имеет собственное состояние отправки; можно разместить несколько полных фрагментов на одной странице. Перезагрузка страницы сбрасывает поля и ключ. Отмена запроса не отменяет уже сохранённую на сервере заявку; автоматического повтора после перезагрузки нет.

Как проверить подключение

  1. Начните проверку в мастере Прилетела. Временно разместите на своём сайте пробный фрагмент из мастера: он помечает заявку как тестовую. Отправьте вымышленные данные с опубликованной страницы и дождитесь пробной заявки в мастере.
  2. Верните выбранный пример React или Fetch с обычным адресом формы. Проверьте его в тестовом проекте и убедитесь, что все поля появились в карточке. Обычная отправка без пробного кода считается рабочей заявкой, даже с вымышленными контактами.
  3. При необходимости подтвердите email-получателя и подключите Telegram отдельно. Сохранение заявки не подтверждает доставку уведомлений.

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

Частые ошибки

Заявка не появилась в кабинете

Проверьте адрес формы, выбранный проект, папки спама и пробных заявок. Форма должна быть включена. Не заполняйте скрытое поле website: оно служит защитой от спама.

Кнопка отправки недоступна

Проверьте выполнение JavaScript и установку всего примера. Для React нужен собранный компонент JSX; embed/v1.js подключать не надо. Отключённый JavaScript или блокировщик может помешать подключению.

Ошибка соединения или блокировка браузером

Проверьте HTTPS и адрес приёма. Для запроса разрешите https://priletela.ru в connect-src. Сам JavaScript и стили подключайте согласно CSP вашего сайта; не используйте no-cors. Не отключайте CSP целиком. Поля при ошибке остаются заполненными.

Поля заполнены, но форма не отправляется

Проверьте обязательные поля, формат email и чекбокс. Код 429 означает временное ограничение частоты: подождите и повторите. Не добавляйте файлы: этот пример принимает текстовые поля.

Заявка есть, а уведомления нет

Email и Telegram подключаются отдельно в настройках проекта. Проверьте подтверждение адреса, получателя и состояние канала. Принятая заявка не гарантирует доставку письма или сообщения.

Данные посетителей

Владелец сайта размещает собственные документы и определяет законные основания обработки данных. Чекбокс в коде — заготовка, которую нужно адаптировать к своему процессу. Подключение Прилетела само по себе не обеспечивает соответствие 152-ФЗ. Не собирайте через пример пароли, платёжные реквизиты или другую чувствительную информацию.

Другие примеры

Вернуться в каталог

Готовы получать заявки с сайта?

Подключите свою форму к Прилетела и получайте обращения в едином кабинете. Email и Telegram можно настроить отдельно.

Нужна помощь с подключением? Поддержка в Telegram: @Stop_routine