← 全記事
チュートリアル

Webhook ドキュメントを AI コーディングエージェントに渡したら、動く自動応答ボットができあがった — UnifyPort

おなじみの流れでしょう。ToDo にメッセージ連携のタスクが乗っていて、ドキュメントを最初から最後まで読む代わりに、あなたは Cursor を——あるいは Claude Code や Windsurf を——開いてプロンプトを打ち始める。2026 年、多くの人はこうやってプロジェクトを始めます。本当の問いは「AI で雰囲気コーディング(vibe-coding)するかどうか」ではありません。あなたが相手にする API が、エージェントにタスクをきれいに仕上げさせるだけの材料を与えてくれるかどうか、です。

これはビルドログです。読み終えるころには、動く自動応答ボットが手元にあります。受信メッセージごとに署名を検証し、単一の送信エンドポイントから返信する webhook レシーバ——作業量はおよそひと午後、しかもその大半はエージェントの仕事です。速いのは、UnifyPort の webhook が正規化されているから。学ぶべきイベント構造はひとつ、呼び戻すエンドポイントもひとつ。エージェントが推測すべきものがありません。

準備:まずドキュメントをエージェントに渡す

最も重要なのに、皆が飛ばしがちな一歩——コードを頼む前に、API リファレンスをエージェントのコンテキストへ入れること。いま有効なやり方は三つあります。

  • ページを貼る。 message.received のイベント構造と POST /v1/messages の送信セクションを、そのままチャットに貼り付ける。
  • ドキュメントを添付する。 Cursor の @Docs、Claude Code のファイルコンテキスト、Windsurf のドキュメントパネル——UnifyPort のリファレンスを指して、エージェントが必要に応じて読めるようにする。
  • ドキュメント/MCP コネクタを使う(ツールが対応していれば)。エージェント自身にリファレンスを取りに行かせる。

見返りは具体的です。実際にイベント名を読んだエージェントは、存在しない onMessage() ハンドラや sendText() SDK をでっち上げません。目の前にあるのがそれだから、message.receivedPOST /v1/messages に沿って書きます。ゴミのコンテキストを入れれば、出てくるのは幻覚のエンドポイント——だからその二分を惜しまず、本物を入れましょう。

プロンプトごとに、ボットを組み上げる

最初のプロンプト——レシーバ:

渡した UnifyPort webhook ドキュメントを使って、標準イベントを受け取る POST /webhook ルートを持つ Express サーバを書いて。まずは eventmessage.received のとき、fromtext をログに出すところまで。

イベント構造が固定なので、処理はひとつのフィールドへの分岐になります。

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhook", (req, res) => {
  const evt = req.body;
  if (evt.event === "message.received") {
    console.log(`${evt.provider} ${evt.from}: ${evt.text}`);
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

二つ目のプロンプト——署名を検証する。 ぞんざいな実装は飛ばし、ドキュメントを読んだエージェントは飛ばさない一歩です。

各配信は webhook の signing_secret を使って HMAC-SHA256 で署名されている。ボディを信頼する前に検証して。生のリクエストボディを使い、タイミングセーフな比較で。

ここでエージェントが外せない一点:HMAC は生のバイト列に対して計算する必要があり、再シリアライズした JSON ではありません。「生のリクエストボディを使って」と促せば、良いエージェントは express.jsonverify フックを繋いで rawBody を捕まえます。忘れたら、それがあなたの唯一のレビューポイント——最初の実配信が 401 を返した瞬間に気づきます。

三つ目のプロンプト——送信エンドポイントから返信する:

message.received が届いたら、イベントの account_idfrom を使って POST /v1/messages で送信者に返信して。認証は環境変数の Bearer API キー。

これでボットは完成です。エージェントが落ち着く完全なファイル:

import express from "express";
import crypto from "crypto";

const { UNIFYPORT_API_KEY, UNIFYPORT_SIGNING_SECRET } = process.env;

const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

function isValid(req) {
  const sig = req.get("x-unifyport-signature") || "";
  const expected = crypto
    .createHmac("sha256", UNIFYPORT_SIGNING_SECRET)
    .update(req.rawBody)
    .digest("hex");
  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

app.post("/webhook", async (req, res) => {
  if (!isValid(req)) return res.sendStatus(401);

  const evt = req.body;
  if (evt.event === "message.received") {
    await fetch("https://api.unifyport.ai/v1/messages", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${UNIFYPORT_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        account_id: evt.account_id,
        to: evt.from,
        text: "ありがとうございます!メッセージを受け取りました。担当者より追ってご連絡します。",
      }),
    });
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

動かして、メッセージが届くのを見る

アカウントをひとつ接続し(日本なら LINE を繋ぐのが実運用に直結します)、signing_secretsubscribed_events: ["message.received"] を指定して、自分のサーバを指す webhook エンドポイントを登録し、ボットに DM を送ります。イベントはこう届きます。

{
  "event": "message.received",
  "account_id": "acct_8Q2vK",
  "provider": "line",
  "from": "user_3f9c1a",
  "text": "週末も営業していますか?",
  "timestamp": 1749427200,
  "message_id": "line_msg_5d2b7e"
}

サーバは署名を検証し、POST /v1/messages を呼び、返信がトークに現れます。署名検証が失敗したら 401——それが上の rawBody のバグです。一度直せばループは盤石になります。

拡張する

正規化された webhook の元が取れるのはここです。二つ目のプラットフォームを足すのは、二度目の連携ではなく——ハンドラの新規コードはゼロ。日本で主流の LINE はもちろん、WhatsApp や Zalo のアカウントを接続し、同じ webhook を購読すれば、まったく同じ message.received の分岐が走ります。イベント構造はプロバイダ間で変わらないからです。provider フィールドがどこから来たかを教えてくれて、コードの残りはそれを見もしません。

ボットに「受け取りました」だけでなく、実際に答えさせたい?プロンプトをもう一つ足すだけです。「返信する前に text を LLM に送り、その応答を返信本文に使って。」 送信呼び出しはすでにエージェントが書いています。文字列をモデルの出力に差し替えるだけです。

このひと午後の教訓は「AI がボットを書いてくれる」ではありません。AI エージェントは、あなたが向けた面の良し悪しでしか働けない、ということです。安定したイベント名とひとつの送信エンドポイントを持つ正規化された webhook は、エージェントがきれいに仕上げられるまさにその種の面——学ぶ構造はひとつ、でっち上げるものは何もない。ツールを UnifyPort v1 API に向け、message.received の構造を貼り付ければ、午後が終わる前に検証して返信するボットが手に入ります——あとは残る五つのチャネルに放てばいい。