コンテンツにスキップ

LINE follow フロー(welcome reply + HubSpot リード upsert)

LINE 公式アカウントの 友だち追加 / ブロック解除 (= follow event) をトリガーに、webhook-line Lambda が welcome メッセージを返信し、同時に HubSpot にリード (Contact) を upsert する一連の流れを定義する。

実装は aws/lambdas/webhook-line/ 配下。本ドキュメントは「何がどの順で起き、HubSpot にどう書かれるか」を一点集中で説明する。Webhook 受信基盤の一般論は Webhook 受信 (API Gateway + Lambda) を、LINE 全体の機能設計は チャネル:LINE を、HubSpot 連携の前提は HubSpot 連携 を参照。

全体シーケンス

sequenceDiagram
    autonumber
    participant U as 友だち (新規 / ブロック解除)
    participant LP as LINE Platform
    participant APIGW as API Gateway
    participant WHL as webhook-line Lambda
    participant SM as Secrets Manager
    participant S3 as S3 (tenants bucket)
    participant LM as LINE Messaging API
    participant LP2 as LINE Profile API
    participant HS as HubSpot Contacts API

    U->>LP: 友だち追加 / ブロック解除
    LP->>APIGW: POST /line/webhook/{channel_id} (event=follow)
    APIGW->>WHL: invoke

    WHL->>S3: _meta/tenants.json (channel_id → tenant)
    WHL->>SM: /automedia/<tenant>/line (channel_secret, access_token)
    WHL->>WHL: X-Line-Signature 検証

    par welcome reply (≤ 5 秒)
        WHL->>S3: <tenant>/templates/line/welcome.json
        WHL->>LM: Reply API (welcome message)
    and HubSpot リード upsert (非クリティカル)
        WHL->>SM: /automedia/<tenant>/hubspot (access_token)
        WHL->>LP2: GET /v2/bot/profile/{userId}
        WHL->>HS: POST /crm/v3/objects/contacts/search (by line_user_id)
        alt 既存 contact
            WHL->>HS: PATCH /crm/v3/objects/contacts/{id}<br/>line_follow_count++, line_last_followed_at, line_first_followed_at
        else 新規 contact
            WHL->>HS: POST /crm/v3/objects/contacts<br/>line_follow_count=1, lifecyclestage=lead
        end
    end

    WHL-->>APIGW: 200 OK
    APIGW-->>LP: 200

ポイント:

  • welcome reply は最優先。LINE Reply API は replyToken 取得後 約 1 分以内 / 5 秒以内のレスポンスが前提なので、S3 から welcome.json を読んで即送信する
  • HubSpot upsert は welcome reply と並列に走る。HubSpot 側の失敗は welcome reply の成功を阻害しない (handler.ts:safeSyncToHubspot で try/catch + non-fatal ログ)
  • Lambda は最後に必ず 200 を返す。例外で 5xx を返すと LINE が再送を始める

トリガとなる LINE イベント

LINE 上の操作 LINE が送る event 本フローの対象
友だち追加 (新規) follow
ブロック解除 follow (LINE は新規追加と同等の扱い)
ブロック unfollow ❌ (event ack only ログのみ)
メッセージ送信 message ❌ (現状は ack のみ。将来 SQS enqueue 予定)

follow event は新規・ブロック解除のどちらでも発火するため、HubSpot 側で line_follow_countインクリメント する設計にしている (初回作成済みのリードは PATCH、新規のみ POST)。

HubSpot 側で必要なカスタムプロパティ

HubSpot Portal もしくは aws/tools/hubspot-props.mjs事前に作成 しておく。

Property 用途 SoT?
line_user_id string (unique) LINE userId。identity の SoT
line_channel_id string どのチャネル経由で follow したか (multi-OA 対応)
line_display_name string LINE Profile API 取得値
line_picture_url string LINE Profile API 取得値
line_follow_count number follow event 回数 (= 友だち追加 + ブロック解除の合計)
line_first_followed_at datetime 最新 follow タイムスタンプで毎回上書き (履歴は HubSpot property history に残る)
line_last_followed_at datetime 最新 follow タイムスタンプ

line_first_followed_at を毎回上書きする理由 (PR #97): LINE では「友だち追加 → ブロック → 解除」を繰り返した場合、毎回が新規 follow と等価。「最初に追加された日時」を厳密に保持したい場合は HubSpot の property history で過去値を辿れるため、アプリケーション側で保護コードを書く必要はないと判断した。

Contact identity の SoTline_user_id

email を一意キーにすると、初期実装で使っていた合成 email (<userId>@line.local) を後で <userId>@line.spin-dd.com に変更した際 (PR #93) に旧 contact と新 contact が分裂する問題が発生する。そのため identity の真実は line_user_id (HubSpot 側で unique 設定済) とし、search-first で既存判定する (PR #94)。

// hubspot.ts (要約)
const existing = await searchByLineUserId(opts.userId, headers);
if (existing) {
  // PATCH: count++, first/last_followed_at, display_name, picture_url
} else {
  // POST: count=1, lifecyclestage=lead
}

email は <userId>@line.spin-dd.com を seed として書くが、これは HubSpot のバリデーション (空 email 不可 / 一意 TLD 必須) を通すための合成値であり、実在する SPF/MX は持たない

なぜ .spin-dd.com サブドメインか

.local は HubSpot に invalid TLD として弾かれる (INVALID_EMAIL)。実 TLD のサブドメインを使えば構文チェックを通過する。実在 MX は不要。

Welcome テンプレートの設置

S3 に テナント別に配置:

s3://spin-dd-automedia-tenants/<tenant>/templates/line/welcome.json

形式は LINE Messaging API の messages 配列をそのまま JSON 化したもの:

{
  "messages": [
    {
      "type": "text",
      "text": "spin-dd 公式 LINE へようこそ!\nWeb サイト最新情報や新着コラムをお届けします。"
    }
  ]
}

text / sticker / image 等、LINE Messaging API がサポートする message type は WelcomeTemplate 型 (webhook-line/src/types.ts) で受け付ける。

Webhook redelivery (再送) の扱い

LINE Webhook は配信失敗判定時に 約 1 分後に同一 event を再配信 することがあり、ペイロードには deliveryContext.isRedelivery: true が入る。replyToken は 1 回限り消費なので、再送時に同じトークンで Reply API を呼ぶと 400 Invalid reply token になる。

そのため webhook-line Lambda は events ループの先頭で redelivery を skip する (PR #102):

if (ev.deliveryContext?.isRedelivery) {
  console.log('skip redelivered event', { tenant, type, userId, webhookEventId });
  return;
}

副作用:

  • 初回の welcome reply は既に成功している前提で skip する
  • HubSpot upsert も skip するが、search-first 構造 (PR #94) のため初回が成功していれば冪等。仮に初回が失敗していて再送だけ成功してもリードは作られる (= データ的に許容)。次回 follow で確実に upsert される

CloudWatch ログでの観測:

04:29:17  webhook-line received { body_len: 357 }     ← 初回
04:29:18  follow welcome sent { tenant, userId }
04:30:19  webhook-line received { body_len: 356 }     ← 再送 (isRedelivery:true)
04:30:20  skip redelivered event { tenant, type, userId, webhookEventId }

body_len の 1 バイト差は "isRedelivery":false (20 字) → "isRedelivery":true (19 字) の差分。

失敗ハンドリング

失敗箇所 影響 Lambda の挙動
channel_id 未知 (tenants.json にない) welcome 送れない 404 を返す (LINE は再送)
署名検証失敗 受け取らない 401 を返す
welcome.json 取得失敗 welcome 送れない 例外 → outer try/catch で 200 にして再送防止
LINE Reply API 失敗 welcome 送れない 例外 → 200 で受け切る
HubSpot Secret 不在 HubSpot 同期 skip placeholder access_token を検知してログのみ
HubSpot 各 API 失敗 リード作成・更新されず non-fatal: welcome reply に影響させない。エラーログのみ
LINE Profile API 失敗 displayName/pictureUrl 欠落 displayName/pictureUrl なしで HubSpot upsert を続行

設計方針:

  • LINE には常に 200 を返す。5xx は LINE 側で再送ループになり、CloudWatch のエラーが増える
  • HubSpot は best-effort。LINE 体験 (welcome 即受領) を優先

テナント設定

場所 中身
s3://spin-dd-automedia-tenants/_meta/tenants.json { by_line_channel: { "<channelId>": "<tenant>" } }
Secrets Manager /automedia/<tenant>/line { channel_id, channel_secret, access_token }
Secrets Manager /automedia/<tenant>/hubspot { access_token } (Private App Token / Service Key)
S3 <tenant>/templates/line/welcome.json welcome message テンプレ

新テナントを追加する場合は チャネル オンボーディング を参照。

動作確認方法

  1. LINE 公式アカウントでブロック → 解除する (= follow event 発火)
  2. CloudWatch Logs /aws/lambda/automedia-webhook-line で以下のログが出ることを確認:
    • webhook-line received { channelId, body_len }
    • follow welcome sent { tenant, userId }
    • hubspot lead synced { tenant, userId, created, followCount }
  3. 約 62 秒後の再送ログ:
    • skip redelivered event { tenant, type: 'follow', webhookEventId }
  4. HubSpot Portal もしくは API で contact を確認:
    • line_follow_count がインクリメントされている
    • line_last_followed_at が follow タイムスタンプに更新されている
    • lifecyclestage は変更されない (人手昇格を保護)

関連 PR / Issue

番号 内容
#79 / #98 secret-watch Lambda (LINE/HubSpot トークンの期限切れ警告)
#92 follow を count + first/last timestamp で追跡
#93 HubSpot 合成 email を .local.spin-dd.com サブドメインに
#94 HubSpot upsert を line_user_id search-first に変更
#97 line_first_followed_at を follow 毎に上書きするように変更
#101 / #102 LINE Webhook redelivery を skip して Invalid reply token を解消