Webhook 受信:Narwhal の送信先を設定

HTTPSのURLと、必要な国とファミリーを指定してWebhookの送信先を作成し、各配信は署名を検証してから信頼してください。各アカウントで最大25件の送信先を保持でき、配信1回につき呼び出し1回分の料金がかかります。

送信先を作成して配信を受け取る

Narwhal はデータ API 全体で1つのアカウントと1つの API キーを使い、REST と MCP でも同じキーを共有します。キーは NARWHAL_API_KEY 環境変数に保存し、Authorization: Bearer YOUR_KEY として https://api.narwhalapi.com に送信します。

  1. 送信先を作成する HTTPS URL、国コード、ファミリーのスラッグを POST /v1/webhooks/destinations に送信します。URL は2,048文字まで指定できます。alpha-3 の国コードまたは WLD を1〜60個、ファミリーのスラッグを holidays、predictions、filings などから1〜14個選びます。リクエストには 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 をこのシークレットに設定してください。destination_id は、テスト配信リクエストなど、この送信先を指定するリクエストで使うため保管しておきます。配信署名の計算には返されたシークレットを使います。API キーは API リクエストの authorization ヘッダーに引き続き使います。rotate-secret で返されたシークレットも一度しか表示されません。ローテーションから24時間は、署名ヘッダーに旧シークレットで署名した2つ目の値も含まれます。
  3. 処理する前に署名を検証する 以下のレシーバーは、署名を定数時間で比較し、5分より古いタイムスタンプを拒否します。署名のいずれかが一致すれば配信を受け入れます。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 でエンコードしたシークレットで、小文字の16進 HMAC-SHA256 を計算します。

x-narwhal-signature ヘッダーには sha256=<hex> が入っています。レシーバーと同様に、JSON を解析する前に、計算した値と送られてきた署名を定数時間で比較してください。

Webhook サーバーが停止していた場合は?

配信は再試行され、配信履歴で失敗を確認して再送できます。送信先の履歴は GET .../deliveries で取得し、もう一度受け取りたい配信には POST .../deliveries/{delivery_id}/replay を使います。再送では同じイベントが繰り返されます。

保存した位置から追いつくには、最後のカーソルから GET /v1/events を読みます。このカーソルはジャーナルのパスで使うために保管してください。テスト配信の操作はカーソルを返しません。

同じイベントを2回受け取ることはありますか?

はい。再試行や再送では同じ event_id が繰り返されます。これを冪等キーとして使い、イベントを処理する前に保存してください。本番では、レシーバーのコメントにあるように、保存済み ID にはデータベースのテーブルを使います。

サンプルのレシーバーは ID を seen に保持します。ID がすでにある場合は、イベントを再度表示せずに 200 を返します。未処理の ID なら、その ID を追加してから、イベントの type、country、family を表示します。

計算例:1件の配信を受信する

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

公開。

次のステップ