Docs

Вебхуки

Последнее обновление: 2026-10-035 мин чтения

Вебхук - это 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}/deliveries50 последних доставок: статус, код ответа, задержка, начало ответа
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.failedPDF-презентация

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) показывает код ответа вашего сервера и начало тела ответа - по нему видно, почему доставка не прошла.

Связанные статьи

Эта статья была полезной?