Интеграция

Примеры кода

Готовые примеры для HTML, React, Vue и Svelte с реальным API v3.

HTML + реальная интеграция (JS API v3)

Полный пример с проверкой согласия (152-ФЗ), inline error banner, сбросом капчи после успеха/ошибки.

index.html — форма
<form id="contact-form" novalidate>
  <input type="text" name="name" placeholder="Имя" required />
  <input type="email" name="email" placeholder="Email" required />
  <textarea name="message" placeholder="Сообщение"></textarea>

  <!-- Согласие на обработку ПД (152-ФЗ) -->
  <label>
    <input type="checkbox" id="consent" name="consent" required />
    Даю согласие на обработку персональных данных
  </label>

  <div id="captcha-container"></div>

  <!-- Inline error banner -->
  <div id="form-error" hidden>
    <span id="form-error-text"></span>
  </div>

  <button type="submit" disabled>Отправить</button>
</form>

<script src="https://app.form-hook.com/sdk/v3/loader.min.js" async defer></script>
index.html — скрипт интеграции
var widgetId = null;
var submitBtn = document.querySelector('#contact-form button[type="submit"]');
var consentEl = document.getElementById('consent');
var captchaSolved = false;

function updateSubmitState() {
  if (!submitBtn) return;
  submitBtn.disabled = !(captchaSolved && consentEl && consentEl.checked);
}

if (consentEl) {
  consentEl.addEventListener('change', updateSubmitState);
}

// Коды ошибок SDK v3
function userFriendlyError(err) {
  if (!err) return 'Произошла ошибка, попробуйте позже';
  switch (err.code) {
    case 'CAPTCHA_ANSWER_INVALID': return 'Капча не принята, попробуйте ещё раз.';
    case 'CAPTCHA_NOT_SOLVED':     return 'Пожалуйста, подтвердите, что вы не робот.';
    case 'SUBMIT_RATE_LIMITED':    return 'Слишком много попыток, подождите немного.';
    case 'SUBMIT_VALIDATION':      return 'Проверьте правильность заполненных полей.';
    case 'SUBMIT_NETWORK':         return 'Не удалось связаться с сервером.';
    case 'SUBMIT_IN_FLIGHT':       return 'Отправка уже идёт, подождите…';
    case 'WIDGET_LOAD_TIMEOUT':    return 'Капча не загрузилась, обновите страницу.';
    case 'CAPTCHA_SITE_DISABLED':  return 'Сайт временно отключён, попробуйте позже.';
    default: return 'Произошла ошибка, попробуйте позже';
  }
}

var errorBanner = document.getElementById('form-error');
var errorText = document.getElementById('form-error-text');

function showError(msg) { errorText.textContent = msg; errorBanner.hidden = false; }
function hideError()     { errorBanner.hidden = true; errorText.textContent = ''; }

// Инициализация SDK — используем formhook.ready(), не onloadFormhook
window.formhook.ready(function() {
  widgetId = window.formhook.render('#captcha-container', {
    sitekey: 'ВАШ_SITE_KEY',
    locale: 'ru',
    theme: 'light',
    callback: function() {
      captchaSolved = true;
      updateSubmitState();
    },
    'expired-callback': function() {
      captchaSolved = false;
      updateSubmitState();
    },
    'error-callback': function(err) {
      console.error('captcha error:', err.code, err.message);
      captchaSolved = false;
      updateSubmitState();
    },
  });
});

document.getElementById('contact-form').addEventListener('submit', async function(e) {
  e.preventDefault();
  hideError();

  if (!consentEl.checked) {
    showError('Пожалуйста, дайте согласие на обработку персональных данных.');
    return;
  }

  if (!widgetId || !window.formhook.isSolved(widgetId)) {
    showError('Пожалуйста, решите пример с капчей перед отправкой.');
    return;
  }

  var btn = this.querySelector('button[type="submit"]');
  var originalText = btn.textContent;
  btn.disabled = true;
  btn.textContent = 'Отправка...';

  try {
    // Удаляем consent из данных перед отправкой
    var formData = new FormData(this);
    formData.delete('consent');
    var data = Object.fromEntries(formData.entries());

    await window.formhook.submit(widgetId, data);

    this.reset();
    hideError();
    // Показать success (например, модалка)
    document.getElementById('success-modal').style.display = 'flex';

    // Сброс после успеха
    window.formhook.reset(widgetId);
    captchaSolved = false;
    updateSubmitState();
    btn.textContent = originalText;
  } catch (err) {
    console.error(err.code, err.message);
    showError(userFriendlyError(err));
    btn.textContent = originalText;

    // Сброс капчи при ошибке капчи
    var isCaptchaError = err.code === 'CAPTCHA_ANSWER_INVALID'
      || err.code === 'CAPTCHA_NOT_SOLVED';
    if (isCaptchaError) {
      window.formhook.reset(widgetId);
      captchaSolved = false;
    }
    updateSubmitState();
  }
});

HTML (декларативный режим — минимальный)

Если не нужна кастомная логика — loader делает всё за вас.

html
<form
  data-formhook
  data-sitekey="ВАШ_SITE_KEY"
  data-success-redirect="/thanks">

  <input name="name" placeholder="Имя" required />
  <input name="email" type="email" placeholder="Email" required />
  <div class="formhook-captcha"></div>
  <button type="submit">Отправить</button>
</form>

<script src="https://app.form-hook.com/sdk/v3/loader.min.js" async defer></script>

React

tsx
import { useState, useEffect, useRef } from 'react';

// Loader подключён в index.html или через next/script
declare global {
  interface Window {
    formhook: {
      ready: (cb: () => void) => void;
      render: (el: string | Element, opts: Record<string, unknown>) => string;
      isSolved: (id: string) => boolean;
      submit: (id: string, data: Record<string, string>) => Promise<{ id: string }>;
      reset: (id: string) => void;
      remove: (id: string) => void;
    };
  }
}

export function ContactForm() {
  const [form, setForm] = useState({ name: '', email: '', message: '' });
  const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');
  const [errorMsg, setErrorMsg] = useState('');
  const [solved, setSolved] = useState(false);
  const widgetId = useRef<string | null>(null);

  useEffect(() => {
    window.formhook.ready(() => {
      widgetId.current = window.formhook.render('#captcha', {
        sitekey: 'ВАШ_SITE_KEY',
        locale: 'ru',
        theme: 'auto',
        callback: () => setSolved(true),
        'expired-callback': () => setSolved(false),
        'error-callback': (e: { code: string }) => {
          console.error(e.code);
          setSolved(false);
        },
      });
    });
    return () => {
      if (widgetId.current) window.formhook.remove(widgetId.current);
    };
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!widgetId.current || !window.formhook.isSolved(widgetId.current)) {
      setErrorMsg('Решите капчу');
      return;
    }
    setStatus('sending');
    try {
      await window.formhook.submit(widgetId.current, form);
      setStatus('success');
      window.formhook.reset(widgetId.current);
      setSolved(false);
    } catch (err: any) {
      setErrorMsg(err.message);
      setStatus('error');
      if (err.code === 'CAPTCHA_ANSWER_INVALID' || err.code === 'CAPTCHA_NOT_SOLVED') {
        window.formhook.reset(widgetId.current);
        setSolved(false);
      }
    }
  };

  if (status === 'success') return <p>Заявка отправлена!</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))}
             placeholder="Имя" required />
      <input value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))}
             type="email" placeholder="Email" required />
      <textarea value={form.message} onChange={e => setForm(p => ({ ...p, message: e.target.value }))}
                placeholder="Сообщение" />
      <div id="captcha" />
      {status === 'error' && <p>{errorMsg}</p>}
      <button type="submit" disabled={!solved || status === 'sending'}>
        {status === 'sending' ? 'Отправка...' : 'Отправить'}
      </button>
    </form>
  );
}

Vue 3

vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const form = ref({ name: '', email: '', message: '' });
const status = ref<'idle' | 'sending' | 'success' | 'error'>('idle');
const errorMsg = ref('');
const solved = ref(false);
let widgetId: string | null = null;

onMounted(() => {
  (window as any).formhook.ready(() => {
    widgetId = (window as any).formhook.render('#captcha', {
      sitekey: 'ВАШ_SITE_KEY',
      locale: 'ru',
      theme: 'auto',
      callback: () => { solved.value = true; },
      'expired-callback': () => { solved.value = false; },
      'error-callback': (e: { code: string }) => { solved.value = false; },
    });
  });
});

onUnmounted(() => {
  if (widgetId) (window as any).formhook.remove(widgetId);
});

async function handleSubmit() {
  if (!widgetId || !(window as any).formhook.isSolved(widgetId)) {
    errorMsg.value = 'Решите капчу';
    return;
  }
  status.value = 'sending';
  try {
    await (window as any).formhook.submit(widgetId, form.value);
    status.value = 'success';
    (window as any).formhook.reset(widgetId);
    solved.value = false;
  } catch (err: any) {
    errorMsg.value = err.message;
    status.value = 'error';
    if (err.code === 'CAPTCHA_ANSWER_INVALID' || err.code === 'CAPTCHA_NOT_SOLVED') {
      (window as any).formhook.reset(widgetId);
      solved.value = false;
    }
  }
}
</script>

<template>
  <p v-if="status === 'success'">Заявка отправлена!</p>
  <form v-else @submit.prevent="handleSubmit">
    <input v-model="form.name" placeholder="Имя" required />
    <input v-model="form.email" type="email" placeholder="Email" required />
    <textarea v-model="form.message" placeholder="Сообщение" />
    <div id="captcha" />
    <p v-if="status === 'error'">{{ errorMsg }}</p>
    <button type="submit" :disabled="!solved || status === 'sending'">
      {{ status === 'sending' ? 'Отправка...' : 'Отправить' }}
    </button>
  </form>
</template>

Svelte

svelte
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';

  let form = { name: '', email: '', message: '' };
  let status: 'idle' | 'sending' | 'success' | 'error' = 'idle';
  let errorMsg = '';
  let solved = false;
  let widgetId: string | null = null;

  onMount(() => {
    (window as any).formhook.ready(() => {
      widgetId = (window as any).formhook.render('#captcha', {
        sitekey: 'ВАШ_SITE_KEY',
        locale: 'ru',
        theme: 'auto',
        callback: () => { solved = true; },
        'expired-callback': () => { solved = false; },
        'error-callback': () => { solved = false; },
      });
    });
  });

  onDestroy(() => {
    if (widgetId) (window as any).formhook.remove(widgetId);
  });

  async function handleSubmit() {
    if (!widgetId || !(window as any).formhook.isSolved(widgetId)) {
      errorMsg = 'Решите капчу'; return;
    }
    status = 'sending';
    try {
      await (window as any).formhook.submit(widgetId, form);
      status = 'success';
      (window as any).formhook.reset(widgetId);
      solved = false;
    } catch (err: any) {
      errorMsg = err.message;
      status = 'error';
      if (err.code === 'CAPTCHA_ANSWER_INVALID' || err.code === 'CAPTCHA_NOT_SOLVED') {
        (window as any).formhook.reset(widgetId);
        solved = false;
      }
    }
  }
</script>

{#if status === 'success'}
  <p>Заявка отправлена!</p>
{:else}
  <form on:submit|preventDefault={handleSubmit}>
    <input bind:value={form.name} placeholder="Имя" required />
    <input bind:value={form.email} type="email" placeholder="Email" required />
    <textarea bind:value={form.message} placeholder="Сообщение" />
    <div id="captcha" />
    {#if status === 'error'}<p>{errorMsg}</p>{/if}
    <button type="submit" disabled={!solved || status === 'sending'}>
      {status === 'sending' ? 'Отправка...' : 'Отправить'}
    </button>
  </form>
{/if}