ZendeskからWebhookを受信する統合を構築する場合、インターネットからのHTTPリクエストを受け入れるエンドポイントをサーバー上に開きます。適切な検証がないと、誰でも偽のリクエストをそのエンドポイントに送信し、システム内で不要なアクションをトリガーする可能性があります。そこで、署名検証が重要になります。
Zendesk Webhook署名検証を使用すると、受信Webhookが実際にZendeskから送信されたものであり、転送中に改ざんされていないことを暗号的に証明できます。このガイドでは、正しく実装するために必要なすべての手順を、5つの一般的なプログラミング言語での動作するコード例とともに説明します。
ZendeskでのWebhookの設定に関するより広範なガイダンスをお探しの場合は、ZendeskメッセージングWebhook設定ガイドで完全な構成プロセスについて説明しています。

Webhook署名検証とは何か、そしてなぜ重要なのか
Webhook署名検証は、受信Webhookリクエストの信頼性を確認できるセキュリティメカニズムです。ZendeskがWebhookをエンドポイントに送信すると、Zendeskのみが生成できた暗号署名が含まれます。サーバーは、共有シークレットを使用してその署名を再計算し、結果を比較します。一致する場合、Webhookは本物です。
この検証がないと、エンドポイントはいくつかの攻撃に対して脆弱になります。
- スプーフィング(Spoofing): WebhookのURLを発見した人は誰でも、Zendeskを装って偽のリクエストを送信できます。
- リプレイ攻撃(Replay attacks): 攻撃者は正当なWebhookをキャプチャし、複数回再送信できます。
- ペイロードの改ざん(Payload tampering): リクエストデータは、検出されずに転送中に変更される可能性があります。
機密性の高いチケットデータを処理したり、自動化されたワークフローをトリガーしたりする本番環境での統合では、署名検証はオプションではありません。システムと顧客データの両方を保護する基本的なセキュリティ制御です。
eesel AIでは、Zendeskアカウントを接続すると、Webhookセキュリティが自動的に処理されます。当社のプラットフォームは署名を透過的に検証するため、暗号実装ではなく自動化の構築に集中できます。
Zendesk Webhook署名の仕組み
Zendeskは、SHA256 HMAC(Hash-based Message Authentication Code:ハッシュベースのメッセージ認証コード)アルゴリズムを使用してWebhook署名を生成します。このプロセスでは、Webhookの署名シークレットとリクエストペイロードおよびタイムスタンプを組み合わせて、各リクエストの一意の署名を作成します。
数式は次のようになります。
base64(HMACSHA256(TIMESTAMP + BODY))
ZendeskがWebhookを送信するときに何が起こるかは次のとおりです。
- Zendeskは、タイムスタンプと生のリクエストボディを連結して、単一の文字列にします。
- Webhookの署名シークレットをキーとして使用して、HMAC-SHA256ハッシュを作成します。
- ハッシュはBase64エンコードされ、最終的な署名が生成されます。
- Zendeskは、2つの重要なヘッダーを含むWebhookを送信します。
X-Zendesk-Webhook-Signature- 生成された署名X-Zendesk-Webhook-Signature-Timestamp- 署名で使用されたタイムスタンプ
ZendeskからのすべてのWebhookリクエストには、次の標準ヘッダーが含まれています。
x-zendesk-account-id: 123456
x-zendesk-webhook-id: 01F1KRFQ6BG29CNWFR60NK5FNY
x-zendesk-webhook-invocation-id: 8350205582
x-zendesk-webhook-signature: EiqWE3SXTPQpPulBV6OSuuGziIishZNc1VwNZYqZrHU=
x-zendesk-webhook-signature-timestamp: 2021-03-25T05:09:27Z
サーバーでこれらのヘッダーを抽出し、保存されている署名シークレットを使用して署名を再計算し、結果を比較します。署名が一致する場合、WebhookがZendeskから送信されたものであり、ペイロードが変更されていないことを信頼できます。
Webhook署名シークレットの取得
署名を検証する前に、Webhookの署名シークレットが必要です。Zendeskの各Webhookには、Webhookの作成時に生成される独自のシークレットがあります。
管理センターでシークレットを見つける
- Zendesk管理センター(管理センター > アプリと連携機能 > Webhook)に移動します。
- 検証するWebhookを選択します。
- Webhookの詳細ページで、署名シークレットフィールドを探します。
- 「シークレットを表示」をクリックして、値を表示します。

このシークレットは、他の資格情報と同様に扱ってください。コードにコミットしたり、クライアント側のアプリケーションで公開したりしないでください。また、チーム内でのアクセスを制限してください。
API経由で取得する
Webhook署名シークレットの表示APIを使用して、プログラムで署名シークレットを取得することもできます。
GET /api/v2/webhooks/{webhook_id}/signing_secret
開発用の静的テストシークレット
ZendeskでWebhookを作成する前にWebhookをテストする場合は、実際のシークレットはWebhookの作成後にのみ生成されるため、静的な署名シークレットが必要になります。開発中は、次のテストシークレットを使用してください。
dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==
Webhookが作成されたら、実際の署名シークレットに切り替えます。テストWebhookとライブWebhookは異なるシークレットを使用するため、検証コードは各環境の正しいシークレットを処理する必要があります。

ステップバイステップの実装ガイド
署名検証の実装には、4つの主要なステップがあります。各ステップを分解してみましょう。
ステップ1:生のリクエストボディをキャプチャする
署名は、解析されたJSONやフォームデータではなく、文字列としての生のリクエストボディで計算されます。フレームワークがアクセスする前にボディを解析する場合、生のバイトが変換されているため、署名検証は失敗します。
ほとんどのWebフレームワークは、解析前に生のボディをキャプチャするためのミドルウェアまたは構成オプションを提供しています。通常、署名計算に使用できるように、生のボディをreq.rawBodyのようなプロパティに格納する必要があります。
よくある落とし穴:ボディ解析ミドルウェア(Expressのexpress.json()など)は、多くの場合、ルートハンドラーの前に実行されます。生の文字列をキャプチャする前にボディがJavaScriptオブジェクトに解析される場合、署名検証のために元のバイトを復元することはできません。最初に生のボディをキャプチャするようにミドルウェアを構成してください。
ステップ2:署名ヘッダーを抽出する
受信リクエストから、署名に関連する2つのヘッダーを取得します。
X-Zendesk-Webhook-Signature- 検証する署名X-Zendesk-Webhook-Signature-Timestamp- 署名計算で使用されたタイムスタンプ
一部のフレームワークはヘッダー名を変換することに注意してください。たとえば、Ruby on Railsでは、ヘッダーX-Zendesk-Webhook-Signatureは、リクエスト環境でHTTP_X_ZENDESK_WEBHOOK_SIGNATUREになります。
ステップ3:予期される署名を計算する
タイムスタンプと生のボディを連結し、署名シークレットを使用してHMAC-SHA256ハッシュを作成します。
- 文字列を作成します:
timestamp + body(最初にタイムスタンプ、次に生のボディ) - 署名シークレットをキーとして使用して、HMAC-SHA256を生成します。
- 結果のハッシュをBase64エンコードします。
この計算された署名は、ZendeskがX-Zendesk-Webhook-Signatureヘッダーで送信した署名と一致する必要があります。
ステップ4:署名を安全に比較する
一定時間比較関数を使用して署名を比較します。通常の文字列比較(==または===)は、タイミング分析を通じて署名に関する情報を漏洩させる可能性があり、理論的には攻撃者が有効な署名を偽造するのに役立つ可能性があります。
ほとんどの言語は、一定時間比較関数を提供しています。
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals() - Ruby:
ActiveSupport::SecurityUtils.secure_compare() - C#:組み込みの一定時間比較はありませんが、.NET Coreの
CryptographicOperations.FixedTimeEquals()があります。
署名が一致する場合は、Webhookを処理します。一致しない場合は、401 Unauthorizedレスポンスを返し、調査のために失敗をログに記録します。
一般的な言語でのコード例
最も一般的なWeb開発言語の完全な動作する実装を次に示します。
Node.js/Express
const express = require('express');
const crypto = require('crypto');
const SIGNING_SECRET = 'your_webhook_signing_secret_here';
const PORT = 3000;
const app = express();
// Middleware to capture raw body
function storeRawBody(req, res, buf) {
if (buf && buf.length) {
req.rawBody = buf.toString('utf8');
}
}
app.use(express.json({ verify: storeRawBody }));
app.use(express.urlencoded({ verify: storeRawBody, extended: true }));
function isValidSignature(signature, body, timestamp) {
const hmac = crypto.createHmac('sha256', SIGNING_SECRET);
const sig = hmac.update(timestamp + body).digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(sig)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-zendesk-webhook-signature'];
const timestamp = req.headers['x-zendesk-webhook-signature-timestamp'];
const body = req.rawBody;
if (!isValidSignature(signature, body, timestamp)) {
console.log('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Process the verified webhook
console.log('Webhook verified, processing...');
res.status(200).send('OK');
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Python (Flask)
from flask import Flask, request, abort
import hmac
import hashlib
import base64
app = Flask(__name__)
SIGNING_SECRET = b'your_webhook_signing_secret_here'
@app.route('/webhook', methods=['POST'])
def handle_webhook():
# Get raw body
raw_body = request.get_data()
# Extract headers
signature = request.headers.get('X-Zendesk-Webhook-Signature', '')
timestamp = request.headers.get('X-Zendesk-Webhook-Signature-Timestamp', '')
# Calculate expected signature
signed_payload = (timestamp + raw_body.decode('utf-8')).encode('utf-8')
expected_signature = base64.b64encode(
hmac.new(SIGNING_SECRET, signed_payload, hashlib.sha256).digest()
).decode('utf-8')
# Verify signature
if not hmac.compare_digest(expected_signature, signature):
abort(401)
# Process verified webhook
return '', 200
if __name__ == '__main__':
app.run(port=3000)
PHP
<?php
define('SIGNING_SECRET', 'your_webhook_signing_secret_here');
function verify_webhook($body, $signature, $timestamp) {
// Concatenate timestamp and body
$signed_payload = $timestamp . $body;
// Calculate HMAC (binary output)
$calculated_hmac = base64_encode(
hash_hmac('sha256', $signed_payload, SIGNING_SECRET, true)
);
// Constant-time comparison
return hash_equals($signature, $calculated_hmac);
}
// Handle webhook request
$signature = $_SERVER['HTTP_X_ZENDESK_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_ZENDESK_WEBHOOK_SIGNATURE_TIMESTAMP'] ?? '';
$body = file_get_contents('php://input');
if (!verify_webhook($body, $signature, $timestamp)) {
http_response_code(401);
exit('Unauthorized');
}
// Process verified webhook
http_response_code(200);
echo 'OK';
Ruby on Rails
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
SIGNING_SECRET = ENV['ZENDESK_WEBHOOK_SECRET']
def zendesk
signature = request.headers['HTTP_X_ZENDESK_WEBHOOK_SIGNATURE']
timestamp = request.headers['HTTP_X_ZENDESK_WEBHOOK_SIGNATURE_TIMESTAMP']
body = request.body.read
# Calculate signature
signed_payload = timestamp + body
expected_signature = Base64.strict_encode64(
OpenSSL::HMAC.digest('SHA256', SIGNING_SECRET, signed_payload)
)
# Verify
unless ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
head :unauthorized
return
end
# Process webhook
head :ok
end
end
C#
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
private const string SigningSecret = "your_webhook_signing_secret_here";
[HttpPost]
public IActionResult HandleWebhook()
{
string signature = Request.Headers["X-Zendesk-Webhook-Signature"];
string timestamp = Request.Headers["X-Zendesk-Webhook-Signature-Timestamp"];
// Read raw body
using var reader = new StreamReader(Request.Body);
string body = reader.ReadToEnd();
// Calculate signature
string signedPayload = timestamp + body;
byte[] keyBytes = Encoding.UTF8.GetBytes(SigningSecret);
byte[] payloadBytes = Encoding.UTF8.GetBytes(signedPayload);
using var hmac = new HMACSHA256(keyBytes);
byte[] hash = hmac.ComputeHash(payloadBytes);
string expectedSignature = Convert.ToBase64String(hash);
// Compare (case-insensitive for compatibility)
if (!signature.Equals(expectedSignature, StringComparison.OrdinalIgnoreCase))
{
return Unauthorized();
}
return Ok();
}
}
一般的な問題とトラブルシューティング
正しいコードを使用しても、署名検証は微妙な理由で失敗する可能性があります。開発者が遭遇する最も一般的な問題を次に示します。
JSONのスペーシングとフォーマット
最もイライラする問題の1つは、JSONのフォーマットに関係しています。署名は、Zendeskが送信する正確なバイト(空白を含む)で計算されます。フレームワークがJSONを再フォーマットする(スペースを追加または削除する)場合、署名は一致しません。
Zendeskコミュニティの開発者は、これを痛いほど思い知りました。
解決策は、解析または変換が発生する前に、常に生のリクエストボディに対して署名を検証することです。
テストWebhookとライブWebhookの違い
もう1つの一般的な問題は、ZendeskのテストWebhook機能とライブWebhookの呼び出しの違いに関係しています。ペイロードの形式は2つでわずかに異なる場合があり、テストでは署名が検証されますが、本番環境では失敗します。
本番環境にデプロイする前に、実際のZendeskイベントからの実際のWebhook呼び出しで常にテストしてください。
文字エンコーディング
署名計算のために連結するときは、タイムスタンプとボディの両方がUTF-8文字列として処理されるようにしてください。サーバーとZendeskのペイロード間のエンコーディングの不一致は、検証の失敗を引き起こします。
タイムスタンプの検証
リプレイ攻撃を防ぐために、タイムスタンプの検証を追加することを検討してください。ヘッダーのタイムスタンプが、現在の時刻から妥当な範囲内(たとえば、5分以内)であることを確認してください。古いタイムスタンプは、リプレイ攻撃を示している可能性があります。
シークレットを再生成するタイミング
署名シークレットが侵害された疑いがある場合は、Zendesk管理センターからすぐに再生成してください。再生成後、新しいシークレットでサーバーを更新します。飛行中のWebhookが古いシークレットを使用する短い期間がある可能性があるため、移行中は両方をサポートすることを検討してください。
Webhook検証のテスト
本番環境にデプロイする前に、署名検証の実装を徹底的にテストしてください。
静的テストシークレットの使用
開発中は、Zendeskの静的テストシークレット(dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==)を使用して、実装ロジックが正しいことを確認します。これにより、ライブWebhookを作成せずにテストできます。
Zendeskのテスト機能を使用したテスト
ZendeskでWebhookを作成または編集するときは、「Webhookをテスト」ボタンを使用してテストペイロードを送信します。エンドポイントがこれらのリクエストを受け入れ、署名を正しく検証することを確認してください。
ライブテスト
特定のイベント(チケットの作成など)でWebhookを呼び出す実際のトリガーを作成します。Zendeskでそのアクションを実行し、エンドポイントがWebhookを受信して検証することを確認します。サーバーログで署名の不一致がないか確認してください。
ロギングとデバッグ
開発中に次の情報をログに記録して、失敗のデバッグに役立ててください。
- 生のリクエストボディ(解析前)
- 受信した署名ヘッダー
- 計算された署名
- タイムスタンプヘッダー
署名シークレット自体は絶対にログに記録しないでください。受信した署名と計算された署名を文字ごとに比較して、どこが異なるかを特定します。
eesel AIでZendesk統合を保護する
Webhook署名検証を正しく実装するには、暗号の詳細、フレームワーク固有のボディ処理、およびJSONフォーマットに関するエッジケースに注意を払う必要があります。複雑な統合を構築するチームにとって、この複雑さは開発を遅らせ、セキュリティリスクをもたらす可能性があります。
eesel AIでは、Zendesk統合にWebhookセキュリティを直接組み込んでいます。Zendeskアカウントを当社のプラットフォームに接続すると、署名検証が自動的に処理されます。検証コードを作成および保守することなく、セキュリティ上のメリットが得られます。

Zendesk用AIエージェントはさらに進んで、すべてのWebhookセキュリティをバックグラウンドで処理しながら、自律的なチケット解決を提供します。Webhookベースの自動化を構築していて、暗号実装ではなくビジネスロジックに集中したい場合は、統合を簡素化するお手伝いをします。
よくある質問
この記事を共有

Article by
Stevia Putri
Stevia Putri is a marketing generalist at eesel AI, where she helps turn powerful AI tools into stories that resonate. She’s driven by curiosity, clarity, and the human side of technology.



