Fetch · Запрос стоимости
Запрос стоимости через Fetch
Компания, email и требования: запрос расчёта через Fetch с подтверждением сохранения заявки.
Когда подходит
Для заказных работ и B2B-обращений, где цену нельзя назвать без уточнений. Форма собирает запрос, но не рассчитывает стоимость и не принимает оплату. Подходит, если вы редактируете HTML и JavaScript и хотите управлять отправкой самостоятельно.
Весь обработчик включён в пример. Не подключайте к этой же форме embed/v1.js или второй обработчик: они могут отправить заявку дважды.
Понадобятся аккаунт Прилетела, адрес приёма заявок выбранной формы и возможность редактировать HTML своего сайта.
Поля: контактное лицо, компания, email, что нужно рассчитать. Звёздочкой отмечены обязательные поля примера. Проверка обязательности в браузере не заменяет серверную валидацию.
Форма и готовый код
Демонстрация
Данные никуда не отправляются и не сохраняются. Используйте вымышленные контакты.
<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" required></label>
<label>Компания <input type="text" name="company" maxlength="20000"></label>
<label>Email <input type="email" name="email" maxlength="20000" required></label>
<label>Что нужно рассчитать <textarea name="message" maxlength="20000" required></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 к этой форме не нужно: второй обработчик может создать отдельную отправку.
- Создайте аккаунт или откройте мастер подключения. Создайте проект, назовите форму и скопируйте её адрес приёма из настроек.
- Скопируйте весь пример на страницу сайта. Скрипт должен оставаться сразу после закрывающего тега формы внутри общего блока: так каждый экземпляр получает свой обработчик.
- Замените
YOUR_FORM_ENDPOINTна полный адрес выбранной формы, аYOUR_POLICY_URL— на ссылку на документы владельца сайта. Адаптируйте текст согласия. Пароли, токены Google и другие серверные секреты в код не добавляйте. - Опубликуйте страницу. Если политика CSP запрещает встроенные скрипты или стили, согласуйте их подключение через nonce либо вынесите код в разрешённые файлы. При выносе JavaScript в отдельный файл передавайте ему ссылку на нужную форму вместо document.currentScript. Не отключайте CSP целиком.
- Откройте опубликованную страницу. До запуска JavaScript поля отключены; после загрузки можно вводить данные. При отправке повторный клик блокируется, а подтверждение показывается только после ответа Прилетела.
Как проходит отправка
Обработчик делает POST с текстовыми полями в формате application/x-www-form-urlencoded, без cookie. Заголовок Idempotency-Key защищает повтор неизменённой попытки. CORS текущего API разрешает этот заголовок; не используйте режим no-cors, иначе нельзя прочитать подтверждение. Для своего сайта разрешите адрес Прилетела в connect-src.
При ошибке или тайм-ауте через 25 секунд поля и ключ остаются в памяти страницы. Повторите отправку без изменения данных. Если поля изменены после неудачной попытки, пример попросит подтвердить новую заявку: предыдущая могла уже сохраниться. После успеха поля сбрасываются и следующая отправка получает новый ключ.
Каждая форма имеет собственное состояние отправки; можно разместить несколько полных фрагментов на одной странице. Перезагрузка страницы сбрасывает поля и ключ. Отмена запроса не отменяет уже сохранённую на сервере заявку; автоматического повтора после перезагрузки нет.
Как проверить подключение
- Начните проверку в мастере Прилетела. Временно разместите на своём сайте пробный фрагмент из мастера: он помечает заявку как тестовую. Отправьте вымышленные данные с опубликованной страницы и дождитесь пробной заявки в мастере.
- Верните выбранный пример React или Fetch с обычным адресом формы. Проверьте его в тестовом проекте и убедитесь, что все поля появились в карточке. Обычная отправка без пробного кода считается рабочей заявкой, даже с вымышленными контактами.
- При необходимости подтвердите 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
