Подготавливаем страницу…
Подготавливаем страницу…
Готовый файл-прокси на PHP: один API-ключ обслуживает все чекбоксы согласия в форме, и на обработку ПДн, и на маркетинг. Тот же production-паттерн работает на 1opd.ru и у клиентов платформы. Время подключения около 30 минут.
Напрямую в произвольный API Tilda данные не отправляет, зато умеет звать Webhook по указанному URL. Отсюда схема из четырёх шагов:
Пользователь → Tilda-форма → Tilda Webhook → ВАШ proxy.php → 1ОПД (галочка (forms_scope[] (POST согласия) в форме) /api/v2/ create- agreement)
Один proxy.php на сервере клиента смотрит, какие чекбоксы субъект отметил в форме, и шлёт в 1ОПД столько POST-запросов, сколько чекбоксов активно: обычно один на согласие на обработку ПДн и, если он есть, второй на маркетинг. Все чекбоксы сайта при этом обслуживает один API-ключ 1ОПД, такой ключ называют multi-form (см. Шаг 02).
sk_ (secret, только server-side) forms_scope=[id1, id2, …] , и в каждом запросе передаёте form_id в body.Ключ выпускается в кабинете 1ОПД и показывается один раз. Все endpoints → API Reference
Прочитайте один раз: дальше в инструкции эти слова уже не будут пугать.
Здесь лежат формы согласия и их form_id.
Зайдите на lk.v2.1opd.ru под своей учётной записью. Если кабинета ещё нет — заведите его сами: вход открывает боевой доступ сразу, приём согласий работает с первого дня.
form_id и API-ключ — вы создаёте в кабинете сами. Обращаться к менеджеру для этого не нужно.Кнопка в карточке сайта. Полный ключ показывается один раз.
Ключ выпускается в кабинете, в разделе «Интеграция»: карточка вашего сайта → «Выпустить ключ для этой формы». Один ключ может писать сразу в несколько форм согласия — например, одновременно в «обработку ПДн» и в «маркетинг»: за это отвечает forms_scope, отдельный ключ на каждую форму выпускать не нужно.
form_id=1 · Согласие на обработку ПДн sk_): он лежит на вашем сервере и посетителю не виден. Для варианта без своего сервера (JS в HEAD, в конце страницы) — publishable (pk_): он по замыслу виден в исходниках, а защищает его привязка к домену.form_id ваших форм — они видны там же, в карточке сайта, строкой form_id=1 · название формы. Они понадобятся в Шаге 04. sk_abc1234567... и список form_id (например: 1 = обработка, 2 = маркетинг).В ЛК → раздел «Интеграция» → Формы → form_id.
В ЛК 1ОПД откройте раздел «Интеграция» в левой панели и найдите список форм: в названии каждой стоит её form_id=N. Это и есть номер, который вы передаёте в поле form_id в body POST-запроса.
form_id какой цели обработки соответствует. Например: 1 для основного согласия на обработку ПДн, 2 для маркетинга.sk_… и таблица «чекбокс → form_id». Можно собирать proxy.php.Один файл, приёмник Tilda Webhook. Правятся только переменные сверху.
Скопируйте код ниже целиком, отредактируйте 4 блока «НАСТРОЙКИ» (API-ключ, чекбоксы→form_id, маппинг полей, поле-идентификатор) и загрузите файл на веб-сервер по адресу вида https://mydomain.ru/opd/proxy.php.
opd/ в корне сайта. Положите туда файл proxy.php с кодом ниже.opd/ права 755. <?php
// proxy.php — приёмник Tilda Webhook для отправки согласий в 1ОПД (152-ФЗ).
// Положите этот файл на ваш веб-сервер (PHP 7.4+) и пропишите URL
// в Tilda → Настройки сайта → Формы → Сервис «Webhook».
//
// ОДИН файл обслуживает ВСЕ чекбоксы согласия в форме Tilda через ОДИН
// API-ключ. Главное — чтобы у ключа в 1ОПД в forms_scope были все нужные
// form_id (это выбирается при выпуске ключа в кабинете).
// ── НАСТРОЙКИ — отредактируйте эти 4 блока ───────────────────────────────
// 1. API-ключ 1ОПД. Выпускается в кабинете и показывается ОДИН раз — потом
// в ЛК виден только префикс.
$api_key = 'sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// 2. Endpoint 1ОПД (можно оставить как есть).
$endpoint = 'https://app.1opd.ru/api/v2/create-agreement';
// 3. Какой чекбокс Tilda какой form_id 1ОПД соответствует.
// Имена слева ('Checkbox', 'Checkbox_2') — это поле name=... у чекбокса
// в форме Tilda (см. Шаг 06). Числа справа — form_id из ЛК 1ОПД (см.
// Шаг 03). Если у вас один чекбокс — оставьте только первую строку.
$consent_form_ids = [
'Checkbox' => 1, // основной чекбокс «Согласен с обработкой ПДн»
'Checkbox_2' => 2, // дополнительный «Маркетинговая рассылка» (опц)
];
// 4. Маппинг полей Tilda → имена в 1ОПД (как заведены в «Реестре процессов»).
$field_mapping = [
'Name' => 'first_name',
'Email' => 'email',
'Phone' => 'phone_number',
// 'Company' => 'company_name', // раскомментируйте при необходимости
];
// Какое поле Tilda — главный идентификатор субъекта (для дедупа в 1ОПД).
$hash_field = 'Email';
// ── ЛОГИКА (обычно править не нужно) ─────────────────────────────────────
$tilda_data = $_POST;
if (empty($tilda_data)) {
$tilda_data = json_decode(file_get_contents('php://input'), true) ?? [];
}
$log_file = __DIR__ . '/opd-debug.log';
function opd_log(string $line): void {
global $log_file;
file_put_contents($log_file, date('Y-m-d H:i:s') . ' ' . $line . "\n", FILE_APPEND);
}
$formatted_fields = [];
foreach ($tilda_data as $key => $value) {
if (!isset($field_mapping[$key])) continue;
if (is_array($value)) $value = implode(', ', $value);
$formatted_fields[] = ['field' => $field_mapping[$key], 'value' => (string)$value];
}
$hash_value = (string)($tilda_data[$hash_field] ?? '');
if ($hash_value === '') {
opd_log("SKIP: пустой hash_field ({$hash_field}) — субъект не идентифицируется.");
http_response_code(200);
exit;
}
$sent_count = 0;
foreach ($consent_form_ids as $checkbox_name => $form_id) {
if (($tilda_data[$checkbox_name] ?? '') !== 'yes') {
opd_log("SKIP form_id={$form_id} ({$checkbox_name}): чекбокс не активен.");
continue;
}
$payload = [
'form_id' => $form_id,
'hash_field' => $hash_value,
'fields' => $formatted_fields,
];
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\nAPI-KEY: {$api_key}\r\n",
'content' => json_encode($payload, JSON_UNESCAPED_UNICODE),
'ignore_errors' => true,
'timeout' => 10,
],
]);
$response = @file_get_contents($endpoint, false, $ctx);
$code = 'no-response';
if (isset($http_response_header)) {
foreach ($http_response_header as $h) {
if (strpos($h, 'HTTP/') === 0) { $code = $h; break; }
}
}
opd_log("SEND form_id={$form_id} ({$checkbox_name}) -> {$code} | response: {$response}");
$sent_count++;
}
opd_log("DONE: отправлено {$sent_count} согласий для {$hash_field}={$hash_value}.");
http_response_code(200);
echo json_encode(['ok' => true, 'sent' => $sent_count]);
$consent_form_ids первую строку и выпустите ключ с forms_scope=[1]. Остальное в файле менять не нужно.proxy.php открывается по адресу https://mydomain.ru/opd/proxy.php и в браузере отдаёт JSON {"ok":true,"sent":0}: без данных пересылать нечего.Tilda → Настройки сайта → Формы → «Добавить сервис» → «Webhook».
В Tilda откройте сайт → «Настройки сайта» (вверху справа) → «Формы» → блок «Подключение к сервисам приёма данных» → «Webhook».
proxy.php: полный URL с https://.Обязательный чекбокс «Согласие на обработку ПДн» и опциональный «Маркетинг».
В Tilda откройте блок с формой → «Контент» → раздел «Поля». Добавьте поле типа «Checkbox», имя ему Tilda присвоит сама: Checkbox. В текст поля впишите формулировку согласия. Она обязана точно совпадать с формулировкой в PDF-документе, загруженном в 1ОПД.
① Основной чекбокс: согласие на обработку ПДн (обязательный)
Checkbox (имя по умолчанию) «Даю согласие на обработку персональных данных в соответствии с Политикой обработки персональных данных».
Required = Да. Без отметки форма не отправляется: согласие субъект даёт своей волей и в своём интересе (ст. 9 ч. 1 152-ФЗ).
② Опциональный чекбокс: маркетинговая рассылка (добровольный)
Checkbox_2 (Tilda сама нумерует второй чекбокс) «Даю согласие на получение информационной и рекламной рассылки».
Required = Нет. Согласие на рассылку субъект даёт отдельно от согласия на обработку.
proxy.php обязаны совпадать буква в букву, включая регистр. Поле СогласиеПДн в Tilda означает тот же СогласиеПДн в массиве $consent_form_ids.Checkbox и Checkbox_2 либо свои, тогда поправьте PHP.required, маркетинговый нет.Заполните форму тестовым email. Сверьте debug.log и ЛК 1ОПД.
Откройте опубликованную страницу с формой. Заполните Name / Email / Phone, отметьте оба чекбокса и нажмите «Отправить».
opd/opd-debug.log. В нём должны быть строки:2026-05-21 13:42:01 SEND form_id=1 (Checkbox) -> HTTP/1.1 200 OK | response: {"hash":"..."}
2026-05-21 13:42:01 SEND form_id=2 (Checkbox_2) -> HTTP/1.1 200 OK | response: {"hash":"..."}
2026-05-21 13:42:01 DONE: отправлено 2 согласий для Email=test@example.ru. test@example.ru.debug.log есть строка «SEND form_id=… → 200 OK» либо «→ 202 Accepted»: оба кода означают приём.Самый частый вопрос при подключении: как одним ключом обслуживать и обработку ПДн, и маркетинг, и обратную связь.
Один ключ на одну форму согласия. Форма с двумя чекбоксами требовала двух ключей и двух PHP-файлов либо усложнённого JS.
Один ключ с forms_scope=[1, 2, 5] закрывает несколько форм. В каждом POST-запросе вы указываете form_id явно, и бэкенд направляет запись в нужную форму. PHP-файл при этом один на весь сайт.
Сценарий 1: одно согласие закрывает несколько форм сбора. На сайте стоят «Заказать звонок», «Обратная связь» и ещё пара форм, и цель обработки у всех одна (например, обработка обращений субъектов). Привязка идёт к согласию, а не к кнопке. Достаточно одного multi-form ключа со scope этого согласия, а в каждом запросе вы проставляете один и тот же form_id.
Сценарий 2: в одной форме два чекбокса под разные согласия. Обработка ПДн и маркетинг составляют разные цели обработки. Один multi-form ключ с forms_scope=[1, 2] покрывает обе формы: в каждом запросе вы передаёте свой form_id (1 для обработки, 2 для маркетинга), и бэкенд по scope направляет запись в нужный реестр.
Зачем тогда «выбранные формы» (per-form scope)? Для изоляции отдельных точек сбора, когда один ключ не должен писать сразу в несколько форм. Например, лендинг партнёрской кампании получает свой ключ с forms_scope=[5], и его утечка не затронет данные основной формы. Передавать form_id в body всё равно обязательно: бэкенд проверит вхождение в scope ключа и при несовпадении вернёт 403. Отказ виден в журнале audit_log в ЛК клиента.
form_id.proxy.php добавляете строку в $consent_form_ids, например 'Checkbox_3' => 5.Checkbox_3 и нужным текстом.Когда нужен второй ключ: когда в одном кабинете 1ОПД ведутся разные юридические лица. Один ключ принадлежит одному юрлицу. Для холдинга с формами разных компаний на одном сайте выпустите по ключу на каждое юрлицо.
Когда поставить PHP-файл на сервер негде, согласие можно отправлять прямо из браузера. Тот же код выдаёт кабинет 1ОПД — уже с вашим form_id, именами полей формы и id блока, так что править в нём нужно только ключ. Ниже — тот же код с плейсхолдерами: он показывает, как всё устроено.
Что важно знать про этот путь:
allowed_origins в карточке ключа. Ключ идёт в теле запроса, а не в адресе — иначе он попадёт в журналы сервера, историю браузера и веб-аналитику сайта.Content-Type: text/plain и ни одного своего заголовка. Стоит вынести ключ в заголовок API-KEY — браузер добавит preflight OPTIONS, и при малейшем расхождении CORS отменит сам POST молча, без ошибки в консоли.submit Tilda не порождает: форму отправляет её собственный обработчик. Код ловит факт успешной отправки — сетевой запрос Tilda к форм-процессору и появление блока .t-form__successbox. Подписка на submit не сработает ни разу, и заметить это нельзя: форма работает, заявка приходит, согласий нет.Email против email). Одно несопоставленное имя отбивает весь запрос с кодом 400 — поэтому код сопоставляет имена сам, а служебные поля Tilda (tildaspec…) и поля с кириллицей в названии не отправляет вовсе.debug.log здесь тоже нет — диагностика идёт через консоль браузера.Куда вставлять — одно из двух. На одной странице: Настройки страницы → «Дополнительно» → «HTML-код для вставки внутрь HEAD». На всех страницах: Настройки сайта → «Вставка кода» → HEAD. Поля для вставки в BODY у Tilda нет, и оно не нужно — обработчик вешается на документ. После вставки страницу нужно опубликовать заново.
<script>
// 1ОПД · приём согласий с форм Tilda — без своего сервера.
//
// Куда вставлять: Настройки страницы → «Дополнительно» → «HTML-код для
// вставки внутрь HEAD» (на одной странице) либо Настройки сайта →
// «Вставка кода» → HEAD (на всех). Поля для вставки в BODY у Тильды нет —
// и оно не нужно: обработчик вешается на document.
(function () {
if (window.__opd1Loaded) return; // код нередко вставляют дважды
window.__opd1Loaded = true;
var OPD = {
url: 'https://v2.1opd.ru/api/v2/create-agreement',
key: 'ВАШ_КЛЮЧ', // выпускается в кабинете, в карточке формы
formId: 0, // form_id вашей формы согласия
selector: 'form', // 'form[name="form123456"]' — если форм несколько
hashField: 'Email',
known: ['email', 'first_name', 'phone_number'], // поля вашей формы в 1ОПД
alias: {
email: ['email', 'e-mail', 'mail'],
first_name: ['name', 'firstname', 'first_name'],
phone_number: ['phone', 'tel', 'telephone']
}
};
var sent = (window.__opd1Sent = window.__opd1Sent || {});
// Имя поля Тильды → имя поля формы в 1ОПД. Не сопоставилось — не шлём:
// одно чужое имя отбивает весь запрос вместе с согласием.
function mapName(name) {
if (OPD.known.indexOf(name) >= 0) return name;
var low = String(name).toLowerCase();
if (OPD.known.indexOf(low) >= 0) return low;
for (var i = 0; i < OPD.known.length; i++) {
if ((OPD.alias[OPD.known[i]] || []).indexOf(low) >= 0) return OPD.known[i];
}
return null;
}
function pick(form) {
var fd = new FormData(form), fields = [], hash = '';
fd.forEach(function (v, k) {
if (String(v) === '') return;
if (!/^[-a-zA-Z0-9_]+$/.test(k)) return; // «Задача» и подобные
if (k.indexOf('tildaspec') === 0) return; // служебные поля Тильды
var mapped = mapName(k);
if (mapped) fields.push({ field: mapped, value: String(v) });
});
fd.forEach(function (v, k) {
if (!hash && k.toLowerCase() === OPD.hashField.toLowerCase()) hash = String(v);
});
if (!hash) {
var byType = form.querySelector('input[type="email"]');
if (byType) hash = byType.value || '';
}
return { fields: fields, hash: hash };
}
function send(form) {
if (!form || !form.matches || !form.matches(OPD.selector)) return;
var data = pick(form);
if (!data.hash) return;
var mark = OPD.formId + '|' + data.hash;
if (sent[mark]) return; // одна отправка — одно согласие
sent[mark] = true;
setTimeout(function () { delete sent[mark]; }, 60000);
// Запрос намеренно «простой»: ключ в теле, Content-Type text/plain, ни
// одного кастомного заголовка — тогда браузер не шлёт preflight.
fetch(OPD.url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
body: JSON.stringify({
api_key: OPD.key, form_id: OPD.formId,
hash_field: data.hash, fields: data.fields
})
}).then(function (r) { return r.json().then(function (d) { return { s: r.status, d: d }; }); })
.then(function (res) {
if (res.s === 200 || res.s === 202) console.log('1ОПД: согласие принято', res.d && res.d.hash);
else console.warn('1ОПД: приём отказал', res.s, res.d && (res.d.reason || JSON.stringify(res.d)));
})
.catch(function (e) { console.warn('1ОПД: ответ не получен', e); });
}
// Тильда отправляет форму своим обработчиком и нативного submit не
// порождает — ловим факт успешной отправки: её сетевой запрос и блок
// «Спасибо». Согласие пишется только по успеху.
var lastForm = null;
document.addEventListener('click', function (e) {
var el = e.target && e.target.closest ? e.target.closest('form') : null;
if (el) lastForm = el;
}, true);
function isTilda(u) {
u = String(u || '');
return u.indexOf('tildacdn') >= 0 || u.indexOf('/procces/') >= 0;
}
var xhrOpen = XMLHttpRequest.prototype.open, xhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (m, u) { this.__opdUrl = u; return xhrOpen.apply(this, arguments); };
XMLHttpRequest.prototype.send = function () {
var xhr = this;
if (isTilda(xhr.__opdUrl)) {
xhr.addEventListener('load', function () {
if (xhr.status >= 200 && xhr.status < 300) send(lastForm);
});
}
return xhrSend.apply(this, arguments);
};
var nativeFetch = window.fetch;
if (nativeFetch) {
window.fetch = function (input) {
var u = typeof input === 'string' ? input : (input && input.url);
var p = nativeFetch.apply(this, arguments);
if (isTilda(u)) p.then(function (r) { if (r && r.ok) send(lastForm); }).catch(function () {});
return p;
};
}
new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
var added = muts[i].addedNodes || [];
for (var j = 0; j < added.length; j++) {
var n = added[j];
if (n.nodeType === 1 && (n.className || '').toString().indexOf('t-form__successbox') >= 0) {
send(n.closest ? n.closest('form') : lastForm);
}
}
}
}).observe(document.documentElement, { childList: true, subtree: true });
})();
</script> Ключ вы выпускаете сами: кабинет → «Интеграция» → карточка сайта → «Выпустить ключ для этой формы». Показывается он один раз, целиком — скопируйте сразу; потеряли, значит выпускайте новый (прежний перестанет действовать). В allowed_origins внесите свой домен и https://*.tilda.ws, если проверяете на предпросмотре.
Если согласия не появились. Откройте консоль браузера (F12): код пишет туда и успех, и причину отказа. Пусто — код не подключился, проверьте вставку в HEAD и публикацию страницы. приём отказал 400 — в ответе названо поле, которого нет в форме согласия; добавьте его в кабинете или уберите из формы. 401 — ключ скопирован не целиком либо домен не внесён в allowed_origins. Ответ 202 — это успех: согласие принято в очередь, строка в реестре появится в течение минуты, повторять отправку «ради 200» не нужно.
Самые частые проблемы и что с ними делать. Решает 95% случаев.
Проверьте по порядку:
Проверьте 3 пункта:
proxy.php в $api_key стоит полный ключ: префикс sk_ и 64 hex-символа.API-KEY: и ключом в HTTP-заголовке нет лишних пробелов и переносов (в шаблоне выше всё корректно). Откройте proxy.php → $consent_form_ids и сверьте числа справа с номерами форм в ЛК 1ОПД → раздел «Интеграция». Под новую форму выпустите ключ заново — forms_scope задаётся в момент выпуска.
Откройте опубликованный сайт → правый клик на чекбоксе → «Посмотреть код» и найдите атрибут name="…". Это имя обязано точно совпадать с ключом в массиве $consent_form_ids в PHP-файле.
Проверьте и значение чекбокса: по умолчанию Tilda ставит yes. Если вы его меняли, поправьте сравнение в PHP (=== 'yes').
В $field_mapping укажите соответствие имён Tilda именам полей в 1ОПД (как они заведены в «Реестре процессов»). Например: 'Name' => 'first_name', 'Phone' => 'phone_number'.
Опубликуйте сайт через «Опубликовать». Webhook'и работают только на опубликованном (custom) домене, а не в превью *.tilda.cc.
chmod 644 proxy.php и chmod 755 opd/.Откройте ЛК → «Интеграция»: персональные snippet'ы + готовый HTML-чекбокс с авто-ссылками на согласие и политику + кнопка «IP-whitelist» для каждого ключа.
Открыть ЛК →Запросите тестовый доступ: выдаём ключ под ваш домен и помогаем подключиться.
Связаться →Все endpoints, коды ошибок, форматы HMAC/Origin/IP. Таблица «какую защиту выбрать».
Открыть →Успехом считаются и 200, и 202. Проверка «строго 200» отметит ошибкой согласие, которое 1ОПД принял и доставит.
В массиве fields передавайте любые из 28 полей ниже, имя поля стоит в столбце field. Обязательное одно: email, он же идентификатор субъекта (hash_field). Остальные 27 опциональны, незаполненное поле не передавайте. Все значения 1ОПД шифрует на своей стороне (AES-256-GCM). Актуальный список полей конкретной формы отдаёт POST /api/v2/get-fields.
| field | Название | Примечание |
|---|---|---|
email | Электронная почта | обязательное · идентификатор (hash_field) |
first_name | Имя | опциональное |
last_name | Фамилия | опциональное |
sur_name | Отчество | опциональное |
birth_date | Дата рождения | опциональное |
phone_number | Номер телефона | опциональное |
birth_place | Место рождения | опциональное |
registration_address | Адрес регистрации | опциональное |
delivery_address | Адрес доставки | опциональное |
passport_details | Паспортные данные | опциональное |
inn | ИНН | опциональное |
snils | СНИЛС | опциональное |
bank_details | Банковские реквизиты | опциональное |
account_vk | Аккаунт VK | опциональное |
account_telegram | Аккаунт Telegram | опциональное |
account_instagram | Аккаунт Instagram | опциональное |
account_facebook | Аккаунт Facebook | опциональное |
account_viber | Аккаунт Viber | опциональное |
account_ok | Аккаунт OK | опциональное |
account_whatsapp | Аккаунт WhatsApp | опциональное |
job_title | Должность | опциональное |
organization_name | Название организации | опциональное |
field_of_activity | Сфера деятельности | опциональное |
city | Город | опциональное |
mattermost_account | Аккаунт Mattermost | опциональное |
slack_account | Аккаунт Slack | опциональное |
ms_teams_account | Аккаунт MS Teams | опциональное |
sex | Пол | значения male / female · в карточках: «мужчина» / «женщина» |
Набор одинаков для всех способов интеграции (Tilda, WordPress, Bitrix, браузерный JS, server-side API). Каждое значение приходит строкой длиной до 5000 символов.