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.failed | PDF 演示文稿 |
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)显示您服务器的响应码和响应体开头:从中可以看出投递失败的原因。
相关文章
- 任务与结果:任务状态。
- Swipe Match:模块线索的事件代码。