
📤 Отправка форм в headless WordPress: REST API Contact Form 7 и Gravity Forms
Вы строите сайт на WordPress, и форма обратной связи для вас, решённый вопрос. Плагины вроде Contact Form 7 дают готовый HTML, валидацию, хранение отправок и десятки интеграций. Нажать «Установить» и вставить шорткод, дело двух минут.
Но всё меняется, когда WordPress становится headless CMS. Вы отвечаете за фронтенд целиком: React, Vue, чистый HTML/JS. И плагин форм, который раньше рендерил разметку за вас, больше не контролирует клиентскую часть. Его REST API, однако, никуда не делся. Достаточно отправить POST на правильный эндпоинт, и вся мощь плагина (проверка полей, хранение, интеграции) остаётся при вас.
На практике через REST API плагинов форм можно закрыть и сугубо «традиционные» кейсы. Скажем, верстаете кастомную тему на Tailwind, и фиксированная разметка CF7 с её жёсткой структурой классов режет глаз. Отправка через API позволяет контролировать каждый пиксель формы без отказа от наработанной экосистемы плагина.
💡 Быстрый обзор:
- Какие эндпоинты дают Contact Form 7 и Gravity Forms и как их активировать в headless-окружении.
- В каком формате отправлять поля, чтобы плагин корректно принял данные и вернул результат.
- Как собрать HTML-форму, привязать fetch-запрос и показать пользователю success-сообщение или ошибки валидации.
- Как унифицировать разнобой форматов ответа от CF7 и Gravity Forms в единую удобную структуру.
Что нужно знать о конечных точках
Отправка данных через REST API, технически простая часть. Оба плагина ожидают POST на эндпоинт, где динамический сегмент URL, идентификатор конкретной формы.
Contact Form 7 предоставляет REST API сразу после активации. Эндпоинт выглядит так:
1 https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback
С версии 5.8 (август 2023) Contact Form 7 перешёл на хешированные SHA-1 идентификаторы форм. Старые числовые ID продолжают работать, но для новых форм идентификатор нужно брать из URL страницы редактирования формы в админке, это последний сегмент после post=. На апрель 2026 года плагин насчитывает более 10 миллионов активных установок и протестирован вплоть до WordPress 7.0.
Gravity Forms использует REST API v2 (доступен с версии 2.4):
1 https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions
Важный нюанс: REST API Gravity Forms отключён по умолчанию. Чтобы активировать, зайдите в настройки плагина → вкладка REST API → отметьте «Enable access to the API». API-ключ для эндпоинта отправки форм не нужен, он публичный по дизайну. Идентификатор формы в Gravity Forms, числовой, виден в админке при редактировании.
Структура тела запроса
Возьмём для примера форму из пяти полей: обязательные текст, email и дата (до 4 октября 1957 года), опциональный textarea и обязательный чекбокс.

Contact Form 7 ожидает ключи в формате, заданном через синтаксис тегов формы. Ключ совпадает с name соответствующего поля в HTML:
1 { 2 "somebodys-name": "Marian Kenney", 3 "any-email": "[email protected]", 4 "before-space-age": "1922-03-11", 5 "optional-message": "", 6 "fake-terms": "1" 7 }
Gravity Forms использует другой подход: автоматически генерируемые инкрементные идентификаторы с префиксом input_. ID поля виден прямо в админке при редактировании конкретного поля.

Для той же формы тело запроса к Gravity Forms выглядит иначе:
1 { 2 "input_1": "Marian Kenney", 3 "input_2": "[email protected]", 4 "input_3": "1922-03-11", 5 "input_4": "", 6 "input_5_1": "1" 7 }
Ключевой вывод: если вы дадите HTML-инпутам атрибуты name, совпадающие с ожидаемыми ключами плагина, то сопоставление происходит автоматически, FormData соберёт данные в нужном формате без ручного маппинга.
Собираем HTML и отправляем запрос
Для Contact Form 7 HTML-разметка будет такой (обратите внимание на action, это эндпоинт, а name полей совпадает с ключами выше):
1 <form action="https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback" method="post"> 2 <label for="somebodys-name">Ваше имя</label> 3 <input id="somebodys-name" type="text" name="somebodys-name" required> 4 5 <label for="any-email">Email</label> 6 <input id="any-email" type="email" name="any-email" required> 7 8 <label for="before-space-age">Дата</label> 9 <input id="before-space-age" type="date" name="before-space-age" max="1957-10-04" required> 10 11 <label for="optional-message">Сообщение</label> 12 <textarea id="optional-message" name="optional-message"></textarea> 13 14 <label> 15 <input type="checkbox" name="fake-terms" value="1" required> 16 Я принимаю условия 17 </label> 18 19 <button type="submit">Отправить</button> 20 </form>
Для Gravity Forms меняются только action и атрибуты name:
1 <form action="https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions" method="post"> 2 <label for="input_1">Ваше имя</label> 3 <input id="input_1" type="text" name="input_1" required> 4 <!-- ... --> 5 </form>
Теперь отправка через JavaScript, FormData сам собирает значения по name, ничего маппить не нужно:
1 const formSubmissionHandler = (event) => { 2 event.preventDefault(); 3 4 const formElement = event.target; 5 const { action, method } = formElement; 6 const body = new FormData(formElement); 7 8 fetch(action, { method, body }) 9 .then((response) => response.json()) 10 .then((response) => { 11 if (isFormSubmissionError(response)) { 12 // Обработка ошибок валидации 13 handleValidationErrors(response); 14 return; 15 } 16 // Успешная отправка 17 handleSuccess(response); 18 }) 19 .catch((error) => { 20 // Сетевая ошибка или недоступность сервера 21 handleNetworkError(error); 22 }); 23 }; 24 25 const formElement = document.querySelector("form"); 26 formElement.addEventListener("submit", formSubmissionHandler);
Данные ушли. Но пользователю этого мало, ему нужна обратная связь: сообщение об успехе, подсветка полей с ошибками, глобальное уведомление. К счастью, оба плагина возвращают эту информацию в ответе.
Валидация: сервер решает, клиент показывает
Помимо встроенной HTML5-валидации (атрибуты required, type="email" и max) разумно положиться на серверную проверку правил, которую дают плагины. Почему: правила настраиваются в админке WordPress централизованно, а дублировать их на клиенте, двойная работа и источник расхождений.
И Contact Form 7, и Gravity Forms возвращают ошибки валидации прямо в теле ответа. Для сложных сценариев, условные поля, зависимая валидация, опора на серверную проверку особенно выигрышна: не нужно синхронизировать логику между фронтендом и настройками плагина.
Задача сводится к трём шагам: разобрать JSON-ответ, извлечь сообщения об ошибках и вставить их в DOM рядом с соответствующими полями.
Форматы ответов и их нормализация
Ответ Contact Form 7 при ошибке валидации:
1 { 2 "into": "#", 3 "status": "validation_failed", 4 "message": "One or more fields have an error. Please check and try again.", 5 "posted_data_hash": "", 6 "invalid_fields": [ 7 { 8 "into": "span.wpcf7-form-control-wrap.somebodys-name", 9 "message": "The field is required.", 10 "idref": null, 11 "error_id": "-ve-somebodys-name" 12 } 13 ] 14 }
При успехе ответ компактнее:
1 { 2 "into": "#", 3 "status": "mail_sent", 4 "message": "Thank you for your message. It has been sent.", 5 "posted_data_hash": "d52f9f9de995287195409fe6dcde0c50" 6 }
Ответ Gravity Forms при ошибке валидации устроен иначе:
1 { 2 "is_valid": false, 3 "validation_messages": { 4 "1": "This field is required.", 5 "2": "This field is required.", 6 "3": "This field is required.", 7 "5": "This field is required." 8 }, 9 "page_number": 1, 10 "source_page_number": 1 11 }
А успешный ответ содержит подтверждение внутри HTML:
1 { 2 "is_valid": true, 3 "page_number": 0, 4 "source_page_number": 1, 5 "confirmation_message": "<div>Thanks for contacting us! We will get in touch with you shortly.</div>", 6 "confirmation_type": "message" 7 }
Разница подходов очевидна: CF7 вкладывает ошибки в массив объектов с CSS-селекторами, а Gravity Forms, в плоский объект с числовыми ключами без префикса input_. Сообщение об успехе у Gravity Forms приходит в HTML-обёртке. Ключи полей в ответах CF7 «зашиты» в селекторы (например, span.wpcf7-form-control-wrap.somebodys-name) и требуют извлечения регуляркой.
Вместо того чтобы ветвить логику под каждый плагин, удобнее привести оба ответа к единому формату:
1 { 2 "isSuccess": false, 3 "message": "One or more fields have an error. Please check and try again.", 4 "validationError": { 5 "somebodys-name": "The field is required.", 6 "any-email": "The field is required.", 7 "input_3": "The field is required.", 8 "input_5": "This field is required." 9 } 10 }
При успехе isSuccess выставляется в true, а validationError приходит пустым объектом.
Код нормализации для Contact Form 7:
1 const normalizeContactForm7Response = (response) => { 2 const isSuccess = response.status === 'mail_sent'; 3 const message = isSuccess 4 ? response.message 5 : response.message || 'One or more fields have an error.'; 6 7 const validationError = isSuccess 8 ? {} 9 : Object.fromEntries( 10 response.invalid_fields.map((error) => { 11 const key = /cf7[-a-z]*.(.*)/.exec(error.into)[1]; 12 return [key, error.message]; 13 }) 14 ); 15 16 return { isSuccess, message, validationError }; 17 };
Код нормализации для Gravity Forms (обратите внимание: ключам ошибок добавляется префикс input_, чтобы они совпадали с ключами запроса):
1 const normalizeGravityFormsResponse = (response) => { 2 const isSuccess = response.is_valid; 3 const message = isSuccess 4 ? stripHtml(response.confirmation_message) 5 : 'There was a problem with your submission.'; 6 7 const validationError = isSuccess 8 ? {} 9 : Object.fromEntries( 10 Object.entries(response.validation_messages).map(([key, value]) => [ 11 `input_${key}`, 12 value, 13 ]) 14 ); 15 16 return { isSuccess, message, validationError }; 17 };
Теперь у вас есть единый объект ответа независимо от плагина. Остаётся дописать отображение ошибок и переключение классов на DOM-элементах, и форма готова к бою.
От нормализации к живому интерфейсу
Когда ответ приведён к единой структуре, отображение обратной связи сводится к манипуляции DOM. Добавить сообщение об ошибке рядом с полем, переключить класс у обёртки и показать глобальное уведомление: этих трёх действий достаточно для подавляющего большинства сценариев.
Для реактивного обновления интерфейса удобно использовать лёгкие декларативные библиотеки вроде Alpine.js. Минимальный синтаксис, отсутствие шага сборки и естественная интеграция с серверным ответом делают его практичным выбором для форм в headless-окружении. Подход с Alpine.js детально разбирался на CSS-Tricks, код из того материала практически без изменений ложится на нормализованный ответ, который мы получили выше.
Что в итоге
Повторить клиентскую часть, которую плагины форм дают «из коробки», для простых форм, задача на пару часов. Приятный бонус: абстрагировав ответ через функцию-нормализатор, вы получаете сменный бэкенд. Перейти с Contact Form 7 на Gravity Forms (или наоборот) можно без правок фронтенда, достаточно заменить эндпоинт и функцию нормализации.
Многостраничные формы, предпросмотр загруженных изображений, калькуляторы стоимости, да, это уже серьёзная разработка. Но чем более уникальные требования у проекта, тем сильнее аргумент в пользу собственного фронтенда поверх REST API: вы не боретесь с чужой разметкой и не обходите ограничения готового рендеринга.
Headless-подход к формам, не гипотетическое будущее. Уже сегодня плагины вроде Contact Form 7 и Gravity Forms дают полноценный REST API, а фронтенд-фреймворки позволяют собрать форму за часы, а не дни. Попробуйте на ближайшем проекте, где внешний вид формы критичен: возьмите CF7 или GF как бэкенд и соберите интерфейс с нуля. Скорее всего, вы удивитесь, насколько это прямолинейно.
⁉️🤔 Частые вопросы
Работает ли REST API Contact Form 7 на бесплатной версии?
Да, REST API доступен сразу после активации бесплатного плагина, никаких дополнительных настроек не требуется. На апрель 2026 года у Contact Form 7 более 10 миллионов активных установок, и REST API остаётся стабильной частью ядра плагина начиная с версии 4.8.
Чем отличаются хешированные ID форм в новых версиях Contact Form 7?
Начиная с версии 5.8 (август 2023) CF7 генерирует SHA-1 хеш в качестве идентификатора формы вместо числового ID. Старые числовые ID продолжают работать. Хеш можно найти в URL страницы редактирования формы в админке:
/wp-admin/admin.php?page=wpcf7&post=<HASH>&action=edit. В эндпоинте он подставляется так же, как и числовой ID.
Нужен ли API-ключ для отправки форм через Gravity Forms REST API?
Нет, эндпоинт
/gf/v2/forms/<ID>/submissionsне требует аутентификации для отправки. Однако сам REST API Gravity Forms отключён по умолчанию, его нужно включить в настройках плагина (Forms → Settings → REST API → Enable access to the API).
Можно ли использовать один и тот же JavaScript-код для Contact Form 7 и Gravity Forms?
Да, именно для этого нужна нормализация ответа. Обе функции-нормализатора (для CF7 и GF) возвращают объект с одинаковой структурой: поля
isSuccess,messageиvalidationError. Подключите нужную функцию в зависимости от плагина, и весь остальной код (отображение ошибок, подсветка полей, глобальное уведомление) будет работать без изменений.
Что делать, если форма не отправляется и сервер возвращает 404?
Проверьте три вещи: включён ли REST API плагина (особенно актуально для Gravity Forms), правильный ли идентификатор формы в URL эндпоинта, и не заблокирован ли REST API на уровне сервера или плагина безопасности. В Contact Form 7 также убедитесь, что WordPress REST API активен глобально, без него CF7 не сможет обработать AJAX-отправку.



