Docs

Webhooks

最后更新: 2026-10-03阅读约 7 分钟

Webhook 是发生某件事时 GREM 向您的 URL 发送的 POST:任务完成、房源或线索发生变化。管理订阅需要 webhooks 权限范围,免费。机构的 Webhook 由整个机构的密钥管理;绑定经纪人的密钥会收到 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:默认端口(443)上的公开 https:// 地址,地址中不能包含 user:password。其他端口、地址中的凭据,以及私有、本地和内部地址会被拒绝(400)。端口规则在设置地址时生效:此前以其他端口保存的地址会继续接收投递。
  • events:下方列表中至少一个事件;未知名称会被忽略。
  • secret:签名密钥。只返回一次,请妥善保存。
  • 每个账户最多 10 个地址,另外最多 10 个用测试密钥创建的沙盒地址;第 11 个会以 422 business_rule 被拒绝。
  • 用测试密钥创建的地址是沙盒地址:只接收沙盒任务的事件(沙盒)。

也可以在后台管理订阅:API → Webhooks。在那里还可以更换签名密钥。

管理

请求作用
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

events 中没有任何有效事件,或 status 不是 active / disabled 的 PATCH 会以 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 分钟内未完成时为带 "reason": "timeout" 的 .failed。只有创建任务时已存在对应事件的订阅,任务才会被跟踪(任务与结果)。

房源

事件时机
object.created您创建了房源
object.updated房源被修改,包括审核员的修改
object.published房源变为公开
object.deleted草稿被删除或已发布房源被归档

data:{ "objectId", "kind", "status", "occurredAt" };object.deleted 中以 mode(deleted 或 archived)代替 status。完整数据: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 是该次尝试的时间。
  • 带列表中 eventType 的测试投递(POST /webhooks/{id}/test)在 data 中带有 test: true;不带时事件类型为 webhook.test。不在列表中的 eventType 返回 400。webhook.test 本身是可以的。

验证签名

签名是用订阅密钥对字符串 "<t>.<原始请求体>" 计算的 HMAC-SHA256,以十六进制放在 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() 完成。

重试与停用

  • 您的服务器在 10 秒内返回 2xx 即视为投递成功。请快速响应,繁重的工作放到后台。
  • 失败的投递会在 5 分钟、30 分钟、2 小时和 6 小时后重试:总共最多 5 次尝试。
  • 连续 5 次投递完全失败后,订阅会被停用(status: disabled)。用 PATCH /webhooks/{id} 加 {"status":"active"} 或在后台重新启用。
  • 投递日志(GET /webhooks/{id}/deliveries)显示您服务器的响应码和响应体开头:从中可以看出投递失败的原因。

相关文章

这篇文章对您有帮助吗?