Skip to content

Wszystko o WordPressie, tworzeniu stron — i nie tylko

📤 Wysyłanie formularzy w headless WordPress: REST API Contact Form 7 i Gravity Forms

📤 Wysyłanie formularzy w headless WordPress: REST API Contact Form 7 i Gravity Forms

Budują Państwo stronę na WordPress i formularz kontaktowy to dla Państwa sprawa załatwiona. Wtyczki takie jak Contact Form 7 dają gotowy HTML, walidację, zapisywanie zgłoszeń i dziesiątki integracji. Kliknięcie „Zainstaluj" i wstawienie shortcode’a to kwestia dwóch minut.

Wszystko zmienia się jednak, gdy WordPress staje się headless CMS. Odpowiadają Państwo za frontend w całości: React, Vue, czysty HTML/JS. A wtyczka formularzy, która wcześniej renderowała znaczniki za Państwa, nie kontroluje już części klienckiej. Jej REST API jednak nigdzie nie zniknęło. Wystarczy wysłać POST na właściwy endpoint, a cała moc wtyczki (sprawdzanie pól, zapisywanie, integracje) pozostaje w Państwa rękach.

W praktyce przez REST API wtyczek formularzy można obsłużyć również czysto „tradycyjne" przypadki. Załóżmy, że kodują Państwo niestandardowy motyw na Tailwind i sztywny układ CF7 z jego niezmienną strukturą klas razi w oczy. Wysyłanie przez API pozwala kontrolować każdy piksel formularza bez rezygnacji z wypracowanego ekosystemu wtyczki.

💡 Szybki przegląd:

  • Jakie endpointy udostępniają Contact Form 7 i Gravity Forms oraz jak je aktywować w środowisku headless.
  • W jakim formacie wysyłać pola, aby wtyczka poprawnie przyjęła dane i zwróciła wynik.
  • Jak zbudować formularz HTML, podpiąć zapytanie fetch i pokazać użytkownikowi komunikat sukcesu lub błędy walidacji.
  • Jak ujednolicić rozbieżne formaty odpowiedzi z CF7 i Gravity Forms w jedną wygodną strukturę.

Co trzeba wiedzieć o endpointach

Wysyłanie danych przez REST API to technicznie prosta część. Obie wtyczki oczekują POST na endpoint, gdzie dynamicznym segmentem URL jest identyfikator konkretnego formularza.

Contact Form 7 udostępnia REST API od razu po aktywacji. Endpoint wygląda następująco:

1https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback

Od wersji 5.8 (sierpień 2023) Contact Form 7 przeszedł na haszowane identyfikatory formularzy SHA-1. Stare numeryczne ID nadal działają, ale dla nowych formularzy identyfikator należy pobrać z URL strony edycji formularza w panelu administracyjnym, jest to ostatni segment po post=. Według stanu na kwiecień 2026 roku wtyczka ma ponad 10 milionów aktywnych instalacji i jest przetestowana aż do WordPress 7.0.

Gravity Forms używa REST API v2 (dostępnego od wersji 2.4):

1https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions

Ważny niuans: REST API Gravity Forms jest domyślnie wyłączone. Aby je aktywować, należy wejść w ustawienia wtyczki → zakładka REST API → zaznaczyć „Enable access to the API". Klucz API dla endpointu wysyłania formularzy nie jest potrzebny, jest on z założenia publiczny. Identyfikator formularza w Gravity Forms jest numeryczny, widać go w panelu administracyjnym podczas edycji.

Struktura treści żądania

Weźmy dla przykładu formularz z pięcioma polami: obowiązkowe tekst, email i data (do 4 października 1957 roku), opcjonalny textarea oraz obowiązkowy checkbox.

Przykład formularza zwrotnego z pięcioma polami wejściowymi

Contact Form 7 oczekuje kluczy w formacie określonym przez składnię tagów formularza. Klucz odpowiada atrybutowi name danego pola w 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 stosuje inne podejście: automatycznie generowane inkrementacyjne identyfikatory z prefiksem input_. ID pola jest widoczne bezpośrednio w panelu administracyjnym podczas edycji konkretnego pola.

Edycja pola Gravity Forms z widocznym identyfikatorem input_3

Dla tego samego formularza treść żądania do Gravity Forms wygląda inaczej:

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}

Kluczowy wniosek: jeśli nadadzą Państwo polom HTML atrybuty name zgodne z oczekiwanymi kluczami wtyczki, mapowanie odbywa się automatycznie, FormData zbierze dane w wymaganym formacie bez ręcznego mapowania.

Zbieramy HTML i wysyłamy żądanie

Dla Contact Form 7 znaczniki HTML będą następujące (proszę zwrócić uwagę na action, to endpoint, a name pól jest zgodne z kluczami powyżej):

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>

Dla Gravity Forms zmieniają się tylko action i atrybuty 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>

Teraz wysyłka przez JavaScript, FormData sam zbiera wartości według name, niczego nie trzeba mapować:

1const 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
25const formElement = document.querySelector("form");
26formElement.addEventListener("submit", formSubmissionHandler);

Dane zostały wysłane. Użytkownikowi to jednak nie wystarcza, potrzebuje on informacji zwrotnej: komunikatu o sukcesie, podświetlenia pól z błędami, globalnego powiadomienia. Na szczęście obie wtyczki zwracają te informacje w odpowiedzi.

Walidacja: serwer decyduje, klient pokazuje

Oprócz wbudowanej walidacji HTML5 (atrybuty required, type="email" i max) rozsądnie jest polegać na serwerowym sprawdzaniu reguł, które zapewniają wtyczki. Dlaczego: reguły konfiguruje się w panelu administracyjnym WordPress centralnie, a ich powielanie po stronie klienta to podwójna praca i źródło rozbieżności.

Zarówno Contact Form 7, jak i Gravity Forms zwracają błędy walidacji bezpośrednio w treści odpowiedzi. W przypadku złożonych scenariuszy, pól warunkowych, walidacji zależnej oparcie się na sprawdzaniu serwerowym jest szczególnie korzystne: nie trzeba synchronizować logiki między frontendem a ustawieniami wtyczki.

Zadanie sprowadza się do trzech kroków: sparsować odpowiedź JSON, wyodrębnić komunikaty o błędach i wstawić je do DOM obok odpowiednich pól.

Formaty odpowiedzi i ich normalizacja

Odpowiedź Contact Form 7 w przypadku błędu walidacji:

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}

W przypadku sukcesu odpowiedź jest bardziej zwięzła:

1{
2 "into": "#",
3 "status": "mail_sent",
4 "message": "Thank you for your message. It has been sent.",
5 "posted_data_hash": "d52f9f9de995287195409fe6dcde0c50"
6}

Odpowiedź Gravity Forms w przypadku błędu walidacji jest skonstruowana inaczej:

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}

Natomiast odpowiedź udana zawiera potwierdzenie wewnątrz 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}

Różnica w podejściu jest oczywista: CF7 umieszcza błędy w tablicy obiektów z selektorami CSS, a Gravity Forms w płaskim obiekcie z kluczami numerycznymi bez prefiksu input_. Komunikat o sukcesie z Gravity Forms przychodzi w opakowaniu HTML. Klucze pól w odpowiedziach CF7 są „zaszyte" w selektorach (na przykład span.wpcf7-form-control-wrap.somebodys-name) i wymagają wyciągnięcia wyrażeniem regularnym.

Zamiast rozgałęziać logikę pod każdą wtyczkę, wygodniej jest sprowadzić obie odpowiedzi do jednego formatu:

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}

W przypadku sukcesu isSuccess ustawiane jest na true, a validationError przychodzi jako pusty obiekt.

Kod normalizacji dla Contact Form 7:

1const 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};

Kod normalizacji dla Gravity Forms (proszę zwrócić uwagę: kluczom błędów dodawany jest prefiks input_, aby pokrywały się z kluczami zapytania):

1const 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};

Mają już Państwo jednolity obiekt odpowiedzi niezależnie od wtyczki. Pozostaje dopisać wyświetlanie błędów i przełączanie klas na elementach DOM i formularz jest gotowy do działania.

Od normalizacji do żywego interfejsu

Gdy odpowiedź zostanie sprowadzona do jednolitej struktury, wyświetlanie informacji zwrotnej sprowadza się do manipulacji DOM. Dodać komunikat o błędzie obok pola, przełączyć klasę w opakowaniu i pokazać globalne powiadomienie: te trzy czynności wystarczają w zdecydowanej większości scenariuszy.

Do reaktywnej aktualizacji interfejsu wygodnie jest używać lekkich bibliotek deklaratywnych, takich jak Alpine.js. Minimalna składnia, brak kroku budowania i naturalna integracja z odpowiedzią serwera czynią go praktycznym wyborem dla formularzy w środowisku headless. Podejście z Alpine.js było szczegółowo omawiane na CSS-Tricks, kod z tego materiału praktycznie bez zmian pasuje do znormalizowanej odpowiedzi, którą otrzymaliśmy powyżej.

Podsumowując

Odtworzenie części klienckiej, którą wtyczki formularzy dają „od ręki", dla prostych formularzy to zadanie na kilka godzin. Przyjemny bonus: abstrahując odpowiedź przez funkcję normalizującą, zyskują Państwo wymienny backend. Przejście z Contact Form 7 na Gravity Forms (lub odwrotnie) jest możliwe bez poprawek we frontendzie, wystarczy podmienić endpoint i funkcję normalizacji.

Formularze wielostronicowe, podgląd przesłanych obrazów, kalkulatory kosztów, tak, to już poważne programowanie. Ale im bardziej unikalne są wymagania projektu, tym silniejszy jest argument za własnym frontendem na bazie REST API: nie walczą Państwo z cudzym znacznikiem i nie obchodzą ograniczeń gotowego renderowania.

Podejście headless do formularzy nie jest hipotetyczną przyszłością. Już dziś wtyczki takie jak Contact Form 7 i Gravity Forms oferują pełnoprawne REST API, a frameworki frontendowe pozwalają zbudować formularz w godziny, nie dni. Proszę spróbować przy najbliższym projekcie, gdzie wygląd formularza jest krytyczny: wziąć CF7 lub GF jako backend i zbudować interfejs od zera. Najprawdopodobniej będą Państwo zaskoczeni, jak bardzo jest to prostolinijne.

⁉️🤔 Często zadawane pytania

Czy REST API Contact Form 7 działa w wersji bezpłatnej?

Tak, REST API jest dostępny natychmiast po aktywacji bezpłatnej wtyczki, nie są wymagane żadne dodatkowe ustawienia. Według stanu na kwiecień 2026 roku Contact Form 7 ma ponad 10 milionów aktywnych instalacji, a REST API pozostaje stabilną częścią rdzenia wtyczki począwszy od wersji 4.8.

Czym różnią się haszowane identyfikatory formularzy w nowych wersjach Contact Form 7?

Począwszy od wersji 5.8 (sierpień 2023), CF7 generuje hash SHA-1 jako identyfikator formularza zamiast identyfikatora numerycznego. Stare identyfikatory numeryczne nadal działają. Hash można znaleźć w adresie URL strony edycji formularza w panelu administracyjnym: /wp-admin/admin.php?page=wpcf7&post=<HASH>&action=edit. W endpointcie podaje się go tak samo jak identyfikator numeryczny.

Czy do wysyłania formularzy przez Gravity Forms REST API potrzebny jest klucz API?

Nie, endpoint /gf/v2/forms/<ID>/submissions nie wymaga uwierzytelnienia do wysyłania. Jednak samo REST API Gravity Forms jest domyślnie wyłączone, należy je włączyć w ustawieniach wtyczki (Forms → Settings → REST API → Enable access to the API).

Czy można używać tego samego kodu JavaScript dla Contact Form 7 i Gravity Forms?

Tak, właśnie do tego służy normalizacja odpowiedzi. Obie funkcje normalizujące (dla CF7 i GF) zwracają obiekt o identycznej strukturze: pola isSuccess, message i validationError. Należy podłączyć odpowiednią funkcję w zależności od wtyczki, a cały pozostały kod (wyświetlanie błędów, podświetlanie pól, globalne powiadomienie) będzie działać bez zmian.

Co zrobić, jeśli formularz nie jest wysyłany, a serwer zwraca błąd 404?

Proszę sprawdzić trzy rzeczy: czy REST API wtyczki jest włączone (szczególnie istotne w przypadku Gravity Forms), czy identyfikator formularza w adresie URL endpointu jest prawidłowy oraz czy REST API nie jest zablokowane na poziomie serwera lub wtyczki bezpieczeństwa. W przypadku Contact Form 7 należy również upewnić się, że WordPress REST API jest aktywne globalnie, bez niego CF7 nie będzie w stanie obsłużyć wysyłania AJAX.