接收 Narwhal Webhook

用你的 HTTPS URL 以及所需的国家和系列创建 Webhook 目标地址,然后在信任每次投递之前先验证其签名。每个账户最多可保留 25 个目标地址,每次投递按一次调用计费。

创建目标地址并接收投递

Narwhal 的各数据 API 共用一个账户和一个 API 密钥,REST 与 MCP 也共用该密钥。请把密钥保存在 NARWHAL_API_KEY 环境变量中,并以 Authorization: Bearer YOUR_KEY 的形式发送到 https://api.narwhalapi.com。

  1. 创建目标地址 将 HTTPS URL、国家代码和数据族 slug 发送到 POST /v1/webhooks/destinations。URL 最多 2,048 个字符;选择 1 到 60 个 alpha-3 国家代码或 WLD,以及 1 到 14 个数据族 slug,其中包括 holidays、predictions 和 filings。请求中还可以带上 enabled: true。该 curl 请求为 prices 和 labour 数据族选择了 USA 和 MEX。请把示例中的 HTTPS URL 换成你的接收端接受投递的 URL。
    创建 Webhook 目标地址
    curl -s -X POST https://api.narwhalapi.com/v1/webhooks/destinations \
      -H "Authorization: Bearer $NARWHAL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"url":"https://example.com/narwhal",
           "countries":["USA","MEX"],"families":["prices","labour"]}'
  2. 保存目标地址的密钥 响应包含 destination_id 和 secret,其中 secret 只显示一次。请把 NARWHAL_WEBHOOK_SECRET 设置为这个密钥,供接收端使用。请保留目标地址 ID,用于指向该目标地址的请求,包括测试投递请求。计算投递签名时使用返回的密钥;API 密钥则继续放在 API 请求的 authorization 头中。rotate-secret 返回的密钥同样只显示一次;轮换后的 24 小时内,签名头会再带上一个用旧密钥签名的值。
  3. 处理前先验证签名 下面的接收端以恒定时间比较签名,并拒绝早于五分钟的时间戳。只要有一个签名值匹配,就接受该投递。Python SDK 也提供了验证函数:from narwhalapi.webhooks import verify。请尽快返回 2xx,再去做后续处理。
    Python(Flask):验证签名的接收端
    import hashlib, hmac, json, os, time
    from flask import Flask, abort, request
    
    app = Flask(__name__)
    SECRET = os.environ[
        "NARWHAL_WEBHOOK_SECRET"
    ]  # returned once when you create the destination
    seen = set()  # use a database table in production
    
    
    @app.post("/narwhal")
    def narwhal():
        raw = request.get_data()  # exact bytes, before any JSON parsing
        stamp = request.headers.get("x-narwhal-timestamp", "")
        if not stamp.isdigit() or abs(time.time() - int(stamp)) > 300:
            abort(400)  # missing, or older than five minutes
        expected = (
            hmac.new(SECRET.encode(), f"{stamp}.".encode() + raw, hashlib.sha256)
            .hexdigest()
            .encode()
        )
        header = request.headers.get("x-narwhal-signature", "")
        sent = [part.strip().removeprefix("sha256=").encode() for part in header.split(",")]
        if not any(hmac.compare_digest(expected, candidate) for candidate in sent):
            abort(401)  # a missing or malformed header lands here, not in a 500
        try:
            event = json.loads(raw)
        except ValueError:
            abort(400)
        if not isinstance(event, dict):
            abort(400)
        if event.get("event_id") in seen:  # retries and replays repeat the same event_id
            return "", 200
        seen.add(event.get("event_id"))
        print(event.get("type"), event.get("country"), event.get("family"))
        return "", 200
  4. 请求测试投递 使用已保存的目标地址 ID 调用 POST /v1/webhooks/destinations/{destination_id}/test。测试投递带有 is_test: true,没有游标。投递状态为 pending、leased、succeeded、failed 和 cancelled。创建超出上限的目标地址会返回 409 limit_reached。

事件类型包括 release.scheduled、release.rescheduled、release.cancelled、release.updated;dataset.published 和 dataset.revised;yield_curve.published 和 yield_curve.revised;以及 holiday.calendar.revised。预测类有 prediction.event.closed 和 prediction.event.settled。备案文件类有 filing.published。处理事件时请使用载荷中的 type、country 和 family 字段。

当时间戳缺失或无效,以及请求体无法解析为 JSON 对象时,接收端返回 400。签名缺失或格式错误时返回 401。签名比较通过后,接收端先解析请求体,再读取事件字段并记录事件 ID。

接收所关注公司的新备案文件

将 filings 系列和 companies 列表添加到目标地址。此后,这些公司每提交一次文件,Narwhal 就会发送一个 filing.published 事件。对于 13D 或 13G 持股申报,当所列公司是被持股的公司时,也会发送事件。每个账户最多可关注 1,000 家公司。

监控 Apple 的备案文件
curl -s -X POST https://api.narwhalapi.com/v1/webhooks/destinations \
  -H "Authorization: Bearer $NARWHAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/narwhal","countries":["USA"],
       "families":["filings"],"companies":["usa:0000320193"]}'

公司通过 Filings API 的 company_id 指定,例如 Apple 为 usa:0000320193。filings 数据族必须带 companies 列表,缺少时会被拒绝。未知公司会返回 400,并列出找不到的 ID。请求仍需要 countries,但国家并不会缩小备案文件的范围。

备案文件事件载荷
{
  "schema_version": "1",
  "event_id": "...",
  "cursor": "1",
  "type": "filing.published",
  "country": "USA",
  "recorded_at": "2026-10-30T21:05:12Z",
  "filing": {
    "filing_id": "0000320193-26-000106",
    "company_id": "usa:0000320193",
    "form": "10-K",
    "type": "annual",
    "fy": 2026,
    "fp": "FY",
    "period_end": "2026-09-26",
    "filed_at": "2026-10-30T21:01:40Z"
  }
}

用 filing.filing_id 从 Filings API 获取该备案文件。当备案文件没有 fy、fp 和 period_end 时(多数事件类和持股类备案即是如此),这些字段会被省略。

不写代码接收 Webhook

Zapier 的 Catch Hook、Make 的 Custom webhook 或 n8n 的 Webhook 节点都可以提供目标地址 URL。把该 URL 填入目标地址的 url 字段,并为流程选择国家和数据族。请保密 catch hook 的 URL:这些无代码目标地址不会替你验证 Narwhal 签名。

签名验证需要代码,例如上面的 Python 接收端。如果需要验证,请用该接收端处理传入的投递。Webhook 参考介绍了目标地址、投递历史和重放操作;事件参考介绍了可从游标开始读取事件的 REST 日志。

选择接收端还是轮询

情况最合适的方法原因
你自己的服务Python 接收端验证签名并处理重复的事件 ID
无代码流程Zapier、Make 或 n8n 的 catch hook提供目标地址 URL;请保密该 URL
用轮询代替投递GET /v1/events 或经济日历日志按游标读取;日历按日期覆盖

为什么要验证原始 Webhook 请求体?

签名针对的是请求体的精确字节,因此解析后再重新序列化会改变你所比较的内容。把 x-narwhal-timestamp 当作 Unix 秒读取,然后依次拼接它的值、一个英文句点和原始请求体。用以 UTF-8 编码的密钥计算小写十六进制的 HMAC-SHA256。

x-narwhal-signature 头的值为 sha256=<hex>。与接收端的做法一样,请在解析 JSON 之前,以恒定时间将计算出的值与收到的签名进行比较。

如果我的 Webhook 服务器宕机了怎么办?

投递会重试,投递历史可让你查看失败并重放某次投递。用 GET .../deliveries 查看目标地址的历史,再对想重新接收的投递调用 POST .../deliveries/{delivery_id}/replay。重放会重复同一个事件。

若要从保存的位置继续追赶,请从最后一个游标开始读取 GET /v1/events。请为日志这条路径保存好游标;测试投递操作不会提供游标。

同一个事件会收到两次吗?

会。重试和重放会重复相同的 event_id。请把它当作幂等键,并在处理事件之前先存储。在生产环境中,如接收端注释所示,请用数据库表保存这些 ID。

示例接收端把 ID 保存在 seen 中。ID 已存在时,它直接返回 200,不会再次打印该事件。遇到未见过的 ID 时,它先添加该 ID,再打印事件的 type、country 和 family。

示例:接收一次投递

对于记录于 2026-09-25T15:44:56Z 的事件,载荷标明了 dataset.published、国家 MEX 和数据族 labour,还带有模式版本、事件 ID、游标、资源 ID 和资源版本。

经济数据事件载荷
{
  "schema_version": "1",
  "event_id": "...",
  "cursor": "1",
  "type": "dataset.published",
  "country": "MEX",
  "family": "labour",
  "resource_id": "...",
  "resource_version": "...",
  "recorded_at": "2026-09-25T15:44:56Z"
}

投递会带着时间戳和签名头到达。接收端读取时间戳,要求其时长不足 300 秒,然后重新计算 <timestamp>.<body> 的 HMAC。签名匹配后,它解析载荷并查找 event_id。对于未见过的事件,它存储该 ID,打印下面的字段,然后返回 200。

接收端输出
dataset.published MEX labour

发布于。

后续步骤