API LeadForms для разработчиков

Заявки на ваш сервер, в CRM или таблицу: лента, подписки, подпись запросов
Что умеет API
Заявки. Забирать новые заявки по порядку или получать каждую сразу на свой адрес (подписка). Так работают наши приложения для Zapier, Make и n8n.
Чтение кабинета. Квизы, страницы, виджеты, проекты и аналитика доступны по ключу на чтение.
Изменения квизов, страниц и виджетов по ключу делаются только через MCP-сервер: его инструменты меняют ровно то, что нужно, и не затирают остальное. Как подключить ассистента: LeadForms в ИИ-агенте.
Адрес и ключ доступа
Адрес API: https://api.leadforms.ru (для аккаунтов leadforms.org: https://api.leadforms.org, ключ подходит к обоим).
Ключ создаётся в личном кабинете: меню → «ИИ-агенты и API». Он выглядит как lfk_… и показывается один раз, сохраните его сразу. Передавайте ключ в каждом запросе заголовком:
Authorization: Bearer ВАШ_КЛЮЧ
Проверить ключ: GET /v1/me вернёт {id, email, name, brand, locale}.
curl -H "Authorization: Bearer ВАШ_КЛЮЧ" "https://api.leadforms.ru/v1/me"
Назначение ключа = его права
Назначение в кабинетеЧто открывает
ИИ-агент (MCP)MCP-сервер (создание и правка квизов, страниц, виджетов, работа с заявками), чтение кабинета, лента заявок
Zapier / Make / n8nТолько заявки: лента, пример заявки, список квизов, подписки
Своя интеграцияВсё перечисленное: MCP, чтение кабинета, лента и подписки
По ключу недоступны никому: удаление квизов и заявок, оплата, профиль, передача квизов в другой аккаунт и сами ключи. Это делается только в кабинете.
Если ключ мог попасть к посторонним, нажмите «Отозвать» рядом с ним в кабинете: он перестанет работать сразу, а подписки, созданные этим ключом, отключатся.
Лента новых заявок
GET /v1/leads?since_id=0&limit=50 отдаёт заявки по порядку поступления после заявки с номером since_id. Первый раз передайте since_id=0, дальше номер последней полученной.
curl -H "Authorization: Bearer ВАШ_КЛЮЧ" "https://api.leadforms.ru/v1/leads?since_id=0&limit=10"
ПараметрОписание
since_idНомер последней полученной заявки, 0 для начала с самой первой
limitСколько заявок вернуть, от 1 до 100, по умолчанию 50
quiz_idsТолько заявки этих квизов, страниц или виджетов: 6903,7012. Без параметра все
orderasc (по умолчанию) или desc: сначала новые, для «последних N»
Ответ: {"data": [заявки…], "meta": {"since_id", "next_since_id", "has_more", "limit"}}. Следующий запрос делайте с since_id = meta.next_since_id, пока has_more равно true.
Номера заявок не идут подряд, но порядок ленты держит сервер: ни одна заявка не пропадёт между запросами.
GET /v1/leads/sample вернёт последнюю заявку или пример того же вида, если заявок ещё нет (удобно для настройки сценария).
GET /v1/leads/sources вернёт квизы, страницы и виджеты аккаунта: {"data": [{"id", "name", "type", "published"}]}.
Подписка: каждая заявка сразу на ваш адрес
Подпишите свой адрес, и LeadForms будет отправлять на него каждую новую заявку запросом POST.
curl -X POST -H "Authorization: Bearer ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
  -d '{"target_url":"https://example.com/leadforms","quiz_ids":[6903]}' \
  "https://api.leadforms.ru/v1/hooks"
ПолеОписание
target_urlОбязательно. Адрес http или https, доступный из интернета
quiz_idsТолько эти квизы, страницы или виджеты. Без поля все объекты аккаунта
sourceapi (по умолчанию), zapier, make или n8n: подпись в кабинете
Ответ содержит id подписки и secret вида whsec_… для проверки подписи. Секрет отдаётся только здесь, сохраните его. Повторная подписка того же адреса обновит настройки.
GET /v1/hooks: список подписок. DELETE /v1/hooks/{id}: отписаться. Подписка снимается и сама, если ваш адрес ответит 410 Gone, или если отозвать ключ, которым она создана. Не больше 100 подписок на аккаунт.
Как приходит заявка на ваш адрес
Запрос POST с телом JSON (формат ниже) и заголовками:
ЗаголовокЗначение
X-Leadforms-Eventlead.created
X-Leadforms-DeliveryНомер доставки. При повторе тот же: по нему отсеивайте дубли
X-Leadforms-AttemptНомер попытки, от 1 до 6
X-Leadforms-Signaturet=<время>,v1=<подпись>
Ваш ответЧто делает LeadForms
2xxЗаявка доставлена
408, 429, 5xx, нет ответа за 15 секунд, сетевая ошибкаПовтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов
410 GoneПодписка снимается
Другие 4xxОшибка без повтора, видна в кабинете
Отвечайте быстро, а тяжёлую обработку делайте после ответа. Если заявка не дошла, владелец аккаунта увидит это в кабинете.
Проверка подписи
Подпись v1 = HMAC-SHA256 от строки <t>.<тело запроса как пришло> с секретом подписки, в шестнадцатеричном виде. Отклоняйте запросы с неверной подписью и с t старше 5 минут. Если в заголовке несколько v1=, подходит любая.
PHP
function leadformsVerify(string $secret, string $body, string $header, int $tolerance = 300): bool
{
    $t = null; $sigs = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($k === 't') { $t = (int) $v; } elseif ($k === 'v1') { $sigs[] = $v; }
    }
    if ($t === null || abs(time() - $t) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha256', $t . '.' . $body, $secret);
    foreach ($sigs as $sig) {
        if (hash_equals($expected, $sig)) {
            return true;
        }
    }
    return false;
}

$body = file_get_contents('php://input');
if (!leadformsVerify('whsec_…', $body, $_SERVER['HTTP_X_LEADFORMS_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
$lead = json_decode($body, true);
Node.js (тело нужно сырым, до разбора JSON)
const crypto = require('crypto');

function leadformsVerify(secret, rawBody, header, tolerance = 300) {
  let t = null; const sigs = [];
  for (const part of String(header || '').split(',')) {
    const [k, v] = part.trim().split('=');
    if (k === 't') t = Number(v); else if (k === 'v1') sigs.push(v);
  }
  if (!t || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

app.post('/leadforms', express.raw({ type: 'application/json' }), (req, res) => {
  if (!leadformsVerify(process.env.LEADFORMS_SECRET, req.body.toString('utf8'), req.get('X-Leadforms-Signature'))) {
    return res.sendStatus(401);
  }
  const lead = JSON.parse(req.body);
  res.sendStatus(200);
});
Python
import hashlib, hmac, time

def leadforms_verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    t, sigs = None, []
    for part in (header or '').split(','):
        k, _, v = part.strip().partition('=')
        if k == 't' and v.isdigit():
            t = int(v)
        elif k == 'v1':
            sigs.append(v)
    if t is None or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f'{t}.'.encode() + body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)
Формат заявки
Один и тот же в подписке, ленте, примере и вебхуках квиза.
{
  "id": 241166955,
  "quizId": 6903,
  "answers": [
    {"q": "Какая услуга вас интересует?", "questionId": 34092, "a": {"105575": "Консультация"}}
  ],
  "contacts": {"name": "Иван", "phone": "+79991234567", "email": "ivan@example.com"},
  "created": "21.10.2026 12:00",
  "quizDurationSeconds": 95,
  "result": null,
  "extra": {
    "href": "https://site.ru/",
    "ref": "https://yandex.ru/",
    "utm": {"utm_source": "yandex"},
    "cookies": {},
    "device": "mobile",
    "client": {"os": "iOS", "browser": "Safari"}
  },
  "version": 1
}
ПолеОписание
idНомер заявки, как в кабинете
quizIdНомер квиза, страницы или виджета
answersОтветы: q текст вопроса, questionId номер вопроса, a выбранное: ключ это номер варианта (или custom для своего ответа), значение текст
contactsКонтакты из формы: имя, телефон, почта и другие поля, которые есть в форме
createdДата заявки, дд.мм.гггг чч:мм
quizDurationSecondsСколько секунд человек проходил квиз
resultРезультат квиза, если он есть, иначе null
extraСтраница заявки, откуда пришёл, UTM-метки, устройство
booking, paymentТолько у заявок с записью на встречу или с оплатой
versionВерсия формата. Поля выше не меняются; несовместимые изменения выйдут только новой версией. В extra могут появляться новые ключи
Отправляются только оплаченные заявки. Тестовые заявки владельца аккаунта тоже приходят: так удобно проверить сценарий.
Ошибки и лимиты
Ошибка приходит в виде {"error": {"code": "…", "message": "…", "statusCode": 422}}, текст на языке кабинета. Лимит: 240 запросов в минуту на ключ.
КодЧто значит
401 INVALID_API_KEYКлюч не найден или отозван
403 API_KEY_SCOPEУ ключа нет нужного права: создайте ключ с подходящим назначением
403 API_KEY_USE_MCPКвизы, страницы и виджеты по ключу меняются только через MCP
403 API_KEY_FORBIDDENДействие доступно только в кабинете
422 INVALID_CURSORТакой заявки нет в аккаунте: начните с since_id=0
422 UNKNOWN_QUIZВ quiz_ids чужой или несуществующий номер
422 LIMITПодписок на аккаунт уже 100
429 RATE_LIMITСлишком много запросов: подождите столько секунд, сколько в заголовке Retry-After
Готовые подключения
Если писать код не нужно: в кабинете в разделе «ИИ-агенты и API» есть приложения для Zapier, Make и n8n, они уже умеют получать заявки. Для ИИ-ассистентов (Claude, ChatGPT, Cursor) подключите MCP-сервер. Простая отправка заявок одного квиза на ваш адрес: вебхуки квиза.
Не получается настроить?
Если что-то не получается, вы можете , поможем разобраться.

Остались вопросы? Напишите нам

Имя *
* Обязательно к заполнению
e-mail *
* Обязательно к заполнению
Сообщение
Напишите нам!
Ответим в течение 30 секунд