Вебхук - это POST, который GREM отправляет на ваш URL, когда что-то произошло: завершилась задача, изменился объект или лид. Управление подписками требует области webhooks и не тарифицируется. Вебхуками агентства управляет ключ всего агентства; ключ агента получает 403.
Подписаться
curl -X POST https://developers.grem.capital/api/v1/webhooks \
-H "Authorization: Bearer gsk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/grem-hook","events":["content.completed","content.failed","lead.created"]}'
{ "id": "...", "url": "https://example.com/grem-hook", "events": [...], "status": "active",
"failureCount": 0, "lastDeliveryAt": null, "secret": "whsec_..." }
url- публичный адресhttps://на стандартном порту (443), безuser:passwordв адресе. Другие порты, логин и пароль в адресе, частные, локальные и внутренние адреса отклоняются (400). Правило порта действует, когда вы задаете адрес: адреса, сохраненные раньше с другим портом, продолжают получать доставки.events- хотя бы одно событие из списка ниже; неизвестные названия отбрасываются.secret- секрет подписи. Он возвращается только один раз, сохраните его.- На один аккаунт - до 10 адресов и отдельно до 10 адресов песочницы, созданных тестовыми ключами; 11-й отклоняется с
422 business_rule. - Адреса, созданные тестовым ключом, - адреса песочницы: они получают только события задач песочницы (Песочница).
Подписки можно вести и в кабинете: API → Вебхуки. Там же можно сменить секрет подписи.
Управление
| Запрос | Что делает |
|---|---|
GET /webhooks | список ваших подписок (без секретов) |
PATCH /webhooks/{id} | изменить url, events или status (active / disabled) |
DELETE /webhooks/{id} | удалить подписку |
POST /webhooks/{id}/test | отправить пробное событие; тело {"eventType": "content.completed"} задает форму примера |
GET /webhooks/{id}/deliveries | 50 последних доставок: статус, код ответа, задержка, начало ответа |
POST /webhooks/{id}/deliveries/{deliveryId}/resend | отправить доставку еще раз; deliveryId — _id строки из журнала |
PATCH без единого допустимого события в events или со status, отличным от active / disabled, отклоняется с 400. DELETE неизвестной подписки отвечает 404. В журнале доставок payload - это data события.
События
Завершение задач
| Событие | Когда |
|---|---|
valuation.completed, valuation.failed | полный отчет об оценке |
review.completed, review.failed | проверка отчета |
content.completed, content.partial, content.failed | пакет контента (partial - часть файлов не получилась) |
text.completed, text.failed | тексты |
video.completed, video.failed | видео |
avatar.completed, avatar.failed | видео с аватаром |
photo.completed, photo.failed | улучшение фото |
floorplan.completed, floorplan.failed | планировка |
pdf.completed, pdf.failed | PDF-презентация |
data: { "jobId", "status", "state" }. status - исходный статус инструмента в верхнем регистре (например, COMPLETED); ветвитесь по state. Если задача не завершилась за 30 минут - .failed с "reason": "timeout". Задача отслеживается, только если подписка на ее событие существовала в момент создания задачи (Задачи и результаты).
Объекты
| Событие | Когда |
|---|---|
object.created | вы создали объект |
object.updated | объект изменен, в том числе модератором |
object.published | объект стал публичным |
object.deleted | черновик удален или опубликованный объект снят в архив |
data: { "objectId", "kind", "status", "occurredAt" }; в object.deleted вместо status - mode (deleted или archived). Полные данные - через GET /objects/{kind}/{id}.
Лиды
| Событие | Когда |
|---|---|
lead.created | новый лид, адресованный вам |
lead.updated | изменился статус или ответственный |
data: { "leadId", "status", "type", "source", "objectId", "occurredAt" }. Персональных данных клиента в событии нет - полный лид через GET /leads/{id}.
Swipe Match
| Событие | Когда |
|---|---|
match.lead.offered | вам (или агенту вашего агентства) предложен лид |
match.lead.offer_closed | предложение закрылось без вас (вышло время) |
match.lead.matched | лид закреплен за вами |
match.lead.updated | лид перешел на новый этап |
match.lead.unassigned | лид снят с вас |
data: { "leadId", "eventId", "eventCode", "occurredAt", "source", "stage", "active", "leadKind", "clientType", "country", "brokerId", "agencyId" }. Имени, телефона и почты клиента в событиях нет (Swipe Match).
Формат доставки
POST https://example.com/grem-hook
Content-Type: application/json
x-grem-event: content.completed
x-grem-signature: t=1759400000,v1=3b78a03b78058d229023db2a0b4ee072...
{ "id": "...", "type": "content.completed", "createdAt": "2026-10-02T10:00:00.000Z", "data": { ... } }
idодинаковый во всех повторах одной доставки - используйте его, чтобы не обработать событие дважды.tиcreatedAt- момент конкретной попытки.- Тестовая доставка (
POST /webhooks/{id}/test) сeventTypeиз списка несетtest: trueвdata; без него тип события -webhook.test.eventType, которого нет в списке, -400. Самwebhook.testпринимается.
Проверка подписи
Подпись - HMAC-SHA256 от строки "<t>.<сырое тело>" с секретом подписки, в шестнадцатеричном виде в v1. Проверяйте до любой обработки и отбрасывайте устаревшие t (защита от повторной отправки).
Считайте подпись от сырого тела запроса, ровно как оно пришло, до разбора JSON. Повторная сериализация меняет байты, и подпись не совпадет.
const crypto = require('crypto');
function verifyGremSignature(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(String(header || '').split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/grem-hook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyGremSignature(process.env.GREM_WEBHOOK_SECRET, req.get('x-grem-signature'), raw)) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(raw);
// event.id, event.type, event.data
res.sendStatus(200);
});
В SDK то же делает verifyWebhookSignature().
Повторы и отключение
- Доставка успешна, если ваш сервер ответил
2xxв течение 10 секунд. Отвечайте быстро, тяжелую работу делайте в фоне. - Неудачная доставка повторяется: через 5 минут, 30 минут, 2 часа и 6 часов - всего до 5 попыток.
- После 5 подряд полностью неудачных доставок подписка отключается (
status: disabled). Включите ее снова черезPATCH /webhooks/{id}с{"status":"active"}или в кабинете. - Журнал доставок (
GET /webhooks/{id}/deliveries) показывает код ответа вашего сервера и начало тела ответа - по нему видно, почему доставка не прошла.
Связанные статьи
- Задачи и результаты - состояния задач.
- Swipe Match - коды событий лидов модуля.