Вебхуки
claritos сам сообщает, когда что-то происходит, — вместо того чтобы вы спрашивали. Один endpoint, один секрет, подписанные доставки, повторы и повторная отправка.
Как это работает
Вы создаёте endpoint (URL + типы событий) и ровно один раз получаете секрет. Когда происходит подписанное событие, claritos отправляет подписанный HTTPS POST на ваш URL. Ответьте 2xx в течение 10 секунд — всё остальное считается неудачей и повторяется. События называются домен.сущность.глагол (например, contacts.contact.created). GET /webhooks/events возвращает каталог; GET /webhooks/events/{type}/sample — полный пример для типа; GET /webhooks/events/{type}/recent — три последних реальных события. Для «нового лида» есть ровно одно событие, leads.lead.created, независимо от того, пришёл ли он из воронки, формы, поиска специалистов или введён вручную. Endpoint'ы создаются через API (scope webhooks:manage) или в claritos: Организация → Настройки → API. Оба места показывают один и тот же список.
Подписка
Только https-URL, публично доступные — частные сети, localhost и редиректы claritos отклоняет при создании. Пустой eventTypes означает «все события». Не более 25 endpoint'ов на организацию.
curl -X POST https://www.claritos.io/api/v1/webhooks \
-H "Authorization: Bearer capi_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/claritos",
"description": "Neue Leads → CRM",
"eventTypes": ["leads.lead.created", "contacts.contact.created"]
}'
# → 201 { "id": "…", "secret": "whsec_…", … } (secret nur dieses eine Mal)import requests
r = requests.post("https://www.claritos.io/api/v1/webhooks",
headers={"Authorization": "Bearer capi_…"},
json={"url": "https://hooks.example.com/claritos",
"eventTypes": ["leads.lead.created"]})
endpoint = r.json()
secret = endpoint["secret"] # sicher speichern — kommt nie wiederДоставка
Каждая доставка несёт заголовки webhook-id (идентификатор доставки, при повторах тот же), webhook-timestamp (Unix-секунды) и webhook-signature (см. ниже). Тело всегда одной формы: id (событие), type, timestamp, organizationId и data с содержимым события. Событие лида плоское: имя, e-mail, телефон, компания сверху, все значения формы в fields, согласия в consent.
POST /claritos HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: claritos-webhooks/1
webhook-id: cmf2dlv000000000000000001
webhook-timestamp: 1757167391
webhook-signature: v1,K5oZ…base64…=
x-claritos-event: leads.lead.created
{
"id": "cmf2evt000000000000000001",
"type": "leads.lead.created",
"timestamp": "2026-09-06T14:03:11.000Z",
"organizationId": "cmf1org000000000000000000",
"data": {
"source": "funnel",
"sourceId": "cmf2funnel000000000000001",
"sourceName": "Longevity-Standortbestimmung",
"contactId": "cmf2contact00000000000001",
"prospectId": null,
"firstName": "Anna",
"lastName": "Muster",
"email": "anna@example.com",
"phone": "+49 30 1234567",
"company": null,
"jobTitle": null,
"fields": { "interesse": "Standortbestimmung" },
"consent": { "consent-email": true },
"receivedAt": "2026-09-06T14:03:11.000Z"
}
}Проверка подписи
Подпись по Standard Webhooks: HMAC-SHA256 над id.timestamp.body с base64-декодированным секретом (без префикса whsec_), в заголовке в base64 как v1,<подпись>. 24 часа после ротации секрета в заголовке две подписи — должна совпасть одна. Проверяйте метку времени (±5 минут) против атак повторного воспроизведения и сравнивайте за постоянное время. Важно: проверяйте сырое тело, а не заново сериализованный JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
// Express/Next: den ROHEN Body verwenden (kein re-serialisiertes JSON).
export function verifyClaritosWebhook(rawBody: string, headers: Record<string, string>, secret: string): boolean {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sigHeader = headers["webhook-signature"] ?? "";
if (!id || !ts) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 Minuten Toleranz
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
return sigHeader.split(" ").some((part) => {
const [version, sig] = part.split(",");
if (version !== "v1" || !sig) return false;
const given = Buffer.from(sig, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}import base64, hmac, hashlib, time
def verify_claritos_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
msg_id = headers.get("webhook-id"); ts = headers.get("webhook-timestamp")
if not msg_id or not ts or abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
expected = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
for part in headers.get("webhook-signature", "").split(" "):
version, _, sig = part.partition(",")
if version == "v1" and hmac.compare_digest(base64.b64decode(sig), expected):
return True
return Falsefunction verifyClaritosWebhook(string $rawBody, array $headers, string $secret): bool {
$id = $headers['webhook-id'] ?? ''; $ts = $headers['webhook-timestamp'] ?? '';
if ($id === '' || $ts === '' || abs(time() - (int)$ts) > 300) return false;
$key = base64_decode(preg_replace('/^whsec_/', '', $secret));
$expected = hash_hmac('sha256', "$id.$ts.$rawBody", $key, true);
foreach (explode(' ', $headers['webhook-signature'] ?? '') as $part) {
[$version, $sig] = array_pad(explode(',', $part, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, base64_decode($sig))) return true;
}
return false;
}func verifyClaritosWebhook(rawBody []byte, h http.Header, secret string) bool {
id, ts := h.Get("webhook-id"), h.Get("webhook-timestamp")
t, err := strconv.ParseInt(ts, 10, 64)
if id == "" || err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 {
return false
}
key, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
mac := hmac.New(sha256.New, key)
mac.Write([]byte(id + "." + ts + "."))
mac.Write(rawBody)
expected := mac.Sum(nil)
for _, part := range strings.Split(h.Get("webhook-signature"), " ") {
v, sig, ok := strings.Cut(part, ",")
if !ok || v != "v1" {
continue
}
given, _ := base64.StdEncoding.DecodeString(sig)
if hmac.Equal(given, expected) {
return true
}
}
return false
}Повторы и отключение
Если endpoint не отвечает 2xx (таймаут 10 с, редиректы считаются ошибкой), claritos повторяет через 1 мин, 5 мин, 30 мин, 2 ч и 12 ч — пять попыток, затем доставка DEAD. Пять неудачных доставок подряд отключают endpoint (disabledAt); организация получает уведомление. Включить снова: PATCH /webhooks/{id} с active: true — счётчик обнуляется. Тестовые доставки (POST …/test) не учитываются. Доставки на один endpoint идут по очереди (нестрогий порядок); всё же сделайте приёмник идемпотентным — повтор несёт тот же webhook-id.
Повторная отправка
Неудачные или пропущенные события можно получить заново: либо конкретные доставки через deliveryIds, либо все события за период (по умолчанию: последние 24 часа, макс. 500, тестовые исключены). Каждый повтор — новая доставка с новым webhook-id.
# Zustellprotokoll (gescheiterte zuerst filtern)
curl "https://www.claritos.io/api/v1/webhooks/{id}/deliveries?status=DEAD" -H "Authorization: Bearer capi_…"
# Bestimmte Zustellungen erneut senden …
curl -X POST https://www.claritos.io/api/v1/webhooks/{id}/replay -H "Authorization: Bearer capi_…" \
-H "Content-Type: application/json" -d '{"deliveryIds": ["cmf2dlv…"]}'
# … oder alle Ereignisse eines Zeitraums (max. 500)
curl -X POST https://www.claritos.io/api/v1/webhooks/{id}/replay -H "Authorization: Bearer capi_…" \
-H "Content-Type: application/json" \
-d '{"since": "2026-09-05T00:00:00Z", "eventTypes": ["leads.lead.created"]}'Зеркалирование контактов
Чтобы зеркалировать контакты в другую систему, подпишитесь на четыре события: contacts.contact.created (каждый новый контакт, каким бы путём он ни появился; кроме массовых импортов и массовых переносов: импорт из файла, перенос клиентов Stripe, основных данных DATEV или списка партнёров в бухгалтерии и импорт основных данных кредитов, а также тестовые случаи, которые claritos передаёт в организацию для тренировки; их читайте после запуска через GET /contacts), contacts.contact.updated (с changedFields), contacts.contact.deleted (контакта больше нет) и contacts.contact.merged. При объединении остаётся один контакт (contact), контакты из mergedContactIds после этого удалены. Для них не приходит deleted, а для оставшегося не приходит updated: перенесите свои записи с этих идентификаторов на contact.id вместо удаления и примите contact как новое состояние. Объединить самостоятельно можно через POST /contacts/{id}/merge (contacts:write): {id} остаётся, до 20 контактов из mergeContactIds удаляются, и всё, что к ним привязано, переходит к оставшемуся контакту. Это окончательно. Без fieldSources действуют значения оставшегося контакта; fieldSources указывает для каждого поля участника, чьё значение действует. Теги объединяются. Объединять можно только контакты одного типа. 409 называет в reason, что нужно сначала устранить: type-mismatch, bookings-on-multiple (проводки более чем у одного из контактов) или блокировку вроде client-records-on-multiple.
# cmf2contact00000000000001 bleibt, cmf2contact00000000000009 wird gelöscht; die Telefonnummer kommt von ihm
curl -X POST https://www.claritos.io/api/v1/contacts/cmf2contact00000000000001/merge \
-H "Authorization: Bearer capi_…" \
-H "Content-Type: application/json" \
-d '{"mergeContactIds": ["cmf2contact00000000000009"], "fieldSources": {"phone": "cmf2contact00000000000009"}}'
# → 200 { "contact": { "id": "cmf2contact00000000000001", … }, "mergedContactIds": ["cmf2contact00000000000009"], "reassignedReferences": 7 }{
"type": "contacts.contact.merged",
"data": {
"contact": { "id": "cmf2contact00000000000001", "displayName": "Anna Muster", … },
"mergedContactIds": ["cmf2contact00000000000009"],
"reassignedReferences": 7
}
}Zapier, n8n, Make
Zapier (instant trigger): performSubscribe → POST /webhooks с вашим hook-URL, performUnsubscribe → DELETE /webhooks/{id}, performList → GET /webhooks/events/{type}/recent (запасной вариант: …/sample). Авторизация: «API Key» с заголовком Authorization: Bearer. n8n (trigger node): методы create/delete как выше; подпись проверяйте в ноде секретом из credential. Make: instant trigger с той же последовательностью; модуль «Custom Webhook» работает сразу, если создать URL как endpoint вручную. Опрос как запасной путь: каждый список поддерживает updatedSince и стабильную сортировку.