Elido
2分で読了機能

リンクイベントのためのWebhook:あらゆるデータ形式、あらゆる再試行

URL短縮サービスのイベントに対するWebhook仕様の全容。クリック、コンバージョン、link.created、bio.clickのペイロード形式に加え、再試行ポリシー、署名スキーム、べき等性モデルを解説

Marius Voß
DevRel · edge infra
左側にリンクイベントソース(click、conversion、link.created、bio.click)、中央にWebhook配信サービス、そして購読先エンドポイントへのファンアウトが示されたハブ&スポーク図。再試行の間隔として1秒、30秒、5分、1時間、6時間が注記されている

Webhookは、URL短縮サービスのAPIにおいて、誰もが実装するものの、適切に運用できているケースはほとんどありません。難しいのはエンコーディング(ペイロードは単なるJSONオブジェクトです)ではなく、署名の検証、再試行ポリシー、べき等性、配信保証、そして購読先エンドポイントが2日間ダウンした場合の挙動といった運用面での詳細です。

本稿では、Elidoが発行するすべてのWebhookイベント、そのペイロード形式、再試行曲線、そして署名スキームについて解説します。URL短縮サービス API + SDK クイックスタートではインバウンド側のAPI仕様を扱いましたが、今回はアウトバウンド側を解説します。

12種類のイベントタイプ#

Elidoは12種類のWebhookイベントタイプを発行しており、これらは3つのファミリーに分類されます。

クリックおよびトラフィックイベントclickbio.clickqr.scanconversion。これらは、後述する小さなキュー遅延を経て、リダイレクトやスキャンのたびに発行されます。

ライフサイクルイベントlink.createdlink.updatedlink.deletedbio.published。これらは、API層において基礎となるレコードが変更された時に発行されます。

集計および運用イベントdaily.summarycampaign.endedalert.threshold_exceededquota.warning。これらは、スケジュールに基づいて、またはしきい値を超えた時に発行されます。

購読者は、ターゲットURLと配信を希望するイベントタイプの配列を指定して、POST /v1/webhooks でWebhookを登録します。フルサブスクリプションリクエストは以下の通りです。

POST /v1/webhooks
Content-Type: application/json
Authorization: Bearer <api-key>

{
  "url": "https://example.com/webhooks/elido",
  "events": ["click", "conversion", "link.created"],
  "secret": "whsec_<32-byte-base64>",
  "active": true
}

secretは、送信リクエストの署名に使用されるHMACキーです。これはElido側からはブラックボックスであり、作成リクエストへのレスポンス以降、この値をログに記録したり表示したりすることはありません。

Clickイベントのペイロード#

量の面で最も重要なイベントです。短縮URL経由のすべてのリダイレクトは、クライアントにリダイレクトが提供された後、1つのclickイベントを生成します。形式は以下の通りです。

{
  "id": "evt_2g8kFqJxYwPaZcvAm3HsTr",
  "type": "click",
  "created_at": "2026-05-22T14:32:18.847Z",
  "data": {
    "link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
    "short_url": "https://elido.me/abc123",
    "destination_url": "https://shop.example.com/spring",
    "click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
    "ip_prefix": "203.0.113.0/24",
    "country": "DE",
    "city_geoname_id": 2950159,
    "user_agent_family": "Chrome 124",
    "device_type": "mobile",
    "os_family": "iOS 17.5",
    "referrer": "https://www.google.com",
    "utm_source": "newsletter",
    "utm_medium": "email",
    "utm_campaign": "spring-2026",
    "utm_term": null,
    "utm_content": null
  },
  "workspace_id": "ws_12"
}

いくつか特筆すべき詳細があります。

  • ip_prefixであり、ipではありません。フルIPアドレスではなく、/24(IPv4)または/48(IPv6)のネットワークプレフィックスのみを保持します。URL短縮サービスにおけるGDPRの投稿で解説していますが、これにより、フルIPアドレスに伴う個人情報としての責任を負うことなく、分析に必要な地理的精度を購読者に提供できます。
  • city_geoname_idであり、city_nameではありません。GeoNames IDはロケールに関わらず安定していますが、都市名は変化します。ローカライズされた名前が必要な場合は、GeoNames.orgのダンプに対してIDを一度検索し、結果をキャッシュしてください。
  • user_agent_familyであり、完全なUA文字列ではありません。取り込み時に完全なUA(高エントロピーのフィンガープリントデータ)は削除され、ブラウザ+メジャーバージョンのみが残されます。

リダイレクトがクライアントに提供されてからWebhookが発行されるまでの遅延は、通常200msから2秒です。ClickイベントはまずRedpandaを経由し、分析のために集計された後、ファンアウトワーカによってWebhookとして発行されます。これはダッシュボードの分析を支えるものと同じパイプラインです。Fire-and-forget方式によるクリック取り込みの投稿で、そのキューのメカニズムを解説しています。

Conversionイベントのペイロード#

Conversionイベントは、クリックが下流のコンバージョン(購入、サインアップ、リードフォーム入力など、コンバージョン転送パイプラインに接続されているもの)とマッチングされた時に発行されます。

{
  "id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
  "type": "conversion",
  "created_at": "2026-05-22T14:38:42.193Z",
  "data": {
    "click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
    "link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
    "conversion_id": "cnv_2g8mPzKlYxRcBnQvFt5HwS",
    "value": 49.50,
    "currency": "EUR",
    "event_name": "purchase",
    "product_id": "sku_42",
    "metadata": {
      "order_id": "ord_12345",
      "is_new_customer": true
    },
    "attribution_window_minutes": 6,
    "forwarded_to": ["meta_capi", "ga4_mp"]
  },
  "workspace_id": "ws_12"
}

click_idによって元のClickイベントとリンクされているため、サーバーサイドで両者を結合し、クリックからコンバージョンへのパスを再構築できます。attribution_window_minutesはクリックからコンバージョン発行までの経過時間であり、アトリビューションモデリングに役立ちます。

forwarded_to配列は、Elidoがすでにどのプラットフォームピクセルにこのコンバージョンをプッシュしたかを示しています。もし購読者が自身のデータウェアハウスにコンバージョンを取り込んでいる場合、これを利用して下流の分析で二重計上を避けることができます。

Link.createdイベントのペイロード#

ライフサイクルイベントは、リソースとアクターのみを含むシンプルな形式です。

{
  "id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
  "type": "link.created",
  "created_at": "2026-05-22T14:38:42.193Z",
  "data": {
    "link": {
      "id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
      "slug": "abc123",
      "short_url": "https://elido.me/abc123",
      "destination_url": "https://shop.example.com/spring",
      "domain": "elido.me",
      "tags": ["spring-2026", "newsletter"],
      "created_at": "2026-05-22T14:38:42.193Z",
      "created_by": "usr_42"
    }
  },
  "workspace_id": "ws_12"
}

link.updatedには、新しい状態に加えてpreviousスナップショットが含まれます。link.deletedには、削除時点でのリンクの最終状態が含まれます。完全なスキーマは、運用ガイドの/docs/guides/conversion-forwardingに記載されています。

署名の検証#

すべてのWebhookリクエストには、3つのHTTPヘッダーが含まれます。

Elido-Signature: t=1716392538,v1=4f3b2e1d9c8a7b6f5e4d3c2b1a0f9e8d7c6b5a49382716054 ...
Elido-Webhook-Id: evt_2g8kFqJxYwPaZcvAm3HsTr
Elido-Delivery-Attempt: 1

署名スキームはStripeモデルに従っており、Webhook secretを使用して{timestamp}.{body}に対してHMAC-SHA256計算を行います。v1=プレフィックスは署名アルゴリズムのバージョンです。新しいアルゴリズムバージョンはデフォルトになる前に追加されるため、購読者は複数のバージョンを同時に検証できます。

Goでの検証例:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strings"
    "time"
)

func verify(sigHeader, body, secret string) bool {
    parts := strings.Split(sigHeader, ",")
    var t int64
    var v1 string
    for _, p := range parts {
        kv := strings.SplitN(p, "=", 2)
        if len(kv) != 2 { continue }
        switch kv[0] {
        case "t":
            fmt.Sscanf(kv[1], "%d", &t)
        case "v1":
            v1 = kv[1]
        }
    }
    if time.Since(time.Unix(t, 0)) > 5*time.Minute {
        return false // 古いリクエストを拒否
    }
    mac := hmac.New(sha256.New, []byte(secret))
    fmt.Fprintf(mac, "%d.%s", t, body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(v1))
}

5分間の有効期限チェックは、多くの購読者が忘れがちな点です。これがないと、有効なリクエストをキャプチャして後で再送するリプレイ攻撃が、署名がまだ有効であるため成功してしまいます。タイムスタンプチェックを行うことで、Elidoの発行から5分以内のリクエストのみを受け入れるようになります。

署名仕様はOWASPのWebhookセキュリティに関するチートシートに文書化されています。このパターンは私たちが発明したものではなく、既存の標準を実装したものです。

再試行ポリシー#

これは、ほとんどのWebhook実装において不備が生じやすい部分です。

Webhookは、正常なパス(購読者が2xxを返し、配信サービスが成功を記録し、イベントが完了する)では1回発行されます。難しいのは、2xx以外のレスポンス、ネットワークエラー、そしてレスポンスが遅い購読者が存在する場合です。

Elidoの再試行スケジュール:

試行回数前回の試行からの間隔累積時間ステータス
10初期
21s1s初回再試行
330s31s
45m5m 31s
51h1h 5m 31s
66h7h 5m 31s
724h31h 5m 31s最終

7回目の試行(最初の試行から約31時間後)の後、配信サービスは断念し、内部的にwebhook.failedイベントを発行します。購読先エンドポイントは、イベントに関わらず3回連続で失敗すると「低下(degraded)」状態とマークされます。低下した購読先は、24時間の間、再試行回数が制限されます。50回連続で失敗するとサブスクリプションは一時停止され、ワークスペースのオーナーに通知されます。

再試行動作は、購読者からのRetry-Afterヘッダーを尊重します。もしエンドポイントがElidoに対してレート制限をかけている場合(Retry-After: 120を伴う429を返す)、次回の試行はデフォルトの30秒ではなく120秒待機します。

10秒以内に応答がない場合はタイムアウトとして処理され、失敗した試行としてカウントされます。10秒という予算はサーバーレス環境の購読者のコールドスタート遅延を考慮して意図的に寛容に設定されています。しかし、もしエンドポイントが頻繁に5秒以上かかるようであれば、まずその修正を優先してください。さもないと、再試行のボリュームコストがかさむことになります。

べき等性#

購読者は、同じイベントを複数回受け取ることがあります。

これはバグではありません。分散メッセージ配信における仕組みの結果です。購読者がバックエンドの遅延により504を返したとしても、結果的にイベントを処理できた場合、配信サービスは再試行を行うため、購読者は同じイベントを2回受け取り、2回処理してしまう可能性があります。また、配信サービスが配信中にクラッシュし、イベントが再キューイングされた場合も同じイベントが2回発行される可能性があります。

そのための対策として、すべてのイベントには一意のidevt_…プレフィックス)が付与されています。購読者は、すでに処理済みのIDを保存し(小さなキーバリューストアが有効です。TTL 14日間あれば再試行期間を十分カバーできます)、すでに見たことのあるIDを持つイベントをスキップすべきです。

CREATE TABLE webhook_processed_events (
    event_id TEXT PRIMARY KEY,
    received_at TIMESTAMPTZ DEFAULT now()
);

-- ハンドラ内で以下を実行:
INSERT INTO webhook_processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- RETURNINGの結果が空であれば、そのイベントはすでに処理済みです

ON CONFLICT DO NOTHINGは、安価なべき等性チェックです。INSERTが行を返せばそのイベントは初めて見たものであり、何も返さなければすでに処理済みです。

高スループットな購読者(1秒間に1,000イベント以上)の場合、RedisのSETNXとTTLを使うことで、Postgresの行追加よりも低コストで同じ目的を達成できます。

配信順序#

グローバルな順序保証はありません。同一のlink_idを持つイベントは送信順に配信されますが、異なるリンクからのイベントは順序が混在する可能性があります。T+0秒のclickイベントとT+10msのconversionイベントは、ワーカプールの状態によっては、どちらが先に購読者に届くか分かりません。

created_atタイムスタンプが順序付けの権威となります。もし購読者側で厳密な順序が必要な場合は、処理前にcreated_atでサーバーサイドソートを行ってください。

特にクリックからコンバージョンへのパスにおいては、Conversionイベントは常にClickイベントのclick_idを参照しているため、たとえ順序が前後して届いたとしても、サーバーサイドで結合可能です。

Webhookとポーリング — そのトレードオフ#

クリックトラッキングにおけるWebhookとポーリングで詳しく解説しています。結論を言うと、Webhookが適しているのは(a)イベント到着時に低レイテンシ(5秒未満)が求められる場合、かつ(b)購読者がTLSを利用し、パブリックインターネットから到達可能である場合です。ポーリングが適しているのは(a)リアルタイム性が不要な場合、(b)データウェアハウスを自分で管理しており、日次/時間単位のバッチ取得で十分な場合、(c)購読者がインバウンドトラフィックを受け入れないネットワーク内にある場合です。

ほとんどのチームにとって、Webhookが最適な回答です。再試行曲線が一時的な失敗をエレガントに処理し、署名スキームがセキュリティを確保し、べき等性モデルが配信の重複を処理します。作業は購読者側での「堅牢なハンドラの構築」に集中しますが、そのコストはポーリングベースの取り込みパイプラインを構築するコストに比べればわずかなものです。

運用ツール#

ダッシュボードのWebhookページには、購読ごとに以下の3つが表示されます。

  1. 配信履歴:送信されたすべてのイベント、購読者が返したHTTPステータス、レイテンシ、および次回の再試行予定時間(ある場合)。
  2. リプレイ:イベントごとのリプレイボタン。ハンドラの変更をテストするのに便利です。
  3. テストエンドポイント:サブスクリプションごとに実際のクリックを発生させずに、合成テストイベントを送信するボタン。テストイベントはtype: "test"を持ち、固定ペイロードです。

リプレイ機能とテストエンドポイントはAPIとしても公開されています(POST /v1/webhooks/{id}/events/{evt_id}/replay および POST /v1/webhooks/{id}/test)。

高スループットなデバッグについては、オブザーバビリティガイドで、Webhook配信を自身のメトリクスに接続する方法を解説しています。すべての配信はPrometheusカウンタおよびヒストグラムとしてエクスポートされます。

外部リファレンス#

関連資料#

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン — クレジットカード不要。

タグ
URL短縮 Webhook
リンククリック Webhook
Webhook 再試行
Webhook 署名
Webhook べき等性
イベント配信
Webhook ペイロード

続きを読む