Webhook

用 Webhook 接收事件

在 Comu APP 中配置一个 HTTPS endpoint,接收转录完成通知, Comu 会在事件发生时往这个 URL POST 一份 JSON。 本页覆盖 App 内配置、事件清单、payload 结构、验签与投递行为。

Comu APP

在 Comu APP 配置 Webhook

配置路径:Comu APP → 我的页面 → Developers → Webhook。 进入 Webhook 页面后,按表单完成配置即可。

1
打开 Developers

登录 Comu APP,进入我的页面,点击 Developers。

2
进入 Webhook

在 Developers 页面选择 Webhook, 新建或编辑 endpoint,填写 HTTPS 接收 URL。App 自动订阅 content.completed。

3
复制密钥到服务端

将页面展示的 signingSecret 写入你的密钥管理工具, 接收端用它验证 X-Kirby-Signature。

配置完成后关注两件事

接收:事件发生时,Comu 异步往你这个 URL POST 一份 JSON envelope, 带上 X-Kirby-Signature / X-Kirby-Event-Type 等 header。

验签 + 处理:用 signing secret 重算 HMAC-SHA256 比对, 确认是 Comu 发的且未被篡改;拒绝可疑请求,正常请求按业务消费即可。

Webhook 配置项

配置项 说明
Endpoint URL 你的 HTTPS 接收地址。Comu 会向这个 URL POST JSON payload。
订阅事件 App 自动订阅 content.completed,无需选择其他事件。
描述 用于区分多个 endpoint,例如生产环境、测试环境或某个集成系统。
signingSecret 页面展示的签名密钥,接收端验签时使用;请按密钥级别保管,不要写入代码仓库或日志。
Events

事件清单(1 类)

当前仅提供「转录完成」通知,事件码为 content.completed。最新转录与总结任务都成功后推送,不等待洞察或待办生成。

content.completed转录完成
json · content.completed
{
  "id": "evt_example_001",
  "type": "content.completed",
  "api_version": "2026-04-24",
  "created": 1789380000,
  "payload_style": "fat",
  "user_id": "u_123",
  "data": {
    "object": {
      "object": "note",
      "note_id": "note_123",
      "transcript_task_id": "task_transcript_123",
      "summary_task_id": "task_summary_123",
      "completed_at": "2026-09-14T10:00:00Z"
    }
  }
}

事件码会出现在 payload 的 type 和请求头 X-Kirby-Event-Type 中。收到通知后,可用 note_id 通过查询接口读取转录与总结正文。

Verify

签名验证(防伪造 + 防重放)

Comu 在每次推送的请求头里写入 X-Kirby-Signature, 格式与 Stripe 一致:t={unix秒},v1={hex(HMAC-SHA256)}。 接收端用同一把 signing secret 重算并比对即可。

1
解析 header
从 X-Kirby-Signature 解出 t(时间戳)和 v1(hex 签名)。
2
校验时间戳
|now - t| ≤ 300s,超出直接拒绝(防重放)。
3
重算 HMAC-SHA256
对 t + "." + rawBody 用 signing secret 算 HMAC-SHA256 hex, 用 常量时间比对,防时序侧信道。
4
通过后再处理
任何步骤失败一律 401 / 400,拒绝执行业务副作用。
展开 Java 实现(零第三方依赖,可直接拷到你的服务里)
java · WebhookVerifier
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Locale;

public final class ComuWebhookVerifier {

  private static final long TOLERANCE_SECONDS = 300L;
  private static final String ALGO = "HmacSHA256";

  /**
   * 验证 Comu webhook 请求。
   *
   * @param secret  在 Comu APP 的 Webhook 页面拿到的 whsec_xxx 明文
   * @param header  请求头 X-Kirby-Signature 的完整值,形如 t=...,v1=...
   * @param rawBody 原始请求体字符串(必须是未反序列化的 raw bytes 字符串)
   */
  public static boolean verify(String secret, String header, String rawBody) {
    if (secret == null || header == null || rawBody == null) return false;

    long t = -1L;
    String v1 = null;
    for (String part : header.split(",")) {
      int eq = part.indexOf('=');
      if (eq <= 0) continue;
      String k = part.substring(0, eq).trim();
      String v = part.substring(eq + 1).trim();
      if ("t".equals(k)) {
        try { t = Long.parseLong(v); } catch (NumberFormatException ignore) { return false; }
      } else if ("v1".equals(k)) {
        v1 = v;
      }
    }
    if (t < 0 || v1 == null || v1.isEmpty()) return false;

    long now = System.currentTimeMillis() / 1000L;
    if (Math.abs(now - t) > TOLERANCE_SECONDS) return false;

    String expected = hmacSha256Hex(secret, t + "." + rawBody);
    byte[] a = expected.getBytes(StandardCharsets.US_ASCII);
    byte[] b = v1.getBytes(StandardCharsets.US_ASCII);
    return MessageDigest.isEqual(a, b);
  }

  private static String hmacSha256Hex(String secret, String payload) {
    try {
      Mac mac = Mac.getInstance(ALGO);
      mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), ALGO));
      byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
      StringBuilder sb = new StringBuilder(raw.length * 2);
      for (byte by : raw) sb.append(String.format(Locale.ROOT, "%02x", by));
      return sb.toString();
    } catch (Exception e) {
      throw new IllegalStateException("HMAC sign failed", e);
    }
  }
}

关键点: ① 一定用原始字节算签名,不要先 JSON 反序列化再 toString,否则空白字符差异会导致签名永远不匹配; ② 用 MessageDigest.isEqual 而不是 String.equals 做比对,避免时序侧信道; ③ 任意语言只要有 HMAC-SHA256 + 常量时间比对即可,不限于 Java。

Delivery

投递行为

异步投递

事件不会阻塞业务请求 —— 业务侧产生事件后丢消息队列,由独立 worker 异步推送到 endpoint。

失败自动重试

非 2xx / 超时会按指数退避重试若干次;持续失败会进入 DLQ,可在管理后台查看投递历史。

幂等消费

重试可能导致同一事件投递多次,接收端按 payload 中的事件 ID 做幂等去重, 避免一个事件被业务消费两次。

Security checklist

上线前自检

仅 HTTPS

endpoint URL 必须是 HTTPS;纯 HTTP 在公网上既会泄漏 payload 也会让中间人替换响应。

必须验签

没有验签的 webhook 接收端等于一个公网开放的 RPC,任何人都能伪造事件。没有例外。

最小事件订阅

只订阅业务真正需要的事件类型,减少不必要的 QPS 与噪声日志,方便定位异常。

signingSecret 入保险柜

和 API Key 同等敏感。从 Comu APP 获取后立刻写进密钥管理工具,不要写入代码仓库或日志。

幂等键去重

用 payload 内的事件 ID 在你侧建唯一索引; 重试 / 网络抖动导致的重复投递就不会触发二次副作用。

5 秒内回 2xx

收到 webhook 后立即响应 200,长任务异步处理;超时会被判失败并触发重试。