用你的 HTTPS URL 以及所需的国家和系列创建 Webhook 目标地址,然后在信任每次投递之前先验证其签名。每个账户最多可保留 25 个目标地址,每次投递按一次调用计费。
创建目标地址并接收投递
Narwhal 的各数据 API 共用一个账户和一个 API 密钥,REST 与 MCP 也共用该密钥。请把密钥保存在 NARWHAL_API_KEY 环境变量中,并以 Authorization: Bearer YOUR_KEY 的形式发送到 https://api.narwhalapi.com。
- 创建目标地址 将 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。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"]}' - 保存目标地址的密钥 响应包含
destination_id和secret,其中secret只显示一次。请把NARWHAL_WEBHOOK_SECRET设置为这个密钥,供接收端使用。请保留目标地址 ID,用于指向该目标地址的请求,包括测试投递请求。计算投递签名时使用返回的密钥;API 密钥则继续放在 API 请求的 authorization 头中。rotate-secret返回的密钥同样只显示一次;轮换后的 24 小时内,签名头会再带上一个用旧密钥签名的值。 - 处理前先验证签名 下面的接收端以恒定时间比较签名,并拒绝早于五分钟的时间戳。只要有一个签名值匹配,就接受该投递。Python SDK 也提供了验证函数:
from narwhalapi.webhooks import verify。请尽快返回 2xx,再去做后续处理。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 - 请求测试投递 使用已保存的目标地址 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 家公司。
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发布于。