# LINEアフィリ セットアップ手順書（AI向け）

> **あなた（AI）への依頼**: この手順書と APIキー（`lac_…`）の2つだけで、このアカウントの設定を終わらせてください。
> 管理画面は開けなくて構いません。必要な値はすべて API が返します。
> 人にしか出来ない作業は、この手順書が「人に頼む」と明記しています。それ以外はあなたが進めてください。

- Base URL: `https://line-affi.com`
- 認証: 全リクエストに `Authorization: Bearer $LINE_AFFI_API_KEY`
- `lac_…` キーは持ち主のアカウントに固定されます。**`ownerId` は送らなくてよい**
- 成功は `{ "success": true, … }`、失敗は `{ "success": false, "error": "…", "code": "…" }`。`error` に直し方が書いてあります
- 本文の形がおかしいときは、どの項目が悪いかを `400` で返します: `INVALID_JSON`（JSON として読めない）／`INVALID_BODY`（オブジェクトでない）／`INVALID_TYPE`（例: `name は文字列で指定してください`）／`VALUE_TOO_LONG`（通常の項目は2,000文字、`…Html`・`…Body` は30万文字、案件名200・slug 64 など）／`INVALID_CHARACTER`。`error` の文を読んで直して再送してください
- 作成系は同じ内容で再実行しても二重になりません（タイムアウトしたら、そのまま再送してよい）
- `dryRun`（試し実行）が使えるのは `POST /api/bot/line/connect`・`POST /api/bot/affiliates`・`PUT /api/bot/settings/email`・`POST /api/bot/line/recontact`・`POST /api/bot/affiliates/announce` だけです。ほかの書き込み系に `dryRun` を付けると `400 DRY_RUN_UNSUPPORTED` で止まります（実行はされません）
- APIキーは環境変数で受け取り、LP の HTML/JS やリポジトリに書かないこと

## つなぎ方（3通り。いちばん簡単なのは AI社員）

キーは管理画面「設定 → APIキー」で発行します（`lac_…`）。キーに許した操作だけが、AIエージェントに使えます。まるごと任せる（丸投げ）なら、発行画面の「許可する操作」をすべてにチェック。機能を制限したいなら、任せたい部分にだけチェックを入れます。

**① AI社員（EGO）　いちばん簡単**
AI社員のお客様専用ページ（my.aiagentego.com）→「できることを増やす」→「紹介で売れる仕組み（LINE アフィリセンター）」の欄に、発行したキーを貼って「保存する」。URL も設定ファイルも要りません。保存すると、社員に自動で反映されます（1〜2分）。あとは「新しい案件を登録して」「紹介リンクを発行して」と頼むだけ。

**② Claude Code**
```bash
claude mcp add --scope user --transport http lineaffi https://line-affi.com/api/mcp --header "Authorization: Bearer lac_…"
```

**③ Codex**
```bash
echo 'export LINEAFFI_API_KEY="lac_…"' >> ~/.zshrc && source ~/.zshrc
codex mcp add lineaffi --url https://line-affi.com/api/mcp --bearer-token-env-var LINEAFFI_API_KEY
```

②・③は、「接続の設定」に次の2つを **1回だけ** 入れる、という意味です。あとは日本語で頼むだけで、依頼のたびにキーを渡す必要はありません。

| 入れるもの | 値 |
|---|---|
| URL | `https://line-affi.com/api/mcp` |
| 認証 | `Authorization: Bearer lac_…` |

- 使える道具は、キーに許した操作の範囲だけが一覧に出ます。まず `get_account` を呼ぶと、何が任されているか・次にやることが分かります。
- MCP の道具は、下の REST と同じ操作を、同じキーで呼ぶだけです。できること・触れる範囲は REST とまったく同じです（そのアカウントの中だけ）。
- いまの道具: `get_account`・`list_offers`・`get_offer`・`create_offer`・`update_offer`・`list_stepmail_sequences`・`create_stepmail_sequence`・`add_stepmail_step`・`get_affiliate_rewards_report`・`get_line_status`・`list_line_channels`・`search_customers`・`stop_customer_sequences`（配信を止める。customers:write が要る）。
- **MCP からはできないこと**（人が管理画面で行うか、下の REST を使う）: 報酬額の変更・アフィリエイターへの公開・LINE の認証情報の登録・一斉メール・クーポンの発行。お金が動く・人にメールが飛ぶ操作だからです。
- 「接続の設定」が無いAIエージェント・スクリプト・外部ツール（Zapier など）は、REST で、下の手順書のとおり `Authorization: Bearer $LINE_AFFI_API_KEY` を付けて `https://line-affi.com/api/bot/…` を呼びます。

## 0. 最初に必ず呼ぶ: `GET /api/bot/account`

```bash
curl -s https://line-affi.com/api/bot/account -H "Authorization: Bearer $LINE_AFFI_API_KEY"
```

| 返るもの | 使い道 |
|---|---|
| `owner` | 誰のアカウントか。作業前に人に名前を見せて確認する |
| `apiKey.scopes` / `apiKey.notGranted` | このキーで出来る操作 / 出来ない操作（下の「権限」） |
| `plan` | プラン、トライアルの残り日数、上限の残り（`limits.offers.remaining` など。`null` は無制限） |
| `setup` | メール（ステップメール用）・案件・アフィリエイター・決済・LINE が設定済みか（`emailVerified` はテスト送信まで済んだか） |
| `offers[]` | 案件ごとの状態（LINE連携、LIFF、報酬の種類と金額、紹介リンクの数、公開済みか、直接紹介リンクの設定 `referral`） |
| `urls` | アフィリエイターの登録ページ、各 webhook の URL |
| **`next[]`** | **次にやること。上から順に片付ける** |

`next[]` の各項目:
- `how` — 呼ぶ API、または人に頼む内容
- `needsHuman: true` — 人の作業か、人の了承が要る。勝手に進めない
- `missingScopes` — 空でなければ、このキーでは出来ない。人に「管理画面 → 設定 → APIキー で、このキーの『権限を変更』からこの権限を付けてほしい」と頼む（キーは変わらないので、渡し直してもらう必要はありません）
- `required: false` — その機能を使う場合だけ

一手終えるたびに `/account` を呼び直すと、`next[]` が更新されます。`next[]` が空になれば設定は完了です。

### トライアルと上限
- 登録から30日間は Standard 相当（案件30・アフィリエイター3000・ステップメール等が使える）
- 切れて**24時間たってもお支払いが無いと、管理画面と API が止まります**（`403 ACCOUNT_SUSPENDED`。データは消えず、お支払いの登録で続きから使える）。`plan.trial.daysLeft` が少なければ、人にお支払いの登録を勧める。終了の7日前・3日前・1日前に、運営者へ案内メールが届く
- 上限に当たると `403 PLAN_LIMIT_REACHED`（`current` / `max` / `plan` つき）。プランの変更は人が管理画面で行う

## 権限（このキーに何が任されているか）

権限が足りない操作は `403 INSUFFICIENT_SCOPE` と `missingScopes` が返ります。**エラーではなく「そこは任されていない」という意味**です。
人に権限の追加を頼むか、その手順を人に引き渡してください。回避しようとしないこと。

| 権限 | 出来ること | 既定 |
|---|---|---|
| `offers:read` | 案件を見る — 案件の一覧・設定内容・LINE連携の診断結果 | オン |
| `offers:write` | 案件を作る・編集する — 案件の作成、LPのURL・文面・サンクスページの変更、アップセルの設定 | オン |
| `stats:read` | 実績を見る — クリック数・登録数・ステップメールの配信実績（個人情報は含まない） | オン |
| `conversions:write` | 外部の購入を計上する — UTAGE など外部で決済された購入を登録し、紹介報酬を発生させる | オン |
| `rewards:write` ⚠ | 報酬額を変える — 登録報酬・購入報酬の率、ランク別報酬、ランクの作成 | オフ |
| `offers:publish` ⚠ | 案件をアフィリエイターに公開する — 公開すると全アフィリエイターに通知メールが飛び、取り消せません | オフ |
| `affiliates:read` ⚠ | アフィリエイターを見る — 氏名・メールアドレス・電話番号・報酬額を含む一覧 | オフ |
| `affiliates:announce` ⚠ | アフィリエイターにお知らせメールを送る — 有効なアフィリエイター全員（または案件の参加者）に、アカウントの差出人から一斉にメールを送る。ポータルのお知らせにも出せる | オフ |
| `affiliates:write` ⚠ | アフィリエイターを登録・変更する — 登録、ランク・紹介元の変更、稼働停止、紹介リンクの発行 | オフ |
| `leads:write` ⚠ | リード（登録者）を取り込む — メール登録者の一括取り込み。会員権限の付与・ステップメールの配信開始・報酬の発生を伴う | オフ |
| `customers:read` ⚠ | 登録者・購読者を見る — メールアドレスを含む購読者の一覧 | オフ |
| `customers:write` ⚠ | 顧客の登録内容を直す — お客様の登録メールアドレス・名前の修正と、会員サイトのログインメールの送り直し（問い合わせ対応用）。メールアドレスを変えると、その人のログイン先が変わります | オフ |
| `stepmail:write` ⚠ | ステップメールを作る・編集する — 実際に配信されるメールの作成・書き換え・削除 | オフ |
| `email:settings` ⚠ | メール配信を設定する — メール配信サービスのAPIキー・送信元アドレスの登録と削除、テスト送信。ステップメールが、この設定の送信元から読者に送られます | オフ |
| `line:connect` ⚠ | LINEを連携する — チャネルの認証情報の登録、LIFFアプリの作成、Webhook の設定、予備アカウント（LINEバン対策）の登録 | オフ |
| `referral:read` | LINE の友だちの紹介者を調べる — LINE の友だちごとに、誰の紹介か（計測ID）と、紹介者が登録した提携先の紹介リンクを調べる。外部の配信システム（LINE など）が、人ごとに正しい紹介リンクを送るのに使う | オフ |
| `purchase:sign` ⚠ | 値段の段の登録日署名を発行する — 登録（友だち追加）の時刻に署名を付け、値段が日数で上がる案件の購入ページで、その日から数えた段を開けるようにする。外部の配信システム（LINE など）がリンクを作るのに使う | オフ |
| `purchase:read` | LINE の友だちが買ったかを調べる — LINE の友だちごとに、案件グループの商品を買ったことがあるか（返金済みも含む）と最後の購入日時だけを調べる。外部の配信システム（LINE など）が、買った人をカゴ落ちの追いかけから外すのに使う | オフ |
| `line:associate` ⚠ | 外部の LINE ログインで紹介元を結びつける — 自社の LINE ログイン（LIFF）で本人確認した人を、紹介リンクのクリックに結びつける。友だち追加で紹介報酬が確定します | オフ |
| `line:recontact` ⚠ | 予備アカウントへの再接触メールを送る — LINEで連絡できなくなった顧客全員に、予備アカウントの友だち追加を案内するメールを一斉に送る | オフ |
| `autoreply:write` ⚠ | LINEの自動応答を設定する — キーワードに反応して、友だちにLINEメッセージを自動で返すルールの作成・変更・削除 | オフ |
| `webinars:write` ⚠ | ウェビナーを作る・編集する — オートウェビナーの作成・開催スケジュール、チャットの台本とAI返信、視聴時間に応じたLINEの自動メッセージ（視聴者にLINEが届きます） | オフ |
| `consultations:write` | 個別相談の予約枠を作る・編集する — 受付ルール・ミーティングURL・確認メールの文面・担当者（Google カレンダーの選択）の設定、予約のキャンセル | オン |
| `coupons:write` ⚠ | クーポンを発行する — 割引クーポンの発行・停止・削除（お客様の支払額が変わります） | オフ |
| `videos:write` ⚠ | 動画を登録する・視聴連動を設定する — 動画のアップロード、視聴率に応じたタグ付け・ステップメール開始のルール（メールが届き始めます） | オフ |
| `subscriptions:write` ⚠ | 月額の契約を解約する — お客様の月額の契約（販売者の PAY.JP の定期課金）を解約する。次の更新日から請求が止まります | オフ |
| `members:write` | 会員サイトの教材・会員を編集する — 会員サイトの章立てと教材（動画・本文）の作成・並べ替え・削除、会員の手での追加・取り外し（追加するときに案内メールを送ることもできます） | オン |

⚠ の権限は、お金が動く／人にメッセージが飛ぶ／個人情報が読める／元に戻せない、のいずれかです。
持っていても、実行前に人へ内容を要約して了承を得てください（特に報酬額の変更と、案件の公開）。

## 1. 最初に人へ聞くこと（6問）

1. 何を紹介してもらいますか？ 無料登録（`OPTIN`）か、有料商品の販売（`BACKEND`・価格）か
   - あわせて「**申し込みはどこで受けますか？**」も聞く。このシステム（購入ページ・フォーム）か、**外部のサービス**（提携先のアフィリエイトプログラム、紹介制度のあるサービスなど）か。
     外部なら「外部サービスへの直接紹介リンク」の章へ（アフィリエイターごとの紹介リンクに直接つなぐ。報酬の計算はこのシステムでは行わない）
2. 登録は LINE・メール・両方のどれで受けますか？（`captureMode`: `LINE` / `EMAIL` / `LINE_EMAIL`）
3. 紹介報酬はいくらですか？（LINE登録1件○円、メール登録1件○円、購入は売上の○%）
4. LP はありますか？ URL は？（無ければシステム内蔵の LP を使える）
5. LINE 公式アカウントを、他の配信ツール（Lステップ等）でも使っていますか？ ← **必ず聞く**（手順3で webhook を壊さないため）
6. 申し込みを外部のサービスで受ける場合: そのサービス名と、あなた自身の紹介リンクは？（アフィリエイターが登録するリンクの形も確認する）

## 2. 案件を作る `POST /api/bot/offers`

```bash
curl -s -X POST https://line-affi.com/api/bot/offers \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"AI副業3ステップ","slug":"ai-fukugyo-3step","offerType":"OPTIN","groupTag":"ai-fukugyo","captureMode":"LINE","lineFollowReward":500}'
```

| 項目 | 必須 | 内容 |
|---|---|---|
| `name` | ✔ | 案件名（アフィリエイターに見える） |
| `slug` | ✔ | 小文字英数とハイフン。**全体で一意・後から変更不可**。重複は `409 DUPLICATE_SLUG` |
| `offerType` | ✔ | `OPTIN` / `BACKEND`。**後から変更不可** |
| `groupTag` | ✔ | 案件グループ。オプトと販売は同じ名前にする。公開日時はグループで1つ |
| `price` | BACKEND のみ ✔ | 円（月額の商品なら1か月の金額） |
| `billingType` | | `ONE_TIME`（1回払い・既定）/ `MONTHLY`（月額）。月額は下の「月額の商品」を読む |
| `trialDays` | | 月額だけ: 最初の◯日間は無料（1〜365。下の「無料お試し」） |
| `renewalRewardOfferId` | | 月額だけ: 2回目以降の毎月の報酬を計算する案件（要 `rewards:write`） |
| `fulfillmentWebhookUrl` / `fulfillmentPlanKey` | | 外部のサービスへの知らせ（下の「外部のサービスへの知らせ」） |
| `captureMode` | | `LINE`（既定）/ `EMAIL` / `LINE_EMAIL` |
| `lineFollowReward` / `emailOptinReward` | | 無料登録の紹介報酬（円）。要 `rewards:write` |
| `purchasePercent` | | 購入の紹介報酬（%）。要 `rewards:write` |
| `externalLpUrl` | | LP の公開 URL。後からでよい |
| `showToAffiliates` | | **テストが済むまで false（既定）のまま** |
| `launchAt` | | 公開日時。**新しい `groupTag` のときだけ**（そのグループの公開日時になる。無ければ 2099-01-01）。既にあるグループでは送らない（`400 LAUNCH_AT_IS_PER_GROUP`。変えるのは `PATCH /api/bot/offer-groups`。下の「9. 公開する」） |

レスポンス: `{ success, ownerId, offerId, slug, urls, integration }`。既存の案件は `GET /api/bot/offers`（一覧）、`GET /api/bot/offers/{id}`（詳細）。

## 3. LINE を連携する（`captureMode` が `LINE` / `LINE_EMAIL` のとき）

友だち追加を紹介者に紐づけるには、LIFF（本人確認）と webhook（友だち追加の検知）が要ります。設定は API が行います。
**人に頼むのは、LINE 側に API が無い次の作業だけです。**

### 人に頼むこと（LINE Developers コンソール）
1. **同じプロバイダーの中に**チャネルを2つ用意する: 「Messaging API」（公式アカウント）と「LINEログイン」。
   別プロバイダーだと同じ人でも userId が変わり、本人確認と友だち追加を突き合わせられません（報酬が1件も付かなくなる）
2. LINEログインのチャネル → チャネル基本設定 → 「リンクされたLINE公式アカウント」に、その公式アカウントを設定
3. LINEログインのチャネルを「開発中」から **「公開」** にする（開発中のままだと管理者以外は開けない）
4. 2つのチャネルそれぞれの **チャネルID と チャネルシークレット**、公式アカウントの @ID（例 `@971hyhga`）を受け取る

### 一括設定 `POST /api/bot/line/connect`（要 `line:connect`）
```bash
curl -s -X POST https://line-affi.com/api/bot/line/connect \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"offerId":"…","messaging":{"channelId":"…","channelSecret":"…"},"login":{"channelId":"…","channelSecret":"…"},"expectBasicId":"@971hyhga","dryRun":true}'
```
- **必ず先に `dryRun:true`**。`account.displayName` が意図した公式アカウントかを人に確認してから、`dryRun` を外して本実行。
  公式アカウントを複数持つ人は、別アカウントの値を渡してくることがあります。`expectBasicId` を付ければ、違えば `WRONG_ACCOUNT` で止まります
- やること: 認証情報の検証 / LIFF アプリの作成（あれば再利用。Scope は `openid` `profile`）/ webhook URL の設定 / 案件への保存。`login.channelSecret` は保存されません
- 作成済みの LIFF を使うなら、`login` の代わりに `"liffId":"1234567890-AbCdEfGh"`
- レスポンスの `todo` が、人に残った手作業です

### リッチメニューなどのボタンを、誰が押したか記録する

リッチメニューのボタンは全員に同じ URL を配るので、配信システムの計測リンクでは「誰が」押したかが分かりません。本人確認をする LIFF の URL の `state` の末尾に **`~{ボタン名}`**（英数字・`_`・`-`、32文字まで）を足すと、押した人ごとに記録します。行き先・視聴の記録・紹介元は、印を付けても変わりません。

| 置く URL | 行き先 |
|---|---|
| `https://liff.line.me/{LIFF ID}?state=watch:{slug}~menu_watch` | 視聴ページ（今までどおり） |
| `https://liff.line.me/{LIFF ID}?state=special:{slug}~menu_special` | 特別ページ（今までどおり） |
| `https://liff.line.me/{LIFF ID}?state=go:{slug}~line_gift` | 案件の `menuLinks` の `line_gift` の URL（LIFF ではない外部ページ用。紹介元 `clickId` を付けて移す） |

- 外部ページは、先に `PATCH /api/bot/offers/{id}` で `{"menuLinks":{"line_gift":"https://example.com/gift/"}}` を入れます（10個まで）
- 記録を見る: `GET /api/bot/menu-taps?ownerId=…&slug={slug}`（権限 `customers:read`）→ ボタンごとの回数・人数と、1件ずつ（LINE のユーザー ID・ボタン・時刻）
- 全自動AI集客システムと連携中のアカウントは、押されるたびに配信システムへも知らせます（`event: "menu_tap"`・`button`・`at`）
- 同じメニューの「文を送るボタン」（メッセージ）は、配信システム側に記録が残るので、この印は要りません

### 登録した後の行き先（2つの欄を混ぜない）
- **`noPaypal`（既定 false）**: 販売の案件の購入ページに PayPal を出しません（カードと銀行振込は出ます）。`PATCH /api/bot/offers/{id}` に `{"noPaypal":true}`。カードだけにするなら下の `cardOnly`
- **期間限定の特典の講座**（`limitedBonusOfferIds`・`limitedBonusDays`・`limitedBonusFromSlugs`。`PATCH /api/bot/offers/{id}`）: 「前の商品を買ってから◯日のうちにこの商品を買った人にだけ、特典の講座を付ける」設定です。例: `{"limitedBonusOfferIds":["<超整理術講座の案件ID>"],"limitedBonusDays":7,"limitedBonusFromSlugs":["kap2-package"]}`。日数は日本時間の暦日で、買った日が1日目（7なら7日目の23:59まで）。条件の案件を複数回買っている人は、いちばん最近の購入から数えます。起点は、案件の購入（`limitedBonusFromSlugs`）または、ステップメールに登録した日（`limitedBonusFromSequenceIds`＝シナリオの ID）のどちらでも指定できます（どちらかが期間内なら対象）。ステップメールを起点にして `limitedBonusDays` を空（null）にすると、そのシナリオの最終のメールの日（シナリオに期限 `deadlineDays` があればその日）の23:59までが期間になります。メールの日数を変えても、特典の期間が自動で追いつきます（「最終のメールで本日締め切りと送る」ときに向く）。期間を過ぎてから買った人には特典の講座だけが付きません（案件本体は同じ値段で買えます）。付いた権利の元はこの案件で、返金すると通常の特典と同じく外れます。KAP などへの知らせ（`/purchase` と fulfillment の `payment`）には、対象の購入のとき `limitedBonus: true` が付きます。通常の特典（`bonusOfferIds`）とは別の設定です
- **`cardOnly`（既定 false）**: 販売の案件の購入ページを、カード（PAY.JP）だけにします（PayPal・銀行振込を出さず、直接叩かれても受けません）。カードの購入のときだけ外部のアプリにアカウントを作らせる商品などに使います。`PATCH /api/bot/offers/{id}` に `{"cardOnly":true}`。アカウントに PAY.JP の設定が無いと「決済方法が設定されていません」になるので、先に確かめてください
- **`purchaseThankYouUrl`＝購入が終わった後に移るページ**（販売の案件。次の商品の案内ページなど。`PATCH /api/bot/offers/{id}`）。入れると、カード・PayPal・分割払いの決済が終わった直後に、そのページへ `?purchased={買った案件のslug}&clickId={紹介元}&lref={LINEの印}&mt={会員サイトに入る印}` を付けて移します（clickId・lref は来た人にだけ付く）。そのページの「次の商品を買う」ボタンは、`clickId` と `lref` をそのまま次の購入ページへ渡してください（紹介報酬と LINE の切り替えが続きます）。会員サイトへの入口は `https://line-affi.com/members/{purchased}?t={mt}`。空なら、今までどおり購入完了の画面です。アップセルの設定がある案件は、アップセルの画面が先に出ます
- **`thankYouExternalUrl`＝メール登録の後に移るページ**（自社のサンキューページ。例 `https://example.com/thankyou.html`）。ここに LINE のURLは入れません
- **`lineAddFriendUrl`＝LINE で本人確認（LIFF）した後に移る友だち追加URL**（`https://lin.ee/…` や `https://line.me/R/ti/p/@…`）。`line/connect` が空のときだけ公式アカウントの友だち追加URLを入れます。空でも、連携した公式アカウントの友だち追加画面になります
- 当システムの LP（`externalLpUrl` なし）で「メール登録 → そのまま LINE 追加」にしたいときだけ、`thankYouExternalUrl` に友だち追加URLを入れます（本人確認を通してから友だち追加に進みます）

### ⚠ 他の配信ツールが webhook を使っている場合（`webhook.plan` が `"relay"`）
LINE の webhook URL は1チャネルに1つだけです。Lステップ等が既に受けているなら、**API は上書きしません**（上書きすると、その配信が止まります）。
**人にも、LINE Developers の Webhook URL を書き換えさせないでください。**
代わりに `relay.url` と `relay.instructions` が返ります。その配信システムの側に、転送を1か所足してもらいます:
- そのシステムの署名検証を通した**後**で、受け取った本文を **1バイトも変えず**、`x-line-signature` ヘッダごと `relay.url` へ POST
- `JSON.parse` → `stringify` し直すと署名が合わず 401。`?noreply=1` は必ず付ける
- 転送を足せないツールの場合は、そのことを人に伝える（紹介専用の公式アカウントを分けるのが確実）

### 診断 `GET /api/bot/line/status?offerId=…`
「報酬が付かない」ときは、まずこれを呼びます。段ごとに `ok` と、直し方 `fix` が返ります。

| key | NG のとき |
|---|---|
| `liff_id` | LIFF 未設定 → connect を実行 |
| `messaging_credentials` | ID と secret の組が LINE に拒否された → 正しい値で connect をやり直す |
| `webhook_endpoint` | URL 未設定、または「Webhookの利用」がオフ |
| `webhook_received` | 一度も届いていない／署名不一致で拒否している（登録した secret が別アカウントのもの） |
| `funnel` | 本人確認は出来ているのに確定が0 → webhook 未着、またはチャネルが別プロバイダー |

`manualChecks`（公開状態・公式アカウントのリンク）は API から見えないので、人に目視してもらいます。

### 値段の段の署名を外部の配信に渡す `GET /api/bot/purchase/sub-sig?ms=…`（要 `purchase:sign` ⚠）
値段が「登録から何日目か」で上がる案件（`groupTag` と `tierFromDay`）の購入ページは、`?sub={ms}&sig={sig}` の署名を確かめて段を決めます（署名が無い・合わないと一番高い段）。メールのステップには Line-Affi が自動で差し込みますが、**LINE など外部の配信システムが送るメッセージ**には、そのシステムがこの API で署名をもらってリンクを作ります。
```bash
curl -s "https://line-affi.com/api/bot/purchase/sub-sig?ms=1790712456392" -H "Authorization: Bearer $KEY"
# → { "success": true, "ms": "1790712456392", "sig": "…22文字…" }
```
- **LINE とメールの両方にいる人は、起点をそろえる**: `&lineUserId=U…` を付けると、その人がメールでも登録していて、メールの起点（ステップメールの開始時刻）の方が `ms` より早ければ、そちらで署名し `source:"email"` を返します（友だち追加が先なら LINE の起点のまま。あとからメール登録しても安い段に戻りません）。見つからなければ `ms`（友だち追加）で署名し `source:"line"`。特定のシナリオの開始時刻に合わせるなら `&sequenceId=…`。同じ人と分かるのは、顧客の記録に LINE とメールの両方がある人、または LINE の本人確認とメール登録が同じ紹介クリックの人です
- `ms` は、その人の日数を数え始めた時刻（epoch ミリ秒）。友だち追加し直した人がいるので、配信の日数の起点と同じ時刻にそろえます（文面の Day N と段が一致する）
- 未来の時刻は `400 FUTURE_MS`。同じ `ms` の結果は変わらないので、送る側で覚えておけば呼び直し不要（`lineUserId` を付けた場合は、あとからメール登録すると起点が変わるので、リンクを作るたびに呼んでください）
- 失敗したときは、定価のリンクで送らず、あとで送り直してください

### 「購入直後だけ」の案件のリンクを外部の配信から送る `GET /api/bot/purchase/immediate-pass?slug=…&orderId=…`（要 `purchase:sign` ⚠）
「前の商品を買ってから◯時間だけ」の案件（`immediateOnlyHours`・`immediateFromSlugs`）の購入ページは、`?ap=`（買った案件と購入時刻の署名）を確かめます。無い・時間切れなら定価の案件（`immediateFallbackSlug`）へ移ります。
- 購入完了の画面から移る人には、Line-Affi が `?ap=` を自動で付けます
- **ステップメール**（`/go/` のリンク）から来る人は、`ap` が無くても通ります。`/go/` が付ける購読者の印（`ssid`/`ssig`）で、その人の購入を Line-Affi が確かめるためです。販売ページ（外部 LP）を通すときは、購入ボタンへ `ssid`/`ssig`（と `lref`・`lid`/`lsig`）を引き継いでください（`embed.js` を読み込むと自動）
- **LINE など外部の配信**は、購入の知らせ（partner-purchase）の `orderId` でこの API を呼び、返ってきた `url`（または `ap`）をリンクに使います。`lref` を付けたリンクで買った人なら、`lref` だけでも通ります
```bash
curl -s "https://line-affi.com/api/bot/purchase/immediate-pass?slug=ai-butler-thanks&orderId=<購入の id>" -H "Authorization: Bearer $KEY"
# → { "success": true, "ap": "kap2-kae-guild~…~…", "url": "https://line-affi.com/purchase/ai-butler-thanks?ap=…", "expiresAt": "…" }
# 対象の購入が無い・時間切れ → 404 { "code": "NOT_ELIGIBLE" }（定価の案内を送るか、送らない）
```
- `orderId` の代わりに `&lineUserId=U…`（その LINE の人の、対象の購入）でも引けます
- 買った時刻は Line-Affi の購入の記録で決めます。`expiresAt` を過ぎたリンクは、定価のページに移ります

### LINE の友だちに、紹介者の提携先リンクを送る `GET /api/bot/referral/resolve?offerSlug=…&lineUserId=U…`（要 `referral:read`）
LINE の配信から販売ページや提携先の登録リンク（直接紹介リンク）を送るときは、友だちごとにこの API で紹介元を調べてリンクを作ります。調べずに送ると、紹介者が運営者に入れ替わります。
- 応答の `url`（`/go/ref/{slug}?clickId=…`）をそのまま送る。押すと紹介者が登録した提携先のリンクへ転送されます
- 応答の `emailRegistered` は、その人がメール登録済みか。LINE で「メール未登録の人にだけメール登録を案内する」ときに使う（判定できないときは出さない側に倒す）
- 販売ページへのリンクには `?clickId={clickId}` を付ける（`clickId` が null の人は付けない＝運営者のリンク）
- 販売ページへのリンクには、あわせて応答の `lid`・`lsig` も `&lid={lid}&lsig={lsig}` で付ける。購入ページが購入者の LINE を記録し、購入の知らせ（`lineUserId`）・購入済みの照会・カゴ落ちの判定（`purchase_page_opened`）に使います。別のメールアドレスで買った人も見分けられます
- メールの中の LINE 友だち追加リンク（案件の公式アカウント宛て）は、紹介元のある購読者には自動で LIFF（本人確認）に差し替わります。メールから LINE に来た人も紹介者と結びつきます

### 媒体別の短い紹介リンク `/r/{紹介コード}/{媒体}`
アフィリエイターの紹介リンク（`https://line-affi.com/api/track/r/{code}`）には、最初から短い形があります。貼る場所ごとに使い分けると、どこから来たか（クリック数・登録数）が媒体別に分かります。発行や設定は要りません（紹介リンクを発行済みなら、そのコードでそのまま使えます）。
```
https://line-affi.com/r/{code}        … 媒体を分けない短いリンク
https://line-affi.com/r/{code}/x      … X 用
https://line-affi.com/r/{code}/x/2    … 追加LP（LP #2）行きの X 用
https://line-affi.com/r/{code}/2      … 追加LP（LP #2）行き・媒体を分けない
```
| 記号 | 媒体 | 記録される `utm_source` |
|---|---|---|
| `x` | X | `x` |
| `fb` | Facebook | `facebook` |
| `ig` | Instagram | `instagram` |
| `th` | Threads | `threads` |
| `tt` | TikTok | `tiktok` |
| `yt` | YouTube | `youtube` |
| `nt` | note | `note` |
| `bl` | ブログ | `blog` |
| `ml` | メルマガ | `mail` |
| `ln` | LINE | `line` |

- 動きは紹介リンクと同じ（クリックを記録して LP へ）。LP の番号は末尾に `/2` のように付けます（`?lp=N` でも同じ）。`utm_medium` は `short`
- アフィリエイターの画面では、LP ごとの枠の中に、その LP 行きの媒体別リンクとクリック数・登録数が出ます
- アフィリエイターの画面（案件 → 「短いリンク（媒体別）」）に、媒体ごとのリンク・コピー・クリック数・登録数が出ます
- 運営者が見る媒体別の数字は `GET /api/bot/go-clicks/by-utm?linkCode={code}`（今までの utm 別の集計にそのまま入ります）
- 人にリンクを渡すときは、長い形ではなく、この短い形で渡してください

### 紹介者のひも付けを切らさない決まり（LINE とページのリンク）
紹介者のひも付けは、**リンクの貼り方ひとつで切れます**。報酬を付けない案件（申し込みを外部で受ける直接紹介リンクなど）でも同じです。リンクを置く・文面を書くたびに、次を守ってください。

| 置く場所 | 貼るもの | 貼ってはいけないもの |
|---|---|---|
| ページの「LINE で友だち追加・相談」ボタン | `integration.snippets.lineButton`（LIFF の URL ＋ `data-lineaffi-line`）。`embed.js` が紹介元を `?state=` で付ける | ふつうの友だち追加リンク（`lin.ee/…`・`line.me/R/ti/p/@…`）。LINE と紹介者が結びつかない |
| ページの申し込みボタン（外部で受ける案件） | `integration.snippets.referralButton`（`/go/ref/{slug}` ＋ `data-lineaffi-referral`） | 提携先の登録リンクそのもの（運営者のリンクになる） |
| LINE の配信・自動応答（人ごとにリンクを作れる） | `GET /api/bot/referral/resolve` の `url`。販売ページなら `?clickId=` を付ける | 固定の提携先リンク・販売ページの素の URL |
| LINE の**手での返信・AI の返信・リッチメニュー**（全員に同じ文面） | **固定リンク `integration.urls.lineSpecial`**（`https://liff.line.me/{LIFF ID}?state=special:{offerSlug}`）。押した本人から紹介元を引いて、紹介元つきで `specialPageUrl` へ送る | 販売ページ・申し込みページの素の URL、提携先の登録リンク。**LINE の中で素の URL を開くと紹介元が付かず、申し込みボタンは運営者のリンクになります** |
| LINE から講座（視聴）ページへ | `integration.urls.lineWatch`（`…?state=watch:{offerSlug}`） | 視聴ページの素の URL |
| ステップメールの中の LINE 友だち追加リンク | そのままでよい（紹介元のある購読者には、送るときに自動で LIFF に差し替わる） | — |

- **固定リンクを使えるようにする**: オプトインの案件の `specialPageUrl` に、LINE から案内したいページ（販売ページ・申し込みページなど。LP か視聴ページと同じホスト）を入れます。すると `integration.urls.lineSpecial` が出ます。人が LINE で手で返信するとき・外部の AI に返信を書かせるときは、**この1本だけを使う**よう人と AI の両方に伝えてください（AI のナレッジや定型文に、提携先の登録リンクや販売ページの URL を置かない）
- **紹介元が分からない人**（紹介リンクを踏まずに友だちになった人、LINE アプリの外で開いた人）は、紹介元なしで進みます（＝運営者のリンク）
- **確かめ方**: ページを `?clickId=test123` 付きで開き、LINE ボタンの行き先が `…?state=test123`、申し込みボタンが `…/go/ref/{slug}?clickId=test123` になっているかを見ます。ふつうの `lin.ee` が残っていたら直します。LINE 側は、テスト用の友だちで固定リンクを押し、開いたページの URL に `clickId=` が付くかを見ます
- 人に報告するときは、「どの入口から来た人が、どこで紹介者と結びつくか」を経路ごとに伝え、確かめていない経路は確かめていないと書いてください

### LINE の友だちが買ったかを調べる `GET /api/bot/purchase/status?lineUserId=U…&group=…`（または `&offer=…`）（要 `purchase:read`）
外部の配信システムが、カゴ落ちの追いかけ・後押しのメッセージを送る**直前に**、その人がもう買っていないかを確かめる口です。
```bash
curl -s "https://line-affi.com/api/bot/purchase/status?lineUserId=U0123456789abcdef0123456789abcdef&group=phantom" -H "Authorization: Bearer $KEY"
curl -s "https://line-affi.com/api/bot/purchase/status?lineUserId=U0123456789abcdef0123456789abcdef&offer=phantom-spb" -H "Authorization: Bearer $KEY"
# → { "success": true, "purchased": true, "lastPurchasedAt": "2026-10-04T05:12:33.000Z" }
# → { "success": false … }（400）lineUserId の形が違う（U + 小文字16進32桁）・group も offer も無い
```
- `group` は案件グループ（`groupTag`）。グループの無い案件は slug でも当たります
- `offer` は案件の slug。その案件（キーのアカウントのもの）の購入だけで判定します（グループの無い案件向け）。`group` と両方あれば `group` を優先
- 人の引き方: **購入時の LINE の本人**（購入ページの `lid`（署名の合うもの）など、決済のときに分かった LINE。アップセル・月額の更新は元の購入のもの）・顧客の LINE の ID・紹介の登録の LINE の ID。メールで見つかった顧客に別の LINE の ID が付いていても、買ったときの本人で当たります。**返金済みも `purchased: true`**（追いかけない方を安全側に）。決済待ち・取り消しは数えません
- 見るのはキーのアカウントのデータだけ。全自動AI集客システムの連携の鍵は、権限を付け直さずに呼べます

### 友だちの行動を知らせる（購入ページを開いた・動画を1本見終えた）`GET/PATCH /api/bot/settings/partner-events`
line-affi から全自動AI集客システムに、watched_all と同じ宛先・同じ鍵で知らせます。**どちらもアカウントごとの設定で、既定はオフ**。オンにしたアカウントだけ送ります（向こうで LINE のメッセージが自動で送られるので、オンにする前に人の了承を得て、向こうの配信の準備ができているか確かめる）。管理画面では「設定 → 全自動AI集客システム」の「お客様の行動を知らせる」の2つのスイッチ。
```bash
curl -s "https://line-affi.com/api/bot/settings/partner-events" -H "Authorization: Bearer $KEY"          # 要 offers:read
# → { "success": true, "settings": { "partnerNotifyPurchaseOpened": false, "partnerNotifyWatchedPart": false } }
curl -s -X PATCH "https://line-affi.com/api/bot/settings/partner-events" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"partnerNotifyPurchaseOpened": true}'              # 要 line:connect ⚠
```
- `purchase_page_opened`（`partnerNotifyPurchaseOpened`）: 購入ページ `/purchase/{slug}` を開いた人を知らせます。`{"ownerId":"…","lineUserId":"U…","event":"purchase_page_opened","offer":"<slug>","group":"<groupTag か null>","at":"<ISO>"}`
  - 送るのは **LINE の本人と分かる人だけ**: `?lid=U…&lsig=…` の署名が合う（`{SALES_LINK}`・特別ページのリンクには付いています）か、会員サイトにログイン中の顧客で LINE の ID がある人。署名の無い `lid` では送りません
  - 同じ人・同じグループでは **6時間に1回まで**。値段の段で別の段へ転送されたときは、転送先の表示で1回だけ。ページの表示は待たせません（失敗は記録だけ。送れなかったときは次の表示で送ります）
- `watched_part`（`partnerNotifyWatchedPart`）: 動画を1本見終えるごとに1回。`{"ownerId":"…","lineUserId":"U…","event":"watched_part","offer":"<slug>","part":2,"total":3,"at":"<ISO>"}`
  - 「見終えた」は `/api/watch/progress` に `partial` なしで届いたとき（埋め込みタグは9割見たとき）。途中経過（25/50/75%）・特別編では送りません。3本そろったときの `watched_all`、特別編の `watched_special` は今までどおり（設定に関係なく送ります）
- 一度見終えた人に、次に開いたときもボタンを出すには `GET /api/watch/status?slug=&w=[&kind=special]`（同じサイトから）。`{watchedParts:[1,2]}` / `{specialWatched:true}` を返す。埋め込みタグ（/embed/video.js）は自動でこれを見る
- **メールから来た人の視聴も記録できます（どのセンターでも）**: メールのリンク（`/go/`）は、視聴ページ・特別ページ（案件の `watchUrl` / `specialPageUrl` のドメイン）へ送るときに、購読者の印 `?ssid=…&ssig=…` を自動で付けます（LINE から来た人には `?w=…`）
  - 記録: `POST https://line-affi.com/api/watch/progress` に、`w` の代わりに `{"ssid":"<受け取った ssid>","ssig":"<受け取った ssig>","slug":"{slug}","part":1,"total":3}`（特別編は `"kind":"special"`）。`w` と `ssid` の両方があれば両方を送ってください（同じ人として、どちらから開き直しても続きになります）
  - 照会: `GET https://line-affi.com/api/watch/status?slug={slug}&ssid={ssid}&ssig={ssig}`（特別編は `&kind=special`）。返事は LINE の人と同じ `{ ok, watchInOrder, watchedParts:[1,2,3], known:true }` / `{ ok, specialWatched:true }`
  - メールだけの人の分は、全自動AI集客システムには知らせません（LINE の ID が無いため）。記録と「見たか」の照会だけです
  - 印が合わない（書き換えた・別のアカウントの・消えた購読者）ときは `400 unknown viewer` で、記録しません
- **3本見終えた人に、その場で4本目（特別編）の入口を出す**（ステップメールの案内を待たずに）:
  1. 視聴ページは、受け取った `w`（LINE）か `ssid` / `ssig`（メール）を持っておく
  2. 開いたときに `/api/watch/status` を呼び、`watchedParts` に 1・2・3 がそろっていれば、すぐ4本目のボタンを出す
  3. 動画を見終えるたびに `/api/watch/progress` に送り、返事の `watchedAll: true` で4本目のボタンを出す
  4. 4本目のボタンの行き先（特別ページ）には、受け取った `w` / `ssid` / `ssig` / `sub` / `sig` をそのまま付けて渡す（特別ページの視聴の記録と、販売ページの値段の段に使う）
  5. 印の無い人（`known:false`）は、その端末の記録（localStorage など）で判断する
- **順番に最後まで見ないと次へ進めないようにする（`watchInOrder`・案件ごと・既定 false）**: `PATCH /api/bot/offers/{id}` に `{"watchInOrder": true}`（管理画面は案件の「LINEから渡す視聴ページ」のチェック）。動画が複数ページに分かれた講座ページ向け。**止める処理は講座ページ側**で行います:
  - ページを開いたら `GET https://line-affi.com/api/watch/status?slug={slug}&w={w}` を呼ぶ（`w` が無い人は `&w=` を付けずに呼ぶ）。返事は `{ ok, watchInOrder, watchedParts:[1,2], known }`
  - `watchInOrder` が true のとき、いま開いている動画を見終えていない（`watchedParts` に無く、その端末でも9割再生していない）人が「次の動画へ」を押したら、移動させずに「まだこの動画を最後までご覧になっていません。最後までご覧いただくと、次の動画へ進めます」と案内する。2本目以降のページを直接開いた人も、前の動画を見終えていなければ、見終えていない最初の動画へ戻す
  - `w` が無い人（`known: false`）は、誰が見たか分からないので、その端末の記録（localStorage など）で判断する
  - `watchInOrder` が false のときは、今までどおり自由に進める。設定を変えたら、次にページを開いたときから効きます
- どちらも、連携が承認済みのアカウントか、知らせてよいと決めた案件（と同じグループ）のときだけ。設定は `GET /api/bot/integrations/fullyautoai` の `partnerNotify` にも出ます
- 受け取る側は、送る前に `GET /api/bot/purchase/status` で買っていないかを確かめてください

### LINE の公式アカウントに紐づく案件を引く `GET /api/bot/channels/{channelId}/offers`（要 `offers:read`）
外部の配信システムが、チャネルから「どの案件の LP・メール登録ページ・販売ページを送るか」を決めるための口です。`{channelId}` はチャネル ID（数字）か、`@` から始まるベーシック ID（`%40` にエンコード）。
```bash
curl -s "https://line-affi.com/api/bot/channels/2000000001/offers" -H "Authorization: Bearer $KEY"
# → { "success": true, "channelId": "2000000001", "basicId": "@abc1234", "offers": [ { "offerSlug": "opt01", "offerType": "OPTIN", "urls": { "lp": "…", "gift": "https://line-affi.com/gift/opt01?lid=" }, … } ], "groups": [ … ] }
```
- 返すのは、`lineChannelId`（または `lineBasicId`）がそのチャネルの案件と、**同じ `groupTag` で LINE チャネル未設定の案件**（バックエンドなど）。止めた案件・別のチャネルの案件は入りません。見つからなければ `404 CHANNEL_NOT_FOUND`（他のアカウントのチャネルも同じ）
- `urls.gift` は末尾に友だちの LINE ユーザー ID を足して送る。`urls.sales`（BACKEND の販売ページ）には `?clickId=`（`/api/bot/referral/resolve` の値）を、値段の段の案件（`priceLadder` あり）には `&sub=&sig=`（`/api/bot/purchase/sub-sig`）も付ける
- `stage` は同じグループの BACKEND を安い順に 1, 2, 3…（低額 → バック → ハイバック）。値段の段の案件どうしは同じ番号
- 全自動AI集客システムの「連携する」で発行された鍵は、この口と `sub-sig`・`referral/resolve`・`purchase/status`・`opt-out` を、権限を付け直さずに呼べます
- 全自動AI集客システムで「連携する」を押したあとの**承認は、全自動AI集客システム側の管理画面で、人が行います**（line-affi の管理画面にはありません）。AI や API からは承認できません。承認されるまで、上の口・記号・知らせは使えません

### 入口が別システムの LINE ログインのとき `POST /api/bot/line/associate`（要 `line:associate` ⚠）
LP の LINE ボタンを、当システムの LIFF ではなく**別システムの LINE ログイン**（診断結果の受け渡しなど、そちらでしか出来ない処理がある）に向けている案件では、当システムの LIFF を挟まないでください（ログインが2回になり離脱が増えます）。代わりに、そのシステムから紹介元を知らせてもらいます:
1. LP のボタンの URL に `?clickId=…` を付け、そのシステムがログインの後まで引き継ぐ
2. そのシステムが LINE ログインで受け取った **id_token**（lineUserId ではなく）と clickId を、ここへ送る
```bash
curl -s -X POST https://line-affi.com/api/bot/line/associate \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"clickId":"…","idToken":"eyJ…","liffId":"2011746710-AbCdEfGh"}'
# → { "success": true, "lineUserId": "U…", "clickId": "…", "offerSlug": "…", "leadId": "…" }
```
- `liffId`（または `loginChannelId`）は、id_token を発行した LINE ログインのもの。省略すると案件の LIFF 設定で照合します
- id_token の期限は発行から約1時間。ログインの直後に送ってください。`400 ID_TOKEN_INVALID` は期限切れか、`liffId` の取り違え
- `400 CLICK_NOT_FOUND` は、別のアカウントの紹介リンクか、clickId の写し間違い
- **そのログインのチャネルは、公式アカウントと同じプロバイダーであること**。違うと lineUserId が一致せず、報酬が確定しません
- 報酬は、この後に届く友だち追加で確定します。**公式アカウントの webhook が当システムに届いている（転送を含む）ことが前提**です（`GET /api/bot/line/status` の `webhook_received`）
- 友だち追加が先に届いていても、この呼び出しの時点で処理されます

### リッチメニューから特別ページへ渡す `state=special:{offerSlug}`（`specialPageUrl`）
リッチメニューのボタンは全員に同じ URL なので、押した人ごとのリンクを作れません。代わりに次の1本を置くと、LIFF で本人を確かめてから、その人の印を付けて案件の「特別ページURL」へ転送します:
```
https://liff.line.me/{LIFF ID}?state=special:{offerSlug}
```
1. 案件に転送先を設定する: `PATCH /api/bot/offers/{id}` に `{"specialPageUrl": "https://…/special/"}`（作成時の `POST` でも可。管理画面は案件の「LINEから渡す視聴ページ」の欄）。**https のみ・その案件の `externalLpUrl` か `watchUrl` と同じホストに限ります**（違うと `400 INVALID_URL`）。`null` で解除
2. 応答の `integration.urls.lineSpecial` が、ボタンに置く URL です（LIFF ID と `specialPageUrl` があるときだけ出ます）
3. 転送先に付く値（既に付いている値は上書きしません）:
   - `clickId` … その人の紹介元（視聴ページと同じ）。分からない人には付かない
   - `sub` / `sig` … 値段の段の起点（LINE で友だちになった日時と、同じ人のメール登録の起点の早い方）と署名。起点が分からない人には付かない（購入ページでは、初めて販売ページに来た日から数える）
   - `lid` / `lsig` … その人の lineUserId と署名
   - `w` … 視聴の印（視聴ページと同じ。特別編の動画の視聴を記録するのに使う）
- 本人確認ができない（LINE アプリの外で開いた等）ときも、印なしで特別ページへ送ります。最長4秒で切り上げます。2回目からは同じ端末なら LINE との往復なしで即座に転送します
- **販売ページ（特別ページと、その先の販売ページ）は、受け取った `lid`・`lsig` を購入ページへ引き継いでください**。`embedScript`（embed.js）を置けば、`clickId`・`sub`/`sig` などと一緒に自動で付きます（7章）。自前でリンクを作るなら `https://line-affi.com/purchase/{slug}?clickId=…&sub=…&sig=…&lid=…&lsig=…`
- 購入ページは `lsig` が合う `lid` だけを購入者の LINE として使います（合わない `lid` は捨てる。`lsig` の無い古い `lid` は今は受けますが、将来は署名必須にします）
- **特別編の動画を最後まで観たら**、特別ページから `POST https://line-affi.com/api/watch/progress` に `{"w":"<受け取った w>","slug":"{offerSlug}","kind":"special","part":1,"total":1}` を送ってもらいます（`Content-Type: text/plain` で可。特別ページURLと同じサイトからだけ受けます）。3本の視聴の記録とは別に残り、連携中のアカウントなら全自動AI集客システムに `watched_special` を1回知らせます。早送りで観終えたことにさせない検査（実際に再生した秒数）はページ側で行ってください
- 観終えた人の数と一覧: `GET /api/bot/offers/{id}/special-views?limit=100`（要 `offers:read`。`viewers`（LINE の ID・観終えた日時・名前）は `customers:read` か `referral:read` のキーにだけ返ります）。管理画面では案件の編集画面の下に出ます
- 購入の知らせ（全自動AI集客システムへの partner-purchase）には、購入者の `lineUserId` が分かれば入ります（購入時の LINE の本人を最優先。顧客に別の LINE の ID が付いていても、買った本人の ID）。`lref` の無い購入でも、連携中（承認済み）のアカウントなら `lineUserId` 付きで知らせます

### 友だち追加されたときのあいさつ（`sendLineWelcome`）
- 既定では、友だち追加された瞬間に line-affi から「友だち追加ありがとうございます」＋プレゼントのURL（`giftUrl`）＋メール登録の案内を返します。メールアドレスが分かっている人（LP でメール登録してから友だち追加した人など）には、メール登録の案内は出しません
- **あいさつやシナリオを別のツール（全自動AI集客システム・Lステップなど）で送っている公式アカウントでは、二重になるので切ってください**: `PATCH /api/bot/offers/{id}` に `{"sendLineWelcome": false}`（作成時の `POST /api/bot/offers` でも可。今の値は `GET /api/bot/offers/{id}` の `offer.sendLineWelcome`）

## 4. メール配信の設定（時間がかかるので、人への依頼は早めに）

**これが要るのはステップメールだけです。** 招待メール・各種の通知・ログインリンクなどのシステムのメールは、
全アカウント共通の送信元（`noreply@…`）から届くので、この設定が無くても送られます。
ステップメールは、アカウントごとのメール配信サービス（Brevo / SendGrid / Mailgun / Resend）から、そのアカウントの送信元アドレスで送ります。
未設定のあいだ、ステップメールは配信されません（登録者は記録され、設定後に配信が始まります）。ステップメールを使わないなら、この章は飛ばせます。
**使う場合も、これを待つ必要はありません。**
案件の作成・LINE 連携・LP・紹介リンク・LINE 登録のテストは、メール未設定でも進められます。

### 人に頼むこと（配信サービス側の作業。API はありません）
人には、画像つきの手順ページ **`https://line-affi.com/guide/mail-setup`**（ドメインの取得 → Resend の DNS 認証 → API キー → line-affi への登録とテスト送信）を案内してください。DNS の作業は人が行います。
1. 配信サービスのアカウントを作る（**Brevo** が無料枠も大きく、おすすめ。既に Resend などを使っているなら、それをそのまま使える）
2. **独自ドメインの送信元アドレス**を用意する。Gmail・Yahoo・携帯キャリアなどのフリーメールは登録できません（`FREE_MAIL_NOT_ALLOWED`）
3. 送信ドメインを認証する（配信サービスが示す SPF / DKIM の DNS レコードを、ドメインの管理画面に追加）。**反映に数時間〜1日**かかります
4. APIキーを発行して、あなたに渡してもらう

### 登録 `PUT /api/bot/settings/email`（要 `email:settings`）
```bash
curl -s -X PUT https://line-affi.com/api/bot/settings/email \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"provider":"BREVO","apiKey":"xkeysib-…","fromEmail":"info@example.com","fromName":"サービス名","dryRun":true}'
```
- `provider`: `BREVO` / `SENDGRID` / `MAILGUN` / `RESEND`（Mailgun は `domain` も必須。`region` は `US` / `EU`）。キーの形式は、Brevo `xkeysib-…`、SendGrid `SG.…`、Resend `re_…`
- まず `dryRun:true` で形式を検証し、問題なければ `dryRun` を外して保存。同じ内容の再送は安全です
- 保存したキーは、この API を含めどこにも返りません（`apiKeyMasked` に末尾4文字だけ）。現在の設定は `GET /api/bot/settings/email`
- キーや配信サービスを変えると `verified` が `false` に戻ります

### テスト送信 `POST /api/bot/settings/email/test`
- **宛先は指定できません。** 常に、このアカウントのオーナーのログインメールへ送られます（1時間に5回まで）
- 成功しても「配信サービスが受け付けた」までです。**受信箱（迷惑メールフォルダも）に届いたかを、人に確認してください**
- `502 SEND_FAILED` は、`error` に原因が入ります。多いのは、送信ドメインの DNS 認証が未完了（反映待ち）と、キーの権限不足です
- 届いたら `verified: true` になり、`/account` の `setup.emailVerified` も `true` になります

削除は `DELETE /api/bot/settings/email` に `{"confirmProvider":"BREVO"}`（現在の業者名）。削除するとステップメールの配信が止まるので、人の了承を得てから。

### メールが迷惑メール・プロモーションに入るとき
新しい送信ドメインは実績が無いため、しばらくプロモーション扱いになりやすいです。こちらで減らせる要因は次のとおりです。
- **テキスト版は自動で併送**されます（対応不要）
- **本文中のリンクは、クリック計測のため `https://line-affi.com/go/…` に置き換わります。** 送信元とリンク先のドメインが食い違うのが一因です。
  `PUT /api/bot/settings/email` に `trackingDomain`（例: `track.example.com`）を指定すると、リンクをそのドメインにできます。
  人に、そのドメインのDNSへ CNAME（値は `https://line-affi.com` のホスト）を追加してもらってから指定してください。
  **疎通確認が取れたときだけ有効になります。** 取れないときは応答に `trackingDomainError` が入り、リンクは既定のドメインのまま（壊れません）。
  初回は運営側での証明書の発行が必要なので、`trackingDomainError` が出たら、その旨を人に伝えてください
- **届かないアドレス・迷惑メールの報告で自動で止める（Resend）**: 人に Resend の管理画面の「Webhooks」で、URL `GET /api/bot/settings/email` の `email.webhookUrl`（`https://line-affi.com/api/webhooks/resend?owner=<ownerId>`）、イベント `email.bounced` と `email.complained` の Webhook を作ってもらい、「Signing secret」（`whsec_…`）を受け取って `PUT /api/bot/settings/email` に `{"webhookSecret":"whsec_…"}` で入れる（`null` / `""` で消す）。値は返らず、`email.hasWebhookSecret` で入ったか分かる。入れると、恒久的な不達・迷惑メールの報告を受けたとき、ステップメールの読者はリストから配信停止、アフィリエイターはメールが「エラー停止」になる（一時的な不達では止めない）。秘密の値はチャットに貼らせない
- 人には「届いたメールをメインに移動し、送信元を連絡先に追加する」よう伝えてください。実績が付くまではこれが一番効きます

## 5. 報酬を決める `PATCH /api/bot/offers/{id}`（要 `rewards:write`）

```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$OFFER_ID \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"lineFollowReward":500,"emailOptinReward":200}'
```
- **未設定のままだと、成果が出ても紹介者に1円も付きません**（エラーにもなりません）。`/account` の `offers[].rewardTriggers` で確認できます
- 金額は必ず人に確認する。変更は今後の成果にだけ効きます
- **報酬を出さない**と人が決めた成果には `0` を送ります（例: `{"emailOptinReward":0}`）。「報酬なしで確定」として記録され、`/account` の `set_reward` が消えます。未設定（何も送っていない）とは区別されます。現在の値は `/account` の `offers[].rewards`（`{"EMAIL_OPTIN":{"yen":0}}` など）
- ランク別に金額を変えるなら `POST /api/bot/offer-rank-rewards`（成果の種類 `trigger` の指定が必須）。ランクの一覧は `GET /api/bot/ranks`。詳しくは下の「ランクと差し込み」
- 2ティア（紹介者の紹介者への報酬）の率は、人が管理画面で設定します。**全センター共通で 2% 以上**（既定 2%。2% 未満にはできません。プランに関係なく使えます）
- **購入報酬の自動承認**（アカウントの設定。既定 ON）: 挙動のおかしくない購入の報酬（と2ティア）は自動で確定します。
  自己アフィリ（購入者のメールが紹介者本人のアドレス・+やドット違いの別名を含む／購入者の LINE が紹介者本人）、紹介者の稼働停止中、
  紹介者に未対応の不正アラート、登録済みアドレスの再登録で紹介者が入れ替わった購入は、ON でも保留（運営者の承認待ち。承認は管理画面の「成果承認」）。
  確定後に返金されたら報酬は取り消し（支払い済みなら次回の支払いから差し引き）。OFF にすると購入報酬はすべて保留から始まります
  ```bash
  curl -s https://line-affi.com/api/bot/settings/rewards -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 要 offers:read
  # → { "success": true, "settings": { "autoApprovePurchaseCommission": true, "holdOptinVelocity": true, "tier2RatePercent": 10 } }
  curl -s -X PATCH https://line-affi.com/api/bot/settings/rewards -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
    -H 'content-type: application/json' -d '{"autoApprovePurchaseCommission":false}'          # 要 rewards:write。人に確認してから
  ```
- **`holdOptinVelocity`（既定 true）**: 1人のアフィリエイターの登録報酬（メール登録・LINE登録）が1時間に30件を超え、かつ中身が明らかにおかしい（同じ接続元に固まる・アドレスが1つのドメインだけ・連番や別名が並ぶ）ときに、以降を保留にして不正アラートを出す歯止めです。**案件の公開（launchAt）から6時間は自動で外れ、過ぎると自動で戻ります**。ローンチのために手で OFF にする必要はありません（true のままにしておく）。OFF の間も、重複アドレス・使い捨てアドレス・自分自身の紹介の検査は続きます。保留になった分は、管理画面の「成果承認」で人に承認してもらう
- **`rejectCarrierEmailOptin`（既定 false）**: 携帯キャリアのアドレス（docomo・au（ezweb）・SoftBank など）でのメール登録報酬を、非承認（取消）で記録します。登録そのものは受け付け、メールも送ります。人が「携帯アドレスは報酬の対象外にしたい」と言ったときだけ `PATCH /api/bot/settings/rewards` に `{"rejectCarrierEmailOptin":true}`。今後の登録にだけ効きます。非承認になった分は、管理画面の「アフィリエイト報酬集計」→ その人の「無料」の一覧にある「報酬承認」のチェックで、人が1件ずつ承認に変えられます（確定済みの分を非承認にするのも同じチェック）
- **承認を厳しくする設定（3つ。どれも整数か `null`。`null` = 使わない。既定は全部 `null`）**: 報酬目当てに登録だけする人が多いとき、人が「承認を厳しくしたい」と言ったら `PATCH /api/bot/settings/rewards` で入れます。今後の登録にだけ効きます
  - `optinEngagementHours`（例 `6`）: **反応つき承認**。登録報酬（メール・LINE）を承認待ちで作り、その人が**反応**（届いたメールを開く・メールのリンクを押す・動画を見る・LINE でメッセージを送る）したら自動で承認します。この時間のうちに反応が無ければ非承認にし、あとから反応があれば承認に戻します。反応を待っている間の報酬は、アフィリエイターの画面には出ません。**入れる前に、アフィリエイター向けの案内に「登録後に反応のあった方が報酬の対象」と書いてあるかを人に確かめてください**
  - `holdFastOptinSeconds`（例 `5`）: クリックからこの秒数より早いメール登録を、承認待ち（理由 `TOO_FAST`）に回します。人が承認画面で判断します。自動入力で10秒台の正当な登録はふつうにあるので、大きくしすぎない
  - `cancelLineBlockDays`（例 `3`）: 友だち追加からこの日数のうちにブロックされたら、その人の LINE 登録の報酬を取り消します（支払いに入った分は動かしません）

### お客様の問い合わせ対応（顧客を探す・登録を直す・ログインメールを送り直す）

問い合わせ対応をする AI・人のための API です。**そのアカウントの顧客だけ**が見えます。権限は、読むのが `customers:read`、直す・送るのが `customers:write`（どちらも既定オフ。管理画面の API キーで、必要な権限だけを付けたキーを発行して渡す）。

```bash
# 探す: メールアドレスか名前（2文字以上・部分一致・最大10人）。購入・会員の権利（期限・受講の進み具合）・ステップメールの購読の状態も返る
curl -H "Authorization: Bearer $KEY" "$BASE/api/bot/customers?ownerId=$OWNER&q=taro@example.com"
# 登録のメールアドレス・名前を直す（前のアドレスへ「変更しました」の知らせが届く。ステップメールの宛先も合わせる）
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" "$BASE/api/bot/customers" \
  -d '{"ownerId":"'$OWNER'","customerId":"...","email":"new@example.com"}'
# 会員サイトのログインメールを、登録のアドレスへ送り直す（offerId を付けると、その講座に入る・1時間に3通まで）
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" "$BASE/api/bot/customers" \
  -d '{"ownerId":"'$OWNER'","customerId":"...","action":"send_login_link","offerId":"..."}'
```

- **お客様1人の「経路」**（要 `customers:read`）: `GET /api/bot/customers/{customerId}/journey`。最初に来たリンク（紹介者・案件・LP・媒体 utm・デバイス）→ LINE 友だち追加 → メール登録 → ステップメールの開封・クリック → 購入（案件・金額・決済手段）・返金 → 会員サイトの視聴を、古い順の `events[]`（`at` `kind` `title` `detail`）で返します。記録が無い項目は出ません。顧客IDが分からないときは、`GET /api/bot/customers?q=`（メールアドレス・名前・顧客ID・LINE ID）で探します。IP は保存していないので返しません。`回線ab12cd` のような表記は、同じ回線・端末を見分けるための短い印です。メールアドレス・LINE ID・購入が読めるので、**人に見せる必要がある場合に限って**使ってください
- **アフィリエイター1人の成績**（要 `affiliates:read`）: `GET /api/bot/affiliates/{affiliateId}/journey`。`totals`（クリック・登録・購入の合計）、`events[]`（紹介したクリック→登録→購入。新しい順、それぞれ最新100件まで。購入には `customerId` が付く）、`referred[]`（紹介した人。`customerId` を上のお客様の経路に渡せます）。管理画面は、顧客一覧の「経路」・顧客詳細の「経路を見る」・アフィリエイター一覧の「成績を見る」、検索は「経路を探す」（`/admin/journey`）
- **「配信を止めてほしい」という返信が来たら**（メールの返信は自動では止まりません。止まるのは、本文の配信停止リンクを押したときだけ）: `POST /api/bot/customers` に `{"ownerId":"…","action":"stop_sequences","email":"返信してきた人のアドレス"}`。その人に配信中のステップメールを全部止めます。**リストからは外しません**（オーナーの方針。ほかの配信は届く）。応答の `stopped` が止めた本数。返信してきたアドレスと登録のアドレスが違って見つからないとき（`SUBSCRIBER_NOT_FOUND`）は、名前で `GET /api/bot/customers?q=` を引くか、人に回す。「今後一切送らないで」とはっきり書いてある・怒っている・迷惑メールだと言っている場合は、止めたうえで人に知らせる（リストから外すのは人が判断）
- **メールアドレスを変える前に、本人であることを確かめる**。変えると、その人の講座に新しいアドレスでログインできるようになる。「アドレスを変えてほしい」と言ってきただけの相手（知らない LINE・知らないアドレス）の依頼では変えない。確かめ方: 前の登録アドレスからの連絡である／その顧客に結び付いた LINE（`hasLine: true` の本人）からの連絡である／購入時の名前と決済の情報が合う、のどれか。迷ったら人に回す
- 新しいアドレスを別の顧客が使っているときは 409（`EMAIL_TAKEN`）。自動ではまとめないので、人に回す
- ログインの URL そのものは返らない（メールで本人に届くだけ）
- 講座の権利を付ける・外すのは `/api/bot/member-access`（権限 `members:write`）。購入・返金の計上は `/api/bot/conversion`

### アフィリエイターの退会（本人）

アフィリエイターは、アフィリエイトセンターの「登録情報」の下の「このアフィリエイトセンターを退会する」から、自分で退会できます（規約への同意は最初からチェック済み。外すと退会できない）。退会すると、そのセンターについて、未払いの報酬（確定・未確定とも。振込の処理に入ったもの・支払い済みは除く）は取り消し（`holdReason: "AFFILIATE_WITHDRAWN"`）、出金の申請中は取り下げ、登録情報（氏名・連絡先・住所・口座など）は消去、ログインを外します。`GET /api/bot/affiliates/{id}` の `withdrawnAt` が本人の退会の日時（`deletedAt` も入る）。退会は取り消せません。お知らせメールだけ止めたい人には、退会ではなくメール通知のオフを案内してください。

### 銀行振込での購入

購入ページに「銀行振込」を出せます（月額の商品には出ない）。出るのは、①アカウントが銀行振込をオンにして振込先を入れていて、②その案件に `PATCH /api/bot/offers/{id}` で `{"bankTransfer":true}` を入れたとき、の両方がそろった案件だけです（どちらも既定オフ）。**受けるかどうかと振込先は、管理画面の「銀行振込の入金確認」（/admin/bank-transfers）で人が設定します。API では変えられません**（振込先を書き換えられると被害が大きいため）。人に「銀行振込も受けますか。受けるなら振込先を管理画面に入れてください」と頼む。振込の期限（お申し込みから◯日以内・その日の 23:59 まで）も同じ画面で人が決めます。入れると、振込の案内メールに期限の日時が入り、申し込みの一覧（`GET /api/bot/bank-transfers` の各申し込みの `dueAt`・`settings.dueDays`）で期限切れが分かります。値段が日で上がる商品は、期限を入れるよう人に勧める。

流れ: お客様が購入ページで申し込む → 振込先を画面とメールで案内（まだ購入は確定しない）→ 人が口座の入金を確かめる → 「入金を確認」で購入が確定（会員サイトの付与・購入メール・紹介報酬・外部への通知は、カード払いと同じ）。

```bash
# 設定と申し込みの一覧（権限: customers:read。申込者の名前とメールアドレスを含む）
curl -H "Authorization: Bearer $KEY" "$BASE/api/bot/bank-transfers?ownerId=$OWNER&status=PENDING"
# 入金を確認した(confirm) / 入金待ちを取り消す(cancel) / 確認済みを返金した(refund)（権限: conversions:write と customers:read の両方）。confirm は、人が「入金があった」と言ったときだけ
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" "$BASE/api/bot/bank-transfers" \
  -d '{"ownerId":"'$OWNER'","orderId":"...","action":"confirm"}'
```

**入金を自分で確かめられないのに confirm を送らない**（口座を見られるのは人だけ。押すと講座が渡り、報酬が付く）。
- **`groupTag`（案件グループ）は必須**です（無いと `400 GROUP_TAG_REQUIRED`）。**オプトインとバックエンドは同じ `groupTag` にする**。バックエンドの購入報酬は、同じグループのオプトインを紹介した人を優先して付きます。
  率はバックエンド案件ごとの `purchasePercent`（0 なら誰にも付かない。紹介者の記録は残る）。未設定の案件の購入に、オプトイン側の設定で報酬が付くことはありません。
  別のグループで先に紹介していた人にも付けるかは、バックエンド案件の `rewardOtherGroupReferrer`（既定 true）。どちらも人に確認してから変える
- **売る順番 `salesStage`（BACKEND だけ）**：`1`＝フロント商品（最初に売る有料の商品）、`2`＝バックエンド商品①、`3`＝バックエンド商品②…（1〜20。省く・`null` はフロント扱い）。
  報酬集計（`/admin/performance`・`GET /api/bot/reports/affiliate-rewards` の `stages`）で、有料の商品をこの順番ごとの列に分けて数えます（無料オプトは OPTIN の案件で別の列）。
  BACKEND の案件を作るときは、**フロントか、何番目のバックエンドかを人に確かめてから** `POST /api/bot/offers` か `PATCH /api/bot/offers/{id}` に `{"salesStage": 2}` のように入れる。
  **値段の段（同じ `groupTag` の `tierFromDay` の段）は、すべて同じ `salesStage` にそろえる**（同じ商品の値段違いなので、別の列に割れないように）
  バックエンド案件を公開（`showToAffiliates`）していなくても、同じグループのオプトイン案件のカード（アフィリエイターのポータル）に、案件名を出さずに「バックエンド報酬 10%」（案件ごとに違えば「10〜20%」、ランク別の上書きがあればその % か「◯円」）と表示されます。公開した販売案件は、案件名・値段つきで表示されます

### 広告バナー（アフィリエイター用。A8 のバナー広告と同じ使い方）
案件ごとにバナー画像を載せると、アフィリエイトセンターの案件一覧で、リンクを発行した人に「バナー付きリンク」が出ます。
その人の紹介リンク入りの HTML（`<a href="{紹介リンク}" target="_blank" rel="nofollow sponsored noopener"><img src="…" width="…" height="…" alt="{案件名}" border="0"></a>`）と画像の URL をコピーして、ブログなどに貼れます。
- リンクはメインの紹介リンク（`/api/track/r/{code}`）と同じ。**クリックはふつうのクリックとして数えます**（表示回数は数えません）
- 画像は PNG / JPEG / WebP / GIF・**1枚 3MB まで**。縦横の大きさはサーバーが読みます（縮小・加工はしません）。`label` を省くと「横長 800×118」のような名前が付きます
- 画像の渡し方は `imageBase64`（`data:image/png;base64,…` 付きでも可）か `imageUrl`（公開されている `https://` の URL。サーバーが取りに行きます）の**どちらか1つ**
- 画像は `https://line-affi.com/api/uploads/…` で公開配信します（よそのサイトに貼っても表示されます）。削除しても画像ファイルは残します（すでに貼られたページで画像が割れないように）。センターから消えるので新しく貼られることはありません
- 人は管理画面の案件の編集画面「広告バナー（アフィリエイター用）」でも、まとめてアップロード・並べ替え・非表示・削除ができます
```bash
# 画像の base64 はコマンドの引数に直接入れると長すぎて失敗するので、本文をファイルに書いてから送る
printf '{"offerId":"<案件のID>","label":"横長 800×118","imageBase64":"%s"}' "$(base64 < banner-800x118.png | tr -d '\n')" > banner.json
curl -s -X POST https://line-affi.com/api/bot/offer-banners -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' --data-binary @banner.json   # 要 offers:write
# → { "success": true, "banner": { "id": "...", "imageUrl": "https://line-affi.com/api/uploads/...png", "width": 800, "height": 118, "label": "横長 800×118", "sortOrder": 0, "isActive": true, "htmlTemplate": "<a href=\"{紹介リンク}\" ...>" } }
curl -s -X POST https://line-affi.com/api/bot/offer-banners -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"offerId":"<案件のID>","imageUrl":"https://example.com/banner-300x250.png"}'
curl -s "https://line-affi.com/api/bot/offer-banners?offerId=<案件のID>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 要 offers:read。offerId を省くと全案件
curl -s -X DELETE "https://line-affi.com/api/bot/offer-banners?id=<バナーのID>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 要 offers:write
```
- `sortOrder`（0 から。小さいほど上）と `isActive`（false で非表示）も POST で指定できます。省くと最後に足して表示します

### ランクと差し込み（案件の説明・お知らせに、読む人のランクの報酬額を自動で入れる）

**ランク**: どのセンターにも最初から「一般」「VIP」「SVIP」があります（`GET /api/bot/ranks` の `sortOrder` が小さいほど下位。一般がいちばん下）。
- ランク別の報酬は最初は入っていません＝**どのランクも案件の標準の報酬**です。`POST /api/bot/offer-rank-rewards`（`offerId`, `rankId`, `trigger`, `rewardYen`）で入れたランクだけ、その金額になります。金額は必ず人に確認してから
  - **購入（販売の案件）は ％ でも入れられます**: `rewardYen` の代わりに `rewardPercent`（15 = 15%、小数は 0.01 まで）か `rewardBps`（1500 = 15%）。購入額 × 率（1円未満切り捨て。標準の % と同じ）。
    標準が % なので、ランクも % にすると単位がそろいます。値段の段（日にちで値段が変わる）の案件でも、段ごとの値段に合わせて付きます。
    `rewardYen` / `rewardPercent` / `rewardBps` は**どれか1つだけ**（2つ以上は 400）。LINE登録・メール登録は値段が無いので `rewardYen` だけ。同じランクに送り直すと、円⇔% も含めて上書きします。`0`（0円 / 0%）は「そのランクには付けない」
  ```bash
  curl -s -X POST https://line-affi.com/api/bot/offer-rank-rewards -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
    -H 'content-type: application/json' -d '{"offerId":"<販売案件のID>","rankId":"<VIPのID>","trigger":"PURCHASE","rewardPercent":15}'
  # → { "success": true, ..., "unit": "PERCENT", "rewardYen": null, "rewardBps": 1500, "rewardPercent": 15 }
  curl -s "https://line-affi.com/api/bot/offer-rank-rewards?offerId=<案件のID>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 要 offers:read
  # → { "success": true, "rankRewards": [{ "rankId": "...", "rankName": "VIP", "trigger": "PURCHASE", "unit": "PERCENT", "rewardYen": null, "rewardBps": 1500, "rewardPercent": 15 }, ...] }
  #   円の上書きは "unit": "YEN", "rewardYen": 3000, "rewardBps": null。一覧に無いランクは標準の報酬
  ```
- ランクが未設定の人・新しく登録した人は、いちばん下のランク（既定は「一般」）として扱います。ランクを変えるのは `PATCH /api/bot/affiliates/{id}` の `rankId` か `rankName`（`null` でいちばん下のランクに戻す）
- ランクの追加は `POST /api/bot/ranks`（同じ名前があれば作らずに既存を返す）。名前の変更・並べ替え・削除は管理画面「ランク管理」で人が行います（削除したランクの人は、いちばん下のランクに移ります）

**差し込み**: 案件の説明（`description`。紹介者ページの案件カード・新案件のお知らせメール）とお知らせ（`POST /api/bot/affiliates/announce` の `subject`・`body`）に書けます。
表示・送信のたびに、**読む人のランク**の金額に置き換わります（ランク別の金額が無ければ標準）。ランクや金額を後で変えても、文を書き直す必要はありません。

| 差し込み（英字の別名） | 入るもの（例） |
|---|---|
| `{{案件名}}`（`{{offer_name}}`） | 案件の名前 |
| `{{ランク名}}`（`{{rank_name}}`） | 読む人のランク名（一般／VIP／SVIP など） |
| `{{報酬_LINE登録}}`（`{{reward_line}}`） | LINE登録の報酬「500円」。LINE で登録を受けない案件・報酬0円なら空 |
| `{{報酬_メール登録}}`（`{{reward_email}}`） | メール登録の報酬「300円」。メールで登録を受けない案件・報酬0円なら空 |
| `{{報酬_購入}}`（`{{reward_purchase}}`） | 販売（BACKEND）案件の購入報酬。率なら「10%」（ランクの % 上書きならその率）、ランクの円の上書きなら「3,000円」 |
| `{{報酬_バックエンド}}`（`{{reward_backend}}`） | 同じ案件グループの販売案件の購入報酬（案件名は出さない）。違えば「10〜20%」。付かなければ空 |
| `{{報酬一覧}}`（`{{reward_list}}`） | その案件の報酬を1行ずつ（「LINE登録：500円」など。設定の無い種類は出さない） |

- **案件を指定する形**（お知らせで使う）: `{{報酬_LINE登録:案件のslug}}`、`{{案件名:案件のslug}}`。slug を書かない形は、案件の説明ならその案件、お知らせなら `offerId` の案件を指します（`offerId` が無いお知らせでは空になります）
- **値があるときだけ出す文**: `{{#報酬_バックエンド}}購入されるとバックエンド報酬 {{報酬_バックエンド}} も付きます。{{/報酬_バックエンド}}`（報酬が無ければ中の文ごと消える）
- **綴り違いや無い slug の差し込みは、読む人には空で表示されます**（記号のまま見せない）。保存・送信の応答の `warnings` に「分からない差し込みがあります」と出るので、出たら直してください。お知らせの `dryRun` は `rewardPreview`（ランクごとの見え方）も返すので、人に見せてから送ってください
- **案件の説明の既定の文**: `POST /api/bot/offers` で `description` を空にすると、次の文が入ります（後から `PATCH` で書き換えられます）
  - オプトイン（LINE とメール）: `{{案件名}}です。` / `登録はメールアドレスかLINEで受け付けています。` / `報酬：LINE登録 {{報酬_LINE登録}}／メール登録 {{報酬_メール登録}}。{{#報酬_バックエンド}}あなたの紹介で登録した方が商品を購入すると、バックエンド報酬 {{報酬_バックエンド}} が加算されます。{{/報酬_バックエンド}}`（LINE だけ・メールだけの案件は、無い方の報酬を書かない文になります）
  - 販売（BACKEND）: `{{案件名}}です。報酬：購入 {{報酬_購入}}。`

### 月額の商品（毎月自動で請求）`billingType: "MONTHLY"`

「毎月○円のサロン・会員サービス」のような商品は、案件を `billingType: "MONTHLY"` にします（`POST` でも `PATCH` でも可。要 `offers:write`）。

```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$OFFER_ID \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"billingType":"MONTHLY","price":9800,"recurringCommissionMonths":12}'
```
- `price` が**1か月の金額**。申し込み時に1か月分をカードで請求し、以後**毎月同じ日に自動で請求**します（販売者の PAY.JP の定期課金）
- **分割払い（◯回払い）**: 月額の案件に `installmentCount`（2〜24）を入れると、分割払いになります。`price` が**1回分の金額**です（例: 598,000円の2回払い → `{"billingType":"MONTHLY","price":299000,"installmentCount":2}`）。申し込み時に1回目、以後は毎月同じ日に自動で請求し、決めた回数を払い終えたら請求を止めます。払い終えたあとは、会員サイトを期限なしで見られます（買い切りと同じ）。購入ページ・購入完了メールには「◯回払い・合計◯円」と出ます。お客さんの側からは途中で解約できません（運営者は「月額の契約」から止められます）。無料お試し（`trialDays`）とは一緒に使えません。紹介報酬は、支払いのたびにその回の金額に対して付きます（`recurringCommissionMonths` を入れなければ全部の回）。一括払いも受けるなら、別の案件（1回払い）を同じ案件グループに作ってください
- **カード（PAY.JP）だけ**です。PayPal・クーポン・アップセル／ダウンセル・値段の段（`tierFromDay`）とは組み合わせられません（`400 INVALID_BILLING` / `MONTHLY_NOT_ALLOWED`）
- 紹介報酬: 申し込み時の紹介者に、**毎月の支払いごと**に `purchasePercent`（とランク別報酬）で付きます。自動承認・不正チェックも購入報酬と同じ。
  何回目の支払いまで付けるかは `recurringCommissionMonths`（初回を1回目と数える。`null` か省略で無期限。変更は要 `rewards:write`）。人に確認してから決める
- **人に頼むこと（必須）**: PAY.JP の管理画面（API → Webhook）に、`GET /api/bot/account` の `webhooks.payjp` の URL を登録し、表示される Webhook トークンを管理画面の「決済設定 → PAY.JP → Webhookトークン」に入れてもらう。
  これが無いと、毎月の支払い・支払い失敗・解約がすぐには記録されません（毎日の見回りで後から拾いますが、遅れます）
- お客様は会員サイトの「ご契約・解約」（`/members/{slug}/billing`）から自分で解約・カードの変更ができます。解約すると次の更新日で止まり、それまでは見られます
- 支払いに失敗すると、お客様にカードの変更を頼むメールが届き、**7日の猶予**のあいだは会員サイトを見られます。過ぎても払われなければ契約を止めます
- 購入完了メールには、月額であること・次の請求日・解約のページが自動で入ります。文面に差し込むなら `{{next_billing_date}}`（次の請求日）・`{{cancel_url}}`（ご契約のページ）
- 金額を変えても、**今契約している人は申し込み時の金額のまま**です。契約中の人がいる案件は `ONE_TIME` に戻せません（新しい案件を作る）

#### 無料お試し `trialDays`
```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$OFFER_ID \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"trialDays":30}'          # 最初の30日間は無料。0 か null でお試しなし（1〜365）
```
- 購入ページに「最初の30日間は無料。◯月◯日から毎月◯円」と「お試し中に解約すれば0円」が出ます。申し込みでは**カードを登録するだけ**（PAY.JP の定期課金を `trial_end` 付きで作り、その場では請求しません）
- お試し中（契約の `status: "TRIAL"`）は**購入にも紹介報酬にも数えません**。会員サイトは見られます。お客様には「無料お試しのご案内」のメール（いつまで無料・いつからいくら・解約のページ）が届きます
- お試しの終わりに PAY.JP が初回を請求すると（PAY.JP の webhook `subscription.renewed` / `charge.succeeded`。届かなくても毎日の見回りで拾う）、その支払いを**初回の購入**として記録し、**申し込み時の紹介者に初回の報酬**を付けます（購入完了メールもこのとき）
- お試し中に解約すると、お試しの終わりで止まり、**請求も報酬もありません**
- 前に同じ案件を申し込んだことがあるメールアドレスは、お試しなしで申し込み時に1か月分を請求します（解約と申し込みの繰り返しで無料が続かないように）
- 月額（`billingType: "MONTHLY"`）の案件だけに付けられます

#### 2回目以降の毎月の報酬を別の率にする `renewalRewardOfferId`（要 `rewards:write`）
初回と継続で報酬の率を変えたいとき（例: 初回 50%・2回目以降 20%）は、継続の報酬の計算用に**バックエンド案件をもう1つ作り**（非公開のままでよい・`purchasePercent` とランク別報酬を入れる）、その ID を月額の案件に入れます。
```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$MONTHLY_OFFER_ID \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"renewalRewardOfferId":"'$RENEWAL_RATE_OFFER_ID'"}'     # null で「この案件と同じ」に戻す
```
- 2回目以降の毎月の支払いの報酬を、その案件の購入報酬（標準の `purchasePercent` と、ランク別の上書き。円でも % でも）で計算します。**0% やランク別の 0 なら、そのランクには継続の報酬が付きません**
- 報酬を付ける相手は、今までどおり**申し込み時（初回）の紹介者**。何回目まで付けるかも `recurringCommissionMonths` のまま
- 毎月の支払い（購入の記録）は月額の案件のまま残ります（売上の集計は月額の案件で見る）。報酬の記録だけ、計算に使った案件のプランに結び付きます
- 同じアカウントのバックエンド案件だけ選べます（`400 INVALID_RENEWAL_REWARD_OFFER`）。計算用の案件を消すと、この案件の報酬に戻ります

#### 月額の契約を見る・解約する
```bash
curl -s "https://line-affi.com/api/bot/subscriptions?status=ACTIVE,PAST_DUE" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 要 customers:read
curl -s "https://line-affi.com/api/bot/subscriptions/$SUB_ID?sync=1" -H "Authorization: Bearer $LINE_AFFI_API_KEY"           # PAY.JP から取り直してから返す
curl -s -X PATCH https://line-affi.com/api/bot/subscriptions/$SUB_ID -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"action":"cancel"}'                                                   # 要 subscriptions:write ⚠
```
- `status`: `TRIAL`（無料お試し中。`trialEnd` に初回の請求日）/ `ACTIVE`（契約中）/ `CANCEL_SCHEDULED`（解約予定。`currentPeriodEnd` まで見られる）/ `PAST_DUE`（支払い失敗・猶予中）/ `CANCELED`（停止）。応答の `byStatus` に件数
- 解約はお客様にメールが届き、取り消せません。**必ず人の了承を得てから**。契約を API で作ることはできません（お客様が購入ページでカードを入れて申し込む）
- 1か月分の返金は、PAY.JP の管理画面でその月の支払いを返金すると、その月の売上と報酬が取り消されます（契約は続く。止めるなら解約も）

### 外部のサービスへの知らせ（fulfillment webhook）`fulfillmentWebhookUrl`

KAP・全自動AI集客システム・AutoShortVideo など**別のサービスを line-affi の案件として売る**とき（報酬は line-affi で計算し、プランの付け外しはそのサービスがする）に使います。
お客様の申し込み・支払い・解約が起きるたびに、案件に入れた URL へ line-affi から POST で知らせます。

```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$OFFER_ID \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"fulfillmentWebhookUrl":"https://service.example.com/api/line-affi/webhook","fulfillmentPlanKey":"pro"}'
# 要 offers:write と customers:read（お客様のメールアドレスと名前が届くため）
# → 初めて URL を入れたときだけ "fulfillmentWebhookSecret": "lafs_…" が返る（署名の鍵。二度と見られないので、すぐ受け取る側へ渡す）
# 鍵を作り直す: -d '{"regenerateFulfillmentSecret":true}'（古い鍵はすぐ使えなくなる）。URL を null にすると知らせを止める
curl -s "https://line-affi.com/api/bot/offers/$OFFER_ID/fulfillment-deliveries?limit=30" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 最近の送信（要 offers:read）
curl -s -X POST https://line-affi.com/api/bot/offers/$OFFER_ID/fulfillment-deliveries -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"deliveryId":"…"}'                                                              # 送り直す（同じ eventId）
```
- `fulfillmentPlanKey`: そのサービスでのプランの名前（`standard` / `pro` / `premium` など。英数字と `._:-`）。空ならこの案件の slug を送ります。1つのサービスで複数のプランを売るなら、プランごとに案件を作って名前を変える
- 管理画面では、案件の編集ページの「外部のサービスへの知らせ」で同じことができます（最近の送信と「再送」も）
- `GET /api/bot/offers/{id}` の `offer.fulfillmentWebhookUrl` / `fulfillmentPlanKey` / `fulfillmentHasSecret` で今の設定を確かめられます（鍵そのものは返しません）

#### 出来事（`event`。ヘッダー `X-LineAffi-Event` にも同じ値）
| event | いつ | 受け取った側ですること（例） |
|---|---|---|
| `trial_started` | 月額の無料お試しの申し込み（請求なし） | プランを付ける（お試し） |
| `subscribed` | 月額の**初回の支払い**（お試しなしなら申し込み時、お試しありならお試しの終わりの請求） | プランを付ける・続ける |
| `renewed` | 2回目以降の毎月の支払い | 期限を延ばす |
| `payment_failed` | 毎月の支払いに失敗（7日の猶予に入った） | 必要ならお知らせ。`subscription.accessUntil` までは使わせてよい |
| `cancel_scheduled` | 解約を受け付けた（次の更新日・お試しの終わりで止まる） | `subscription.accessUntil` まではそのまま |
| `canceled` | 契約が止まった（期間の終わり・支払いが戻らず停止・支払い失敗中の解約） | プランを外す |
| `refunded` | 返金（全額・一部。`payment.refundedAmount` は累計） | 全額ならプランを外すなど |
| `one_time_purchase` | 1回払いの案件の購入 | プランを付ける（期限なし） |

#### 中身（JSON。送り直しでも同じ中身・同じ `eventId`）
```json
{
  "event": "subscribed",
  "eventId": "cm1x…",                      // 出来事ごとに一意。送り直しても同じ。受け取った側はこれで二重処理を防ぐ
  "occurredAt": "2026-11-04T00:00:05.000Z",
  "offer": { "id": "…", "slug": "kap-pro", "name": "KAP プロ", "planKey": "pro" },
  "customer": { "id": "…", "email": "buyer@example.com", "name": "山田 太郎" },
  "subscription": {                         // one_time_purchase と refunded では null（返金は payment.purchaseId で元の支払いを引く）
    "id": "…",                              // line-affi の契約の ID（同じ契約の出来事はこの ID で結び付ける）
    "status": "ACTIVE",                     // TRIAL | ACTIVE | CANCEL_SCHEDULED | PAST_DUE | CANCELED
    "currentPeriodEnd": "2026-12-04T00:00:00.000Z",  // 次の更新日（お試し中は初回の請求日）
    "trialEnd": "2026-11-04T00:00:00.000Z",          // お試しの終わり（お試しなしは null）
    "accessUntil": "2026-12-11T00:00:00.000Z"        // 使わせてよい期限（支払い失敗の猶予7日を含む。解約予定なら期間の終わり）
  },
  "payment": { "amount": 9800, "chargeId": "ch_…", "purchaseId": "…" }   // 支払いの無い出来事では null。refunded は refundedAmount も
}
```
- `customer.name` は購入ページの「お名前」欄（必須・50文字まで。カードでも PayPal でも、1回払いでも月額でも）で入れてもらった名前です。前からの顧客で今回入れた名前があれば、それに置き換わります（空で上書きはしません）。購入完了メールの宛名にも使います。
  購入ページを通さずに決済の API（`/api/payjp-charge`・`/api/payjp-subscribe`・`/api/paypal/capture-order`）を呼ぶときは、`name` は任意です（送らなければ今の名前のまま。古い顧客では `null` のことがあります）

#### 署名の確かめ方（受け取る側で必ず）
ヘッダー: `X-LineAffi-Timestamp`（UNIX 秒）・`X-LineAffi-Signature: sha256=<16進>`・`X-LineAffi-Event`。
署名は `HMAC-SHA256(鍵, "${timestamp}.${生の本文}")` の16進です。**5分より古い timestamp は断ってください**（同じ中身の使い回しを防ぐ）。送り直しのときは、その時刻で署名し直して送ります（`eventId` は同じ）。
```js
// Node（Express の例）。本文は JSON に直す前の「生の文字列」で確かめる
import crypto from "node:crypto";
app.post("/api/line-affi/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  const ts = Number(req.get("X-LineAffi-Timestamp"));
  const sig = (req.get("X-LineAffi-Signature") || "").replace(/^sha256=/, "");
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return res.status(400).send("stale");
  const expected = crypto.createHmac("sha256", process.env.LINE_AFFI_FULFILLMENT_SECRET).update(`${ts}.${raw}`).digest("hex");
  const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.status(401).send("bad signature");
  const evt = JSON.parse(raw);
  if (await alreadyHandled(evt.eventId)) return res.status(200).send("duplicate");   // eventId で二重処理を防ぐ
  // evt.event に合わせて、evt.customer.email のアカウントの evt.offer.planKey のプランを付ける／外す
  await markHandled(evt.eventId);
  res.status(200).send("ok");
});
```
- **2xx を返したら届いた扱い**。それ以外・10秒以内に返らない・通信失敗は、1分・5分・15分・1時間・3時間・6時間・12時間・24時間の間を空けて送り直し、それでも届かなければ「届かなかった」になります（管理画面・API の「再送」で送れる）。リダイレクト（3xx）は追いかけません
- 届く順番は前後することがあります。`occurredAt` と `subscription.status` / `accessUntil` を見て、新しい状態だけを当ててください
- 同じメールアドレスの人が、**そのサービスで直接（line-affi を通さずに）契約している**ことがあります。line-affi は何もしません。受け取る側で「先に有効になっている方を残し、運営者に知らせる」（二重に請求しない・プランを二重に付けない）ようにしてください
- 送り先は https だけです（社内のアドレス・localhost には送りません）

### お支払いした人を自動でアフィリエイターにする `GET/PUT /api/bot/settings/auto-affiliate`
サービスを使っている人に、そのまま紹介してもらうための設定です（既定は OFF）。
```bash
curl -s https://line-affi.com/api/bot/settings/auto-affiliate -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 affiliates:read
curl -s -X PUT https://line-affi.com/api/bot/settings/auto-affiliate -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"rankId":"'$RANK_ID'"}'                                   # 要 affiliates:write。null で OFF
# 案内メールを変えるなら "mailSubject" / "mailBody"（普通の文章。null で既定の文）。{{center}} {{name}} {{loginUrl}} を差し込める
# → { "success": true, "settings": { "enabled": true, "rankId": "…", "rankName": "一般", "mailSubject": null, "mailBody": null, "defaults": { … } } }
```
- 月額の**初回の支払い**（無料お試しの後の初回も）・1回払いの**購入**があったとき、その人がまだこのアカウントのアフィリエイターでなければ、`rankId` のランクで登録し（手で追加するときと同じ）、案内のメール（既定の件名「{{center}}のアフィリエイトセンターにご招待しました」・ログインの URL 付き）を送ります
- すでにアフィリエイターの人には何もしません（**ランクが低くても自動では変えません**）。自己アフィリの扱い（本人の紹介リンクでの購入は保留など）は今までどおり
- プランのアフィリエイターの上限に達しているときは登録しません（ログに残ります）
- 管理画面は「連携設定」の「お支払いした人を自動でアフィリエイターにする」（メールの見本も見られます）

### KAP の会員状況でランクを自動設定（運営アカウント専用・設定なし）
運営アカウント（`LINE_AFFI_REFERRAL_OWNER_ID`）だけの内部連携です。KAP に登録しているのと同じメールアドレスで登録した人は、本人確認（メールのリンク）の後に自動で「利用者」（KAP の有料会員）・「ギルド」になり、毎日 `/api/cron/kap-tier-sync` が KAP に聞き直します。上げるのはすぐ、下げるのは「KAP の利用状況で自動設定」と付いた人だけで、KAP が3日続けて低いと答えたとき（`PATCH /api/bot/affiliates/{id}` などで手でランクを変えた人は自動の対象から外れ、変わりません）。KAP が答えられない日は何も変えません。お祝いのメールは送りません。

**購入の中継（運営アカウントだけ）**: 運営アカウントで売っている商品（KAP 月額など）を買った人を、別のセンター（AI Wealth Fountain など）のステップメールで次の段へ進めます（前の段のシナリオを「購入で終了」にし、次のシナリオを始める）。先のセンターには購入・報酬・顧客を作りません（運営のアフィリリンクで案内し、バックエンド報酬は付けない決まり）。設定（表 `PurchaseRelay`）は API・画面からは作れず、運営が入れます。名簿にいない人は、始めるシナリオがあれば名簿に入れて始めます。配信停止中の人には始めません。月額の毎月の更新では動きません

### 自動ランクアップ `GET/PUT /api/bot/settings/auto-rank`
どのセンターも**最初から ON** です。1つの案件で承認済みの成果が 30件で「VIP」、100件で「SVIP」に自動で上がります。
```bash
curl -s https://line-affi.com/api/bot/settings/auto-rank -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 offers:read
curl -s -X PUT https://line-affi.com/api/bot/settings/auto-rank -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"enabled":true,"rules":[{"minApproved":30,"rankName":"VIP"},{"minApproved":100,"rankName":"SVIP"}]}'   # 要 rewards:write
# 止める: {"enabled":false}　既定のルールに戻す: {"rules":null}　（指定した項目だけ変わります）
# → { "success": true, "settings": { "enabled": true, "rules": [...], "isDefault": false,
#      "resolved": [{ "minApproved": 30, "rankName": "VIP", "rankId": "…", "skipped": false }, …], "defaults": [...] } }
```
- 数えるのは**承認済みの成果**（確定・支払済みの 1ティアの報酬）だけ。承認待ち・取り消し・2ティア・返金された購入は数えません。月額の契約は毎月の更新を数えず、契約1つで1件
- **案件ごと**に数えます（いちばん多い案件の件数で決まる）。上がったランクは他の案件でもそのまま使われます
- **上げるだけで下げません**。対象は「一般」（ランク未設定を含む）とルールのランクの人だけ。ギルドなど、手で付けた別のランクの人は変わりません
- 上がった後の成果から、そのランクの単価（案件の「ランク別報酬」）になります。過去の報酬は変わりません。ランク別報酬を入れていないランクは案件の標準の報酬のままなので、単価を上げるなら `/api/bot/offer-rank-rewards` で VIP・SVIP の額も入れてください
- `rankName` はそのセンターにあるランクの名前（無い名前は 400）。後でランクの名前を変えると、その段は使われなくなります（`resolved[].skipped: true`）
- 判定は、報酬が承認済みで付いたとき・運営者が承認したときと、毎日1回の見直し。上がった人にはお祝いのメール（「ランクが VIP に上がりました。これからの成果は VIP の単価になります」）が届き、アフィリエイトセンターの案件一覧とホームに赤い大きな案内（「1案件で30件以上獲得で単価アップ！」・あと何件か）が出ます
- 管理画面は「ランク管理」の「自動ランクアップ」

### 報酬の支払いルール（最低支払額・出金申請） `GET/PUT /api/bot/settings/payout`
お金の支払い条件が変わるので、**変える前に人の了承を得てください**。
```bash
curl -s https://line-affi.com/api/bot/settings/payout -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 affiliates:read
curl -s -X PUT https://line-affi.com/api/bot/settings/payout -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"minPayoutYen":10000,"payoutFeeYen":1100}'   # 要 rewards:write（指定した項目だけ変わる）
# → { "success": true, "settings": { "minPayoutYen": 10000, "payoutFeeYen": 1100 } }
```
- `payoutFeeYen`: 出金手数料（円・消費税込み）。0〜10,000 の整数、初期値 1,100円、0 は手数料なし。**`minPayoutYen` より小さくする**（同じか大きいと `400 FEE_NOT_BELOW_MINIMUM`）。GET・PUT の応答に、設定できる範囲（`minPayoutYenFloor` `minPayoutYenMax` `payoutFeeYenMax`）がつきます
- `minPayoutYen`: 出金を申請できる最低額（円）。**初期値 10,000円（これ未満の報酬は申請できません）**。変えるなら 1,000円以上・1円単位。アフィリエイターのポータルの表示・案内も、このセンターの金額になります
- **出金は、どのセンターも、アフィリエイター本人の申請から始まります**（申請を必須にするかの設定はありません）。確定済みの報酬の合計が最低額以上のとき、本人がポータルの「出金を申請する」を押します（**申請は API からは作れません**）。1人につき申請中の申請は1件だけ。**支払うのは、申請した時点の確定報酬だけ**（申請後に確定した分は次の申請）。**支払予定日は、申請した月の翌々月末（日本時間）**（例: 10/20 に申請 → 12/31）
- **インボイスと出金手数料**: アフィリエイターの区分 `entityType` は `INDIVIDUAL`（個人。初期値）か `CORPORATE`（法人）。**法人はインボイス登録番号（`invoiceRegistrationNumber`、T + 13桁）が無いと出金を申請できません**（`NO_INVOICE`）。個人は番号不要です。区分は本人がポータルのプロフィールで、運営者は `PATCH /api/bot/affiliates/{id}` の `entityType` で直せます。**出金手数料（`payoutFeeYen`・消費税込み。初期値 1,100円＝1,000円＋消費税10%）を、1回の出金につき報酬の合計から差し引いて振り込みます**（個人・法人とも）。手数料は申請した時点の額で確定します。**アフィリエイト報酬は源泉徴収の対象ではないため、源泉徴収はしません**。報酬が手数料以下のときは申請できません（`BELOW_FEE`。最低額に届かないときは `BELOW_MINIMUM`）
- 申請の一覧: `GET /api/bot/payout-requests?status=REQUESTED&limit=100`（要 `affiliates:read`。`limit` は 1〜500、省略・0・不正は100）。`status` は `REQUESTED`（申請中）/ `PAID`（振り込み済み）/ `REJECTED`（却下）。各行に `amount`（申請額）・`scheduledPayDate`・`due`（支払予定日が来たか）・`taxKind`・`feeYen`（出金手数料）・`netAmount`（振込額＝申請額−手数料。申請中は見込み）・`hasBankInfo`（口座情報の有無。口座の中身は返しません）
- 運営者が申請ごとに決めます（要 `rewards:write`・**人の了承を得てから**）: `POST /api/bot/payout-requests/{id}` に `{"action":"pay"}`（振り込み**済み**にする記録。この API はお金を動かしません。支払予定日の前は `allowEarly:true`、口座情報が無いときは `confirmMissingBank:true` が要ります。稼働停止中・BAN・削除済みの人は `NOT_PAYABLE` で払えません。ブラックリストに当たる人は、申請が自動で却下され、報酬が元に戻って、保留になります）か `{"action":"reject","reason":"理由"}`（却下。理由は本人のポータルに出て、また申請できます）。管理画面は「報酬支払い」と「連携設定」の「報酬の支払いルール」

### アフィリエイターの新規登録の受付 `GET/PUT /api/bot/settings/affiliate-signup`
どのセンターも**最初は「受け付ける」**です。止めると、登録ページ（`/join/{slug}`）は「現在、新規登録を受け付けていません」と出て、2ティア紹介リンク（`?ref=…`）からも登録できません。
```bash
curl -s https://line-affi.com/api/bot/settings/affiliate-signup -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 affiliates:read
curl -s -X PUT https://line-affi.com/api/bot/settings/affiliate-signup -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"enabled":false}'      # 要 affiliates:write。再開は {"enabled":true}
# → { "success": true, "settings": { "enabled": false } }
```
- 止めても変わらないもの: 運営者が送る招待、登録済みのアフィリエイター、止める前に申し込んで確認メールが届いている人
- 管理画面は「連携設定」の「アフィリ向け表示設定」の「アフィリエイターの新規登録を受け付ける」

### 明らかな不正者の自動ブラック `GET/PUT /api/bot/settings/auto-black`

1時間に1回、センターごとに調べます。ルールごとの `action` が `"black"` なら、当たった人を**ブラック**（停止。ログインと新しい報酬が止まり、未払いの報酬は取り消し）にし、どのルールで止めたかを「不正アラート」（確定済み）に残します。`"notify"`（通知だけ）なら、状態も報酬も変えずに、未確認の「不正アラート」を立てて運営者へメールで知らせます（同じ人・同じルールは7日に1回まで。止めるかは人が不正アラートの画面で決める）。運営者・スタッフ本人のアフィリエイター登録は対象外。他センターへの不正情報の共有はしません。

| ルール | 既定 | 中身 |
|---|---|---|
| `SAME_IP_REGISTRATIONS` | オン・`action` "black"・`minCount` 5・`windowHours` 24 | 同じ人の紹介で、同じ接続元（IP）からの登録が、時間内に何件以上 |
| `LOW_BACKEND_CVR` | オン・`action` "notify"（比べる相手が少ないうちは当てにならないので、止めずに知らせる）・`minRegistrations` 100・`minAgeDays` 14・`ratio` 0.2・`minCenterRegistrations` 300・`minCenterPurchases` 5 | 登録から何日たった紹介の登録が何件以上あり、その人の購入の率がセンター平均の何倍以下（0.2 = 1/5）。センターの登録・購入が少ないうちは判定しない |

```bash
curl -s https://line-affi.com/api/bot/settings/auto-black -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 affiliates:read
curl -s -X PUT https://line-affi.com/api/bot/settings/auto-black -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"rules":{"SAME_IP_REGISTRATIONS":{"minCount":8},"LOW_BACKEND_CVR":{"action":"notify"}}}'   # 要 affiliates:write。指定した項目だけ変わる
```
- 範囲外の数字は 400（`OUT_OF_RANGE`）、`action` が "black" / "notify" 以外は 400（`INVALID_VALUE`）。変える前に人の了承を得る（誤判定は報酬の取り消しを伴う）
- 状態を手で変えるのは管理画面のアフィリエイター一覧の「ステータス」（有効・保留・ブラック）か `PATCH /api/bot/affiliates/{id}` の `status`。GS の「無効」は「削除」にあたります

### 新規登録時の自動不正チェック `GET/PUT /api/bot/settings/signup-fraud-check`
どのセンターも**最初はオフ**（今までどおり）です。オンにすると、アフィリエイターが自分でメールのリンクから登録を終えた直後に、他のセンターで不正と判定されて停止中の人と、**ログインアカウント・メールアドレスが同じ**かを調べ、一致したら自動で「保留」にします（自動の BAN はしません。BAN は人が一覧から手で行います）。
```bash
curl -s https://line-affi.com/api/bot/settings/signup-fraud-check -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 affiliates:read
curl -s -X PUT https://line-affi.com/api/bot/settings/signup-fraud-check -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"enabled":true,"action":"PAUSE"}'      # 要 affiliates:write（指定した項目だけ変わる）
# action: "PAUSE"（保留。報酬は承認待ちになる）か "BAN"（停止して、未払いの報酬を取り消す）
# → { "success": true, "settings": { "enabled": true, "action": "PAUSE" } }
```
- **保留**にした人は、`GET /api/bot/affiliates` の `autoHoldReason` が `SIGNUP_SHARED_FRAUD` になり、管理画面の一覧に「自動で保留中」と出ます。解除は `PATCH /api/bot/affiliates/{id}` で `{"status":"ACTIVE"}`（自動の印も消えます）か、一覧の「保留を解除する」
- 自動の **BAN** はありません（誤判定で、無関係の人が登録の直後に BAN され未払いの報酬が取り消されるのを避けるため）。BAN にするかは人が一覧から決めます
- 調べるのは、本人がメールのリンクで登録した人だけです。運営者が API・CSV で登録した人には使いません（任意のアドレスが他センターで停止されたかを調べる道具になってしまうため）
- どちらにしたかは `GET /api/bot/audit-log` に残ります（`keyPrefix: auto`）。オンにする前に、**人の了承を得てください**（登録した人の報酬に影響します）
- 管理画面は「連携設定」の「アフィリ向け表示設定」の「新規登録のときに、他のセンターの不正情報と自動で照らし合わせる」

### 移行元（GS 系）のアフィリエイター・配信リストを大量に取り込む `/api/bot/affiliates/import`

移行元の「アフィリエイター管理」などから書き出した CSV（Shift_JIS / UTF-8 を自動判別、数万件でも可）を、検証してから実行します。**取り込んだ人に、招待・案内などのメールは一切送りません。** 同じ CSV を何度取り込んでも二重には登録されません（メールアドレスが同じ人は飛ばす）。

1. **預ける・検証**: `POST /api/bot/affiliates/import`（要 `affiliates:write`。配信リストは `kind: "SUBSCRIBER"` で要 `leads:write`）。`csvBody`（UTF-8 の文字列。1回30万文字まで）または `base64Body`（元のバイト列を base64 に）。大きいときは `part`（0 から）と `parts`（全体の数）で分け、2回目以降は最初の応答の `jobId` を付ける。最後の `part` で読み取り・検証して、`preview`（登録予定・登録済み・エラー件数と例・ブラックや配信対象外の人数・プラン上限を超える人数）と `columnMap`（列の割り当て、0 始まり）・`headerRow` が返る。**この時点では何も登録されません。**
2. **列を直す**（違うとき）: `PATCH /api/bot/affiliates/import/{jobId}` に `{"columnMap":{"email":3,"sourceId":0,"refSourceId":7}}`。項目名は `email`・`sourceId`（移行元のID）・`displayName`・`lastName`・`firstName`・`lastNameKana`・`firstNameKana`・`rankName`・`status`・`mailPermission`（メール送信）・`refSourceId`（紹介者＝2ティアの親の移行元ID）・`refCode`・`registeredAt`・`phone`・`postalCode`・`prefecture`・`city`・`address`。配信リストは `email`・`listStatus`・`registeredAt`・`unsubscribedAt`。`null` でその項目は取り込まない。`createMissingRank`（既定 true）も指定できる。接続IPアドレスは取り込まない。
3. **実行**: `POST /api/bot/affiliates/import/{jobId}`（検証済みのものだけ。`dryRun` は付けない＝1. が確認の段階）。裏で数百件ずつ進み、止まっても続きから再開される。
4. **進み具合**: `GET /api/bot/affiliates/import/{jobId}`（要 `affiliates:read`。配信リストは `customers:read`）。`status`（`READY` 検証済み / `QUEUED` / `RUNNING` / `DONE` / `FAILED` / `CANCELED`）、`total`・`processed`・`created`・`updated`・`skipped`・`errors`、`referrers`（紹介者のつなぎ `{total, done}`）。`?format=csv`（`&only=errors` でエラーの行だけ）で結果のCSV（行・移行元ID・メール・結果・理由・紹介元）。途中でやめるのは `DELETE`（登録済みの人は残る）。

読み替え: 状態 有効→有効 / 保留→保留 / 無効→保留 / **ブラック→停止（他センターには共有しない区分 `IMPORT_BLACK`）**。メール送信「配信対象外」→メールを送らない設定。登録日時は日本時間として読み、そのまま登録日にする（読めなければ取り込んだ日）。ランクは名前で結ぶ（無ければ作る）。紹介者は全員の登録が終わってから、移行元のIDで結ぶ（親が後ろの行でも可。見つからない・自分自身・輪になる場合は結ばず、結果に理由を残す。すでに紹介元がある人は変えない）。プランの上限を超えた分は登録せず、結果に理由を残す。配信リストは、配信停止・不達・迷惑メールの人を**配信しない状態のまま**登録し、すでに配信中の人が移行元で停止なら停止にする。

### 手入力のブラックリスト `/api/bot/blacklist`
メールアドレス・電話番号・口座番号を登録しておくと、**登録のとき**と**報酬を支払うとき**に同じ人が見つかり次第、自動で「保留」にします（BAN ではありません）。**登録が空のうちは、今までと何も変わりません。**
```bash
curl -s https://line-affi.com/api/bot/blacklist -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 一覧。要 affiliates:read
curl -s -X POST https://line-affi.com/api/bot/blacklist -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"kind":"EMAIL","value":"bad@example.com","note":"返金の不正"}'     # 要 affiliates:write
# kind: EMAIL（メールアドレス）/ PHONE（電話番号）/ BANK_ACCOUNT（口座番号。bankCode・branchCode で金融機関・支店に絞れる）
curl -s -X PATCH https://line-affi.com/api/bot/blacklist/{id} -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' -d '{"note":"…"}'   # 値・メモを直す（種類は変えられない）
curl -s -X DELETE https://line-affi.com/api/bot/blacklist/{id} -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 外す
```
- メールは Gmail のドット・`+` の違いを同じとみなします。電話は数字だけ（ハイフン・全角・+81 の違いは吸収）。口座は頭の 0 を除いて照合します。**口座番号だけだと、ほかの銀行の同じ番号の人にも当たる**ので、`bankCode`（4桁）・`branchCode`（3桁）も入れるのがおすすめです
- 保留にした人は `GET /api/bot/affiliates` の `autoHoldReason` が `BLACKLIST_SIGNUP`（登録時）か `BLACKLIST_PAYOUT`（出金時）になり、管理画面の一覧に「自動で保留中」と出ます。解除は `PATCH /api/bot/affiliates/{id}` の `{"status":"ACTIVE"}` か、一覧の「保留を解除する」。すでに停止中・BAN の人の状態は変えません
- 出金: 当たった人は、「振り込み済みにする」のときに止めて保留にします（報酬は確定のまま残ります。申請は却下してください）
- 登録を足す・外すのは、**人の了承を得てから**（人の支払いに影響します）。管理画面は「ブラックリスト」
- 自動で保留にしたことは `GET /api/bot/audit-log` に残ります（`keyPrefix: auto`）

### IP の入力照合 `POST /api/bot/ip-lookup`
運営者が**調べたい IP アドレスを入力したときだけ**、同じ接続元からのクリック・登録を探します（不正の調査用）。システムは IP そのものを保存していないので、サーバーが入力された IP を、クリックを記録したときと同じ方式でハッシュにして照らし合わせます。**入力した IP は保存せず、ログにも出しません。**
```bash
curl -s -X POST https://line-affi.com/api/bot/ip-lookup -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"ip":"203.0.113.5","days":30}'      # 要 affiliates:read。days は 1〜365（既定 30）
# → { "success": true, "result": { "days": 30, "totalClicks": 12, "totalRegistrations": 3, "truncated": false,
#      "byAffiliate": [{ "affiliateId": "…", "displayName": "…", "status": "ACTIVE", "clicks": 9, "registrations": 3, "firstAt": "…", "lastAt": "…" }],
#      "recent": [{ "at": "…", "affiliateId": "…", "displayName": "…", "offerName": "…", "lpNumber": 1, "registered": true }] } }
```
- **GET ではなく POST** です（IP が URL やアクセスログに残らないように）。IPv4 / IPv6 以外の文字列は 400
- このセンターの紹介リンクのクリックだけが対象です。「登録」は、そのクリックからできたリード（メール登録・LINE 友だち追加）
- 見つかった人を止めるかは人が判断してください（同じ会社・同じ回線・携帯キャリアの共有 IP でも一致します）。止めるときは `PATCH /api/bot/affiliates/{id}` の `status`
- 管理画面は「IP照合」（アフィリエイトのメニュー内）

### 運営者情報 `GET/PUT /api/bot/settings/operator-info`
会社名・フリガナ・住所・電話番号を、センターごとに1つ持てます。メールの差出人情報・特定商取引の表示に使えるようにするための入れ物で、**いまは保存と表示（管理画面の見本）だけ**です。実際のメールや販売ページには、まだ自動では入りません（今までどおり）。
```bash
curl -s https://line-affi.com/api/bot/settings/operator-info -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 offers:read
curl -s -X PUT https://line-affi.com/api/bot/settings/operator-info -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"companyName":"株式会社テスト","companyNameKana":"カブシキガイシャテスト","address":"東京都千代田区1-1","phone":"03-1234-5678"}'   # 要 offers:write
# 指定した項目だけ変わる。空文字か null でその項目を消す
# → { "success": true, "operatorInfo": { "companyName": "…", "companyNameKana": "…", "address": "…", "phone": "…" }, "isEmpty": false }
```
- 住所は改行できます（複数行）。電話は数字8桁以上（ハイフン・+81 可）。長さの上限: 会社名・フリガナ100、住所300、電話30
- 会社の登記や特商法の表記に使う内容なので、**正式な表記を人に確かめてから**入れてください（AI が推測で入れない）
- 管理画面は「連携設定」の「運営者情報」

### アフィリエイター個別の報酬設定 `/api/bot/affiliate-rewards`
特定のアフィリエイターだけ、案件×成果の種類（`LINE_FOLLOW` / `EMAIL_OPTIN` / `PURCHASE`）ごとの報酬を個別に上書きします。**ランク別報酬より優先**され、設定が無い人は今までと同じ（ランク別 → 案件の標準）です。
```bash
curl -s -X POST https://line-affi.com/api/bot/affiliate-rewards -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"affiliateId":"<ID・紹介コード・メールのどれか>","offerId":"<案件ID>","trigger":"EMAIL_OPTIN","rewardYen":800,"note":"特別単価"}'   # 要 rewards:write（PATCH も同じ）
# 購入は円か率: {"trigger":"PURCHASE","rewardPercent":15}。有料案件は trigger を省略できる。0 は「この人には報酬を付けない」
curl -s "https://line-affi.com/api/bot/affiliate-rewards?affiliateId=…&offerId=…" -H "Authorization: Bearer $LINE_AFFI_API_KEY"      # 設定中の一覧。要 offers:read
curl -s "https://line-affi.com/api/bot/affiliate-rewards?history=1" -H "Authorization: Bearer $LINE_AFFI_API_KEY"                    # 変更の記録（新しい順）
curl -s -X DELETE https://line-affi.com/api/bot/affiliate-rewards/{id} -H "Authorization: Bearer $LINE_AFFI_API_KEY"                  # 解除（ランク別 → 標準に戻る）。要 rewards:write
```
- 設定した**後に発生する報酬**から変わります（これまでの報酬は変わりません）。設定・変更・解除はすべて履歴に残ります（前の値・後の値・管理画面かAPIか）
- 率は購入だけ（購入額 × 率、1円未満切り捨て）。1件の上限は 10,000,000 円。同じ内容をもう一度送っても何も変わりません（`changed: false`）
- お金に直接かかわるので、**金額は人の指示どおりに**。推測で入れたり、まとめて変えたりしないでください
- 本人のアフィリエイトセンターの案件一覧にも「あなた専用の報酬が設定されています」と出ます。管理画面は「個別報酬設定」

### この人だけ案件を終了にする `GET/POST /api/bot/affiliate-offer-stops`・`DELETE /api/bot/affiliate-offer-stops/{id}`
特定のアフィリエイターにだけ、案件を終了扱いにします（例: 登録の報酬ばかりで購入が無い人）。**その人の画面だけ**、その案件が「この案件は終了しました」になり、紹介リンク・報酬額・素材が出なくなります。**その人経由の新しい報酬（登録・購入）は付きません**（個別報酬設定・ランク別より優先）。他のアフィリエイターと、これまでの報酬は変わりません。終了中の登録・購入は 0円の記録として残り、**運営者の報酬集計の件数には数えます**（その人の報酬の画面・ランクの件数には出しません）。その人はその案件のランキングに載らず、その人にはその案件のランキングを表示しません。その案件の新案件のお知らせ・追加LPのお知らせのメールも、その人には送りません。（案件のお知らせが届くのは、有効（停止・保留でない）・メール通知オン・ブラックリストに当たらない人だけです）
```bash
curl -s -X POST https://line-affi.com/api/bot/affiliate-offer-stops -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"affiliateId":"<内部ID・紹介コード・メール>","offerId":"…","note":"理由"}'     # 要 rewards:write ⚠
curl -s "https://line-affi.com/api/bot/affiliate-offer-stops?affiliateId=…" -H "Authorization: Bearer $KEY"   # 一覧（要 offers:read）
curl -s -X DELETE https://line-affi.com/api/bot/affiliate-offer-stops/{id} -H "Authorization: Bearer $KEY"     # 再開（要 rewards:write）
```
- 報酬が止まる・その人の画面が変わる操作なので、**実行する前に人の了承を得てください**。操作は記録（`GET /api/bot/audit-log`）に残ります
- 管理画面は「個別報酬設定」の「この人だけ案件を終了にする」

### アフィリエイトセンターのデザイン設定とサポートサイト `GET/PUT /api/bot/settings/portal-design`
アフィリエイトセンター（ポータル・ログイン画面）の見た目と文言を、センターごとに変えられます。**最初は全部未設定で、今までの見た目・文言のまま**です（ロゴは管理画面の「アカウント」）。
```bash
curl -s https://line-affi.com/api/bot/settings/portal-design -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 要 offers:read
curl -s -X PUT https://line-affi.com/api/bot/settings/portal-design -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"accentColor":"#7c3aed","headerLabel":"パートナーセンター","welcomeText":"いつもありがとうございます。","supportSiteUrl":"https://support.example.com"}'   # 要 offers:write
# 指定した項目だけ変わる。空文字か null でその項目を元（今までの見た目・文言）に戻す
# → { "success": true, "design": { "accentColor": "#7c3aed", "headerLabel": "…", "welcomeText": "…", "loginText": null, "supportSiteUrl": "…" } }
```
- `accentColor`: `#rrggbb`（`#rgb` も可）。ヘッダーの肩書き・選択中のメニュー・ロゴ代わりの丸の色になります。**白い背景で読める濃さの色だけ**（薄すぎると `400 COLOR_TOO_LIGHT`）
- `headerLabel`: センター名の下の肩書き（20文字まで。既定は「アフィリエイトセンター」）
- `welcomeText`: ポータルのホームの上に出すあいさつ（300文字まで・文章）／ `loginText`: ログイン画面の案内（200文字まで・文章）
- `supportSiteUrl`: サポートサイトの URL（`https://` だけ）。ポータルとログイン画面に「サポートサイト」のリンクが出ます
- 文言は人に届く表示なので、**内容は人に確かめてから**設定してください。管理画面は「連携設定」の「アフィリエイトセンターのデザイン」（見え方の見本つき）

## 6. ステップメール（`plan.features` に `stepmail` があるとき。要 `stepmail:write`）

```bash
curl -s -X POST https://line-affi.com/api/bot/stepmail/sequence \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"無料講座","steps":[{"stepNumber":1,"subject":"ご登録ありがとうございます","bodyHtml":"<p>…</p>","delayMinutes":0}]}'
```
- **`delayMinutes` は経過時間ではありません。**「登録から X日後の Y時（日本時間）」を `X × 1440 + Y × 60` で表します
  - `0` = 登録直後／`1980` = 1日後の 9時（1×1440 + 9×60）／`4320 + 1200 = 5520` = 3日後の 20時
  - 指定の時刻を既に過ぎている場合（例: 15時に登録した人への「0日後の 9時」）は、すぐ送られます
  - 「登録の30分後」のような分単位の指定は出来ません（時の単位まで）
- 返ってきたシナリオの id を `PATCH /api/bot/offers/{id}` の `stepSequenceId` に入れると、登録と同時に配信が始まります
- 1通ずつの追加・修正は `PUT /api/bot/stepmail/step`（`sequenceId`, `stepNumber`, `subject`, `bodyHtml`, `delayMinutes`）。**既存の番号に送ると上書き**です（応答に上書き前の件名が入ります）
- **配信中のシナリオの途中に足すときは `POST /api/bot/stepmail/step`**（`sequenceId`, `subject`, `bodyHtml`, `delayMinutes`。番号は指定しない）。送る日の順に入る位置を決め、後ろの番号を下げ、途中まで進んでいる購読者の位置も直します（重複・飛ばしが出ない）。新しい回の時刻が過ぎている人には送りません。終わった購読（COMPLETED）は再開しません。PUT で番号を振り直して並べ替えないこと（途中の購読者に同じ回が2度届く・1通飛ぶ）
- シナリオの作成は名前で冪等です。同じ名前で再送しても増えず、`existed: true` と既存の id が返ります（中身は変更されません）
- プランにステップメールの機能が無いと `403 PLAN_FEATURE_UNAVAILABLE`（`/account` の `plan.features` で事前に分かります）
- **1日の送信上限**は2段階です。上限に達したぶんは止まらず、翌日に持ち越されます
  - アカウント全体: `PUT /api/bot/settings/stepmail` `{"dailyLimit":3000}`（`null` で既定に戻す／`0` で上限なし）。今の値と消化状況は `GET` で
  - シナリオごと: `PATCH /api/bot/stepmail/sequence` `{"sequenceId":"…","dailyLimit":1500}`（`null` でアカウントの上限に従う）
  - **取り込んだ大量リストへの配信（リストのクリーニングなど）だけを絞るときは、アカウントを `0`（上限なし）にして、そのシナリオにだけ上限を付けます。** アカウントで絞ると、登録した人へのシナリオまで一緒に止まります
  - 上限はメール配信サービスの月間・1日の送信量に合わせて人と決めてください。急に大きく上げると迷惑メール扱いされやすいので、数日に分けて上げます
- **最後まで届いたら、次のステップへ**: `PATCH /api/bot/stepmail/sequence` `{"sequenceId":"<無料講座>","nextSequenceId":"<マスター講座のご案内>"}`（作成時の `POST` でも可。`null` で外す。同じアカウントのステップだけ・自分自身は不可＝`400 NEXT_SEQUENCE_INVALID`）
  - 最後の通まで届いた人（最後の通が送る条件で飛ばされて終わった人も含む）を、そのとき次のステップに登録します。**移した日時が次のステップの登録日**になるので、`deadlineDays`（「案内から7日間だけ」など）や値段の段の起点になります。1通目は次のステップの1通目の `delayMinutes` どおり
  - 移さない人: 購入で止まった人・配信停止した人・届かなくなった人・別のシナリオへ切り替えた人、次のステップが売っている商品を既に買っている人、既に次のステップに登録がある人（二重に登録しません）。次のステップが停止中・空のときも移しません（あとから移すこともしません）
  - 設定する前に終わっていた人は移りません（設定した後に最後まで届いた人から）。今の値は `GET /api/bot/stepmail` の `sequences[].nextSequenceId`
- **「リストの配信停止」と「シナリオのストップ」は別のことです。取り違えないでください**
  - **リストの配信停止**＝この人にはこのアカウントから一切メールを送らない。**本人の意思**（配信停止リンク・迷惑メール報告・LINE で「もう送らないで」）のときに使います。`PATCH /api/bot/stepmail/subscribers`
    ```bash
    curl -s -X PATCH https://line-affi.com/api/bot/stepmail/subscribers -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
      -d '{"email":"foo@example.com","list":"unsubscribe","reason":"line_optout"}'
    # → { "success": true, "changed": true, "status": "UNSUBSCRIBED", "unsubscribedAt": "…", "unsubscribeReason": "line_optout" }
    ```
    - `reason`: `"api"`（既定）／`"owner_manual"`（運営が決めた）／`"line_optout"`（本人が LINE など別の窓口で停止を希望）。いつ・なぜ止めたかが記録に残ります
    - 戻す: `{"email":"…","list":"resubscribe"}`。**止まっていたシナリオは再開しません**（下のシナリオの口で1本ずつ）
      - 本人が自分で止めた人（理由 `"link"`＝配信停止リンク／`"complaint"`＝迷惑メール報告）は戻せません（`403 SELF_UNSUBSCRIBED`。本人が登録し直すと戻ります）。届かない人も戻せません
      - 理由の記録が無い人（2026年10月7日より前の停止）は、本人が止めた可能性があるので `409 CONFIRM_REQUIRED`。人に確かめてから `"confirmUnknownReason": true` を付けて送ります
    - LINE 側（全自動AI集客システム）で配信解除を受けたときは `POST /api/bot/opt-out`（`lineUserId` でも引けます）。中身は同じリストの配信停止（理由 `"line_optout"`）です
  - **シナリオのストップ**＝そのシナリオ（ステップ）だけを止める。**購入した・別のシナリオへ移した**など運営の都合のときはこちら。他のシナリオや一斉配信は届き続けます。`PATCH /api/bot/stepmail/subscriptions`
    ```bash
    curl -s -X PATCH https://line-affi.com/api/bot/stepmail/subscriptions -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
      -d '{"email":"foo@example.com","sequenceId":"<シナリオの id>","action":"stop","reason":"purchased"}'
    # → { "success": true, "subscriptionId": "…", "changed": true, "status": "COMPLETED", "endReason": "purchase" }
    ```
    - `action`: `"stop"`／`"resume"`（止まった所から再開。1通目には戻しません）。`reason`（stop のとき）: `"api"`（既定）／`"owner_manual"`／`"purchased"`（終わり扱い。次のステップへは移しません）／`"moved"`
    - 再開できないとき: 最後まで届いて終わっている（`409 FINISHED`）／その人がリストから配信停止中（`409 LIST_UNSUBSCRIBED`。先にリストに戻す）
  - 今の状態は `GET /api/bot/stepmail/subscribers?email=foo@example.com`。リストの状態（`status`・`unsubscribeReason`・`unsubscribedAt`）と、シナリオごとの状態（`subscriptions[]` の `status`・`endReason`）が別々に返ります
  - **購入でシナリオを止めるために、リストの配信停止を使わないこと。** 本人が止めたのと区別がつかなくなり、その人には他の案内も一切届かなくなります。購入で止めるのは、案件のグループの設定で自動で行われます
  - **購入者の見張り（毎日自動）**: 買った人（返金されていない購入がある人）が、運営の手・API（理由 `"owner_manual"` / `"api"`）でリストから配信停止になっていたら、自動でリストに戻し、その購入の売り込みシナリオだけ止めます（再開はしません）。本人の意思（`"link"` / `"complaint"` / `"line_optout"`）・不達（`"bounce"`）・理由の記録がない古い停止は戻さず、人数だけ `needsReview` で知らせます（`/api/cron/relist-purchasers`）
  - 他の配信ツール（オートビズなど）でシナリオを「解除」した人を、こちらのリストの配信停止として取り込まないでください。向こうの「解除」はシナリオ単位で、購入したのでステップを外しただけの人も含まれます
- 文面は実際に人へ届きます。送る前に、件名と本文を人に見せて了承を得てください
- **差出人は案件ごとに変えられます。** 同じアカウントで運営者の違う案件を扱うときは、`PATCH /api/bot/offers/{id}` に `fromName`（差出人名）・`fromEmail`（差出人アドレス）・`replyTo`（返信先・任意）を入れてください。この案件のステップメール・登録完了メール・購入完了メールがその差出人から届きます
  - 空（`""` / `null`）ならアカウントのメール配信設定の差出人に戻ります
  - `fromEmail` のドメインは案件ごとに違ってかまいません。ただし**配信サービスで認証（SPF・DKIM）済みのドメイン**にしてください
  - 認証されていないドメインを入れると保存はされますが、応答の `warnings` に認証の手順が入り、**認証が済むまでこの案件のメールは送られません**（別のドメインに差し替えて送ることはしません）。認証が済めば10分ほどで自動的に再開します。状態は `GET /api/bot/account` の `offers[].sender.domainStatus` と `nextSteps` で分かります
- **通知メールの設定も全部 API でできます。** `POST /api/bot/offers`（作成時）でも `PATCH /api/bot/offers/{id}`（あとから）でも、次の項目を渡せます。今の値は `GET /api/bot/offers/{id}` の `offer` に入っています
  - 送信ON/OFF（true / false）: `notifyCustomerOnOptin`（顧客への登録完了メール）・`notifyCustomerOnPurchase`（顧客への購入完了メール）・`notifyOwnerOnOptin`（運営者本人への登録通知）・`notifyOwnerOnPurchase`（運営者本人への成約通知）・`notifyAffiliateOnCommission`（アフィリエイターへの報酬通知）
  - `showInRanking`（true / false。既定 true）: この案件のランキングを出すか。**ランキングは案件ごと**で、その案件の紹介（登録）だけを数えます（ほかの案件は足しません）。公開中で true の案件ごとに、アフィリエイトセンターのホーム（TOP5）とランキングの画面（TOP10）に出ます（センター全体のオン・オフは別にあります）。その案件を終了扱いにした人は載らず、その人にはその案件のランキングを出しません
  - `portalNoticeHtml`（文章か HTML。`null` / `""` で消す。20,000文字まで）: アフィリエイターへのお願いメッセージ。アフィリエイトセンターのこの案件のカードに出ます（終了扱いの人には出ません）。センター全体のお知らせ（`/api/bot/settings/portal-notice`・案件一覧の一番上）とは別
  - 顧客への登録完了メールの文面: `optinEmailSubject`（件名）・`optinEmailBody`（本文）。購入完了メールは `purchaseEmailSubject` / `purchaseEmailBody`。差し込み変数 `{{name}}` `{{offer}}` `{{amount}}` `{{member_url}}` `{{gift_url}}` が使えます
  - 差出人名は2つ: `fromName`＝ステップメール・登録完了（講師名）、`noticeFromName`＝購入完了などの決済の案内（会社名・プロジェクト名・事務局名。例「KAP運営事務局」。空なら「運営事務局」）。人に確認して入れる
  - **有料の商品（`BACKEND`）は、`purchaseEmailBody` に「購入後に何をすればいいか（受け取り方・ログイン先）」と「問い合わせ先（サポートLINE・メールなど）」を必ず書く**。会員サイト（`/api/bot/member-content`）で教材を渡さない商品（ツール・外部サイトの会員権・LINEでのサポートなど）は、このメールが唯一の案内になります。会員サイトに教材が無い商品では、会員サイトへのリンクは自動で付かず、`{{member_url}}` も空になります。書く内容は人に確認してください
  - 差出人 `fromName` / `fromEmail` / `replyTo`（上の項目）
- **登録通知・成約通知は、ジョイント先などにも届けられます。** `POST /api/bot/offers` / `PATCH /api/bot/offers/{id}` に `optinNotifyExtraTo`（登録通知）・`purchaseNotifyExtraTo`（成約通知）をメールアドレスの配列で入れてください（10件まで）
  - 指定すると置き換えます。`[]` / `null` で追加送信先なし
  - 宛先を入れるには `customers:read` 権限が要ります（通知には登録者・購入者の名前とメールアドレスが入るため）。空にするだけなら要りません。宛先は人に確認してから入れてください
  - 運営者本人への通知（`notifyOwnerOnOptin` / `notifyOwnerOnPurchase`）のON/OFFに関係なく、ここの宛先には届きます。1通ずつ別々に送るので、宛先どうしでアドレスは見えません
  - 顧客向けのメール（登録完了・購入完了）の控えは送りません。会員ページのログインURLなど、お客様本人用の情報が入っているためです

### 登録できないアドレス（迷惑メールの報告をした人）`/api/bot/email-blocks`
迷惑メールの報告（Resend / SES の complaint、名簿の取り込みの「迷惑」）を受けたアドレスは、**自動で**「登録できないアドレス」に入り、二度と登録できません（オーナー指示 2026-10-18）。
- 止まる入口: メール登録（`/api/email-optin`・`embed.js` のフォーム）・ギフト・ウェビナー登録・予約・`POST /api/bot/leads`（その1件は `skipped`・`reason: "blocked_email"`）・会員の手での追加（`blocked` に入る）・購読者の手での追加・`/api/stepmail/transition`（`blocked`）・名簿の取り込み
- 登録しようとした人には「このアドレスは以前当社のご案内に対してスパム報告をされているために二度とご登録できません」と出ます（`403`・`code: "EMAIL_BLOCKED"`）。報酬も付きません
- 範囲は「当社」＝そのセンターと、同じ人が OWNER のほかのセンター。ほかの運営者のセンターには広げません
- 購入（決済）は止めません。ただし購入してもステップメールの名簿には入れません（購入の案内メールは届きます）
```bash
curl -s "https://line-affi.com/api/bot/email-blocks?q=yahoo" -H "Authorization: Bearer $LINE_AFFI_API_KEY"            # 一覧（要 customers:read）
curl -s -X POST https://line-affi.com/api/bot/email-blocks -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"email":"someone@example.com","note":"電話で苦情"}'          # 手で入れる（要 stepmail:write）
curl -s -X DELETE "https://line-affi.com/api/bot/email-blocks?id=<id>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 外す（誤報告のとき・要 stepmail:write）
```
- **外すのは、人に確かめてから**（誤報告と分かったときだけ）。管理画面は「メールの配信 → 登録できないアドレス」（`/admin/stepmail/blocked`）

## 外部サービスへの直接紹介リンク（申し込みを外部で受ける案件）

申し込みや会員登録を、このシステムではなく**外部のサービス**で受ける案件のための仕組みです（提携先のアフィリエイトプログラム、紹介制度のあるサービスなど）。
アフィリエイターは自分の紹介リンク（例: `https://example.com/{その人の紹介コード}/join`）を登録し、その人の紹介で来た人が LP の申し込みボタンを押すと、そのリンクへ直接進みます。
このシステムは申し込み情報も決済も扱わず、報酬の計算もしません（登録報酬は `0` を指定して「報酬なしで確定」にできます）。

### 1. 案件に設定する `PATCH /api/bot/offers/{id}`（作成時の `POST` でも可）
```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$OFFER_ID -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"referralLinkLabel":"○○クラブ","referralLinkPrefix":"https://example.com/","referralLinkDefault":"https://example.com/OWNERCODE/join"}'
```
| 項目 | 内容 |
|---|---|
| `referralLinkLabel` | サービス名（提携先の名前）。アフィリエイターの入力欄の名前になる。人に聞く |
| `referralLinkDefault` | **必須**。紹介元が分からない人・紹介者が未登録のときの行き先＝オーナー自身のリンク。人に聞く |
| `referralLinkPrefix` | 登録を許す URL の先頭。アフィリエイターが無関係なサイトを登録するのを防ぐ。**設定を強くおすすめ** |
| `referralLinkRequired` | `true` にすると、アフィリエイターの登録（募集URLのフォーム）で、自分の紹介リンクの入力が**必須**になる。入れないと登録が完了しない。既定は `false`。紹介者のひも付けを厳格にしたい案件で使う（提携先にまだ登録していない人は、先に提携先の登録が要る、と人に伝える） |

`referralLinkLabel` と `referralLinkDefault` は両方そろえます（片方だけは `400 INVALID_REFERRAL_CONFIG`）。やめるときは両方を `null`。

⚠ **設定する前に、人に次を確認してください。** 提携先のサービスには、インターネットでの集客・勧誘の可否や、表示・表現の制限が定められていることがあります。
その方法が提携先の規約で認められているかは、利用者自身の責任で確認する必要があります（規約違反による資格の停止などについて、このシステムは責任を負いません）。
確認が取れていない場合は、設定を進めず、人に確認を促してください。

### 2. LP の申し込みボタンを向ける
`GET /api/bot/offers/{id}` の `integration.snippets.referralButton`（と `embedScript`）を、申し込みボタンの場所に貼ります。
ボタンの向け先は `integration.urls.referral`（`https://line-affi.com/go/ref/{slug}`）です。
- このボタンは、その案件の LP に限らず、**同じアカウントのどの LP に置いてもよい**（例: 無料登録 → ステップメール → 販売ページ、の販売ページ）
- 紹介元は `embed.js` が引き継ぎます。紹介リンクで来たときの `?clickId=` をブラウザに覚えておき、サイト内でページを移動してもボタンに付けます
- ステップメール内のリンクは、案件に登録してある LP のドメイン（`externalLpUrl` / `additionalLpUrls` / `thankYouExternalUrl`）宛てなら、自動で `?clickId=` が付きます。メインLPと同じドメインの販売ページなら、何も登録しなくて付きます。**販売ページが別ドメインなら、有料商品の案件（`offerType: BACKEND`）を作り、その `externalLpUrl` に販売ページを入れてください**（登録が無いドメインには付きません）
  - ⚠ **無料登録の案件（`OPTIN`）の `additionalLpUrls` に販売ページ・申込ページ・サンクスページを入れないこと。** `additionalLpUrls` はアフィリエイターのポータルに「紹介用のLP」としてそのまま表示されます
- `embed.js` を置けない場合は、ボタンの href に自分で `?clickId=…` を付ければ同じ動きになります
- **LINE の中から申し込みへ案内するとき**は、申し込みページ・販売ページの素の URL や提携先のリンクを貼らず、固定リンク `integration.urls.lineSpecial` を使います（オプトインの案件の `specialPageUrl` に販売ページを入れる）。人ごとにリンクを作れる配信なら `GET /api/bot/referral/resolve` の `url`。→「紹介者のひも付けを切らさない決まり」

### 3. アフィリエイターのリンクを登録する
- 本人が入力: 募集URL（`urls.affiliateJoin`）の登録フォームと、ポータルの「登録情報」に入力欄が出ます（任意・あとから入力可）
- API で登録: `POST /api/bot/affiliates` か `PATCH /api/bot/affiliates/{id}` に `"referralLinks":[{"offerSlug":"…","url":"https://…"}]`。`url: null` でその案件の分を削除。先頭が合わないと `400 INVALID_REFERRAL_URL`
- 未登録の人には、ポータルのホームと案件一覧の上に「紹介リンクがまだ登録されていません。このままだとあなたの紹介になりません」という赤い案内と、登録情報へのボタンが出ます（必須でない案件でも出ます）。`referralLinkRequired: true` の案件では、登録情報でリンクを空にして保存することもできません
- 未登録の人は `GET /api/bot/affiliates` の各行の `referralLinks` が空。`/account` の `offers[].referral` に `registered / affiliates`（登録済みの人数 / 全体）が出ます。未登録の人の紹介で来た申し込みは、オーナーの既定のリンクへ進みます

### 4. 確認
`curl -sI "https://line-affi.com/go/ref/{slug}?clickId=<テスト用リンクを踏んで得た clickId>"` の `Location` が、そのアフィリエイターのリンクになっていること。`clickId` なしなら既定のリンクになります。
外部のサービスでの成約は、このシステムからは分かりません（ボタンが押された回数だけ記録されます）。

### 有料の商品は、売り手の特定商取引法の表記を必ず設定する
`PATCH /api/bot/offers/{id}` に `{"tokushohoUrl":"https://…/tokushoho.html","privacyUrl":"https://…/privacy.html"}`。購入ページの下のリンクがこの2つになります。**未設定だと Line-Affi 運営の表記（別の会社名・別の料金）が出ます。** 表記のページが無ければ、人に作ってもらってください（販売業者・所在地・連絡先・販売価格・支払方法・引き渡し時期・返品の条件）

## 7. LP をつなぐ（最重要）

紹介リンクは訪問者を `https://あなたのLP/?clickId=xxxx` に着地させます。
**LP の CTA がこの clickId を引き継がないと、登録されても紹介者に報酬が付きません**（エラーにならず静かに失敗します）。

引き継ぎは `integration.snippets` が行います。`POST` / `GET {id}` / `PATCH` のレスポンスに入っています:

| キー | 貼る場面 |
|---|---|
| `embedScript` | 常に必要。ページに1つだけ |
| `lineButton` | `captureMode` が `LINE` / `LINE_EMAIL` |
| `emailForm` | `captureMode` が `EMAIL` / `LINE_EMAIL` |
| `purchaseButton` | `offerType` が `BACKEND` |

- 返ってきたキーのものだけ貼る。**変えてよい**: 文言、class、style、子要素。**変えてはいけない**: `data-lineaffi-*` 属性、`href`、`data-offer-id` / `data-owner-id`、`input` の `name="email"`
- フォームの送信・エラー表示・サンクスページへの遷移は `embed.js` が行います。自前の submit ハンドラや fetch は書かない
- **外部の販売ページ**（Line-Affi の外に置いた有料商品のセールスページ）にも `embedScript` を置いてください。**embed.js を入れれば、紹介元と購読者の印が購入ページへ引き継がれます**:
  ページの URL（無ければ同じタブで覚えた値。sessionStorage でタブの中だけ）にある `clickId`・`lref`・`sub`/`sig`・`ssid`/`ssig`・`lid`/`lsig` を、ページ内の Line-Affi の購入ページ（`https://line-affi.com/purchase/…`）へのリンクに付けます。
  既に付いている値は上書きしません。形の合わない値は付けません。後からスクリプトで差し込まれたリンクにも、押した時点で付きます。
  購入ページへのリンクが計測ドメイン（`https://track.あなたのドメイン/purchase/…`）なら、`embed.js` を計測ドメインから読み込むか、`https://line-affi.com/embed.js?o={ownerId}` にしてください（`ownerId` は `/account` の値）。それ以外のホストでも `data-lineaffi-purchase` を付けたリンクには付きます
- LP 側のリダイレクトや canonical 化で `?clickId=` を落とさないこと
- `integration.warnings` が空でなければ、計測が成立しない設定があります。先に直す
- 公開したら `PATCH` で `externalLpUrl` に URL を入れる。AB テスト用は `additionalLpUrls`（配列。**渡すと全置換**なので、先に `GET {id}` の `additionalLps` を確認）。**入れてよいのは、アフィリエイターに紹介してもらう登録用のLPだけ**です（ポータルにそのまま表示されます。販売・申込・サンクスページは入れない）
- 追加LPを**少しずつ公開**するときは、`additionalLpUrls` の代わりに `additionalLps` を使います（どちらか一方だけ。こちらも**渡すと全置換**）:
  ```json
  { "additionalLps": [
    { "url": "https://example.com/lp-b", "label": "切り口B", "publishAt": null },
    { "url": "https://example.com/lp-c", "label": "切り口C", "publishAt": "2026-10-13T10:00:00+09:00" },
    { "url": "https://example.com/lp-d", "label": "切り口D", "publishAt": "2026-10-20T10:00:00+09:00" }
  ] }
  ```
  - `publishAt`（ISO 8601。`null`・省略 = すぐ公開）が来るまで、そのLPはアフィリエイターの画面に出ません。来たら1時間以内に、センターのアフィリエイター（メール通知 ON の人）へ「新しいLPが追加されました」のメールが届きます。**すぐ公開・過去の日時ではメールは送りません**
  - 先の `publishAt` を入れるにはキーに `offers:publish` が要ります（全員にメールが飛ぶため）
  - LP番号（`?lp=N`）は、公開日時に関係なく並び順で決まります（上の例なら B=2・C=3・D=4。C が出る前から D は 4 番）。後のLPが公開されても番号はずれません。公開日時がまだのLPの `?lp=N` はメインLPへ転送されます
  - `GET {id}` の `additionalLps` は `{ lp, url, label, publishAt, released }`。`released: false` はまだアフィリエイターに見えていないLP
  - 管理画面の案件の編集でも、LPごとの「公開日時」と、「最初の◯本だけ出して、残りを1週間ごとに公開」のボタンで入れられます
- メール登録 → サンクスページ → LINE ボタン、と**ページをまたぐ**場合、サンクスページの URL に clickId は付きません。
  LP で clickId を localStorage に保存し、サンクスページで `https://liff.line.me/{liffId}?state={clickId}` を組み立てます。clickId が無い訪問者は通常の友だち追加リンクのままにします

## 8. 実機テスト（人と一緒に）

```bash
# テスト用アフィリエイター（冪等）と紹介リンク（冪等）。要 affiliates:write
curl -s -X POST https://line-affi.com/api/bot/affiliates -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"email":"lp-test@example.com","displayName":"疎通テスト"}'
curl -s -X POST https://line-affi.com/api/bot/affiliate-links -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d "{\"offerId\":\"$OFFER_ID\",\"email\":\"lp-test@example.com\"}"   # → trackUrl
```
1. `curl -sI "$trackUrl"` → `302` で `Location: https://LP/?clickId=…`
2. LP を開き、CTA の href を確認（LINE: `https://liff.line.me/…?state=<clickId>` / 購入: `…/purchase/{slug}?clickId=<clickId>`）
3. 人にスマホで通してもらう。**LINE は「ブロック → リンクを踏む → 友だち追加 → 最後にブロック解除」の順**。
   既に友だちの人には友だち追加のイベントが発生せず、報酬が付きません（仕様）
4. `GET /api/bot/affiliates?email=lp-test@example.com` の実績に反映されたか確認。付かなければ `GET /api/bot/line/status`

過去に別の紹介リンクを踏んだことがある人が友だち追加すると、報酬の確定が数分遅れることがあります（最新のリンクを優先するための待ち時間）。

## 9. 公開する — 必ず人の了承を得てから（要 `offers:publish`）

```bash
curl -s -X PATCH https://line-affi.com/api/bot/offers/$OFFER_ID -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"showToAffiliates":true}'
```
全アフィリエイターに「新案件」メールが**1回だけ**飛び、**取り消せません**。
手順8が通っていること、報酬額、通知が飛ぶ人数（`/account` の `plan.limits.affiliates.current`）を人に伝え、了承を得てから実行します。
**公開日時は案件ごとではなく、案件グループ（`groupTag`）で1つです**（オーナーの決まり。オプトも販売も同じ日時）。アフィリエイターへの公開（`showToAffiliates`）は案件ごとです。

- 今の公開日時: `GET /api/bot/offer-groups`（要 `offers:read`）→ `groups[]` に `groupTag`・`launchAt`・`launchProcessedAt`（公開日時の通知を済ませた時刻）・`offers[]`（`showToAffiliates`・`affiliateNotifiedAt` など）。案件の `GET /api/bot/offers` / `GET /api/bot/offers/{id}` の `effectiveLaunchAt` もグループの公開日時です（`launchAt` は古い列で、グループのある案件では使いません）
- 変える: `PATCH /api/bot/offer-groups` に `{"groupTag":"phantom-spb","launchAt":"2026-10-10T10:00:00+09:00"}`（要 `offers:write` と `offers:publish`）。グループの全案件に効きます。`null` で 2099-01-01（未定）
- 新しいグループは、最初の案件を作るときの `launchAt`（無ければ **2099-01-01**）がグループの公開日時になります。**既にあるグループの案件に `launchAt` を送ると `400 LAUNCH_AT_IS_PER_GROUP`**（`POST` でも `PATCH /api/bot/offers/{id}` でも。黙ってグループの日時を変えると、同じグループの他の案件の通知まで動くため）。`offer-groups` で変えてください
- **案件を別のグループへ移す前の確認**: `GET /api/bot/offer-groups/move-impact?offerId=…&groupTag=移す先`（要 `offers:read`。何も変えません）。`pendingReferrals`（進行中の紹介＝登録後まだ確定していない件数）と `pendingCommissions`（未確定の報酬＝承認待ちの件数）を返します。すでにある紹介・報酬は変わらず、**移した後に発生する購入報酬の付き先が、移した先のグループの紹介者に変わります**。移す（`PATCH /api/bot/offers/{id}` の `groupTag`）前に件数を人に見せて、了承を得てください。管理画面は、移す前の確認画面に同じ件数を出します
- **案件グループの管理**（要 `offers:write`）: 作成 `POST /api/bot/offer-groups`（`{"groupTag":"新しい名前"}`。案件はまだ無く、公開日時は 2099＝未定で通知は飛びません）、名前の変更 `POST /api/bot/offer-groups/rename`（`{"groupTag":"今の名前","newGroupTag":"新しい名前"}`。案件・ウェビナー・個別相談のグループ名も一緒に変わります。**外部のシステムがこの名前を使っていれば、そちらも直す必要があります**。新しい名前が使用中なら 400）、管理画面の並び順 `POST /api/bot/offer-groups/reorder`（`{"order":["上のグループ","次のグループ"]}`。書かなかったものは後ろに続く）。`GET /api/bot/offer-groups` の各グループに `sortOrder`（小さいほど上）が付きます
- **案件を別のグループへ移す**のは `PATCH /api/bot/offers/{id}` の `groupTag`。**購入の報酬は「同じグループの紹介者」に付くので、移すとこれから発生する報酬の対象が変わります。人の了承を得てから**。公開してある未通知の案件を、公開日時が過ぎたグループへ移すと、すぐ全アフィリエイターへ通知が飛ぶため断られます（管理画面の「案件グループの管理」から移すときも同じ）
- **案件の管理画面での並び順**: `PATCH /api/bot/offers/{id}` の `sortOrder`（0 以上の整数・小さいほど上。同じなら無料オプトインが先・新しい順）。アフィリエイターの画面の並び（`portalSortOrder`）とは別です。`GET /api/bot/offers` は `q`（案件ID・案件名・slug の一部）で探せて、各案件に `sortOrder` と `badges`（`有効` / `アフィリ公開` / `アフィリ非公開` / `アフィリ公開予定` / `有料(月額)` / `有料(1回払い)` / `有料(分割◯回)` / `無料` など）が付きます
- 公開日時が来ると、そのグループで `showToAffiliates:true` の案件にだけ新案件のお知らせが**1回だけ**届きます（オプトが先。販売の案件は、同じグループに公開中のオプトがあれば省く）。**`showToAffiliates:false` の案件は、公開日時が来ても非公開のまま**（勝手に公開しない。販売を非公開にしても購入報酬は付き、ポータルではオプトの案件にバックエンド報酬として出ます）
- **公開（`showToAffiliates:true`）の前に、グループの公開日時を未来にしておく**（過ぎたまま公開しようとすると `400 LAUNCH_AT_REQUIRED`。その場で通知してよいと人が言ったときだけ `notifyNow:true`）。公開日時が先なら、公開しても表示だけで、お知らせは公開日時に届きます（公開してから人が自分で試せる）
- グループの公開日時を今・過去にすると、公開にしてある未通知の案件に数分以内にお知らせが届くので、そういう案件があると `notifyNow:true` が無い限り `400 LAUNCH_AT_IN_PAST`（応答の `pendingOffers` に届く案件）。人が「通知してよい」と言ったときだけ付けます
- グループの無い古い案件（`groupTag` が空）だけは、今までどおり案件ごとの `launchAt`（来たら公開＋通知）
- **報酬は公開日時から**（オーナーの決まり。アフィリエイトの一般的なやり方）。公開日時より前に起きた LINE 友だち追加・メール登録・購入（と、そこから付く2ティア）には報酬を作りません。誰の紹介かの記録（紹介の確定）は残ります。2099-01-01（未定）の間も同じです。
  公開日時より前でも、`showToAffiliates:true` の案件はポータルに出て紹介リンクも発行できます（準備のため）。カードには「公開前：○月○日 ○:○○（日本時間）から報酬が付きます」と出ます。
  友だち追加は「その人が最初に友だち追加した日時」、購入は「購入の記録の日時」で判定します。新案件のお知らせを送り済みの案件は、後から公開日時を未来に動かしても報酬は止まりません。
  人が「公開前のテストで報酬が付かない」と言ったら、これが理由です（不具合ではありません）

アフィリエイターの募集は `urls.affiliateJoin` を案内します。

## 10. ローンチ一式を組む（登録 → 無料動画3本 → 特別編 → 期限付きの販売 → 購入後）

「LP で登録 → LINE で無料動画を3本 → 観た人にだけ特別編 → 登録から○日間だけの値段で販売 → 購入後は会員サイトと購入後の配信」という型を、一度に組む順番です。
各手順の細かい API は既存の節にあります（「」の見出しを読む）。ここでは**順番・つなぎ方・人に頼むこと**だけを書きます。

**始める前に人へ聞くこと**: 商品名と案件グループ名（例 `spb-launch`）／値段の段の表（何日目から・いくら）／無料動画3本と特別編の動画ファイル／LP・視聴ページ・特別ページを置くサイト（ドメイン）／LINE の配信を全自動AI集客システムで行うか／報酬（登録・購入の率・ランク別）／公開する日。

| 手順 | AI がやること | 人がやること |
|---|---|---|
| 10-1 登録ページ | オプトインの案件・LINE 連携・メール・LP のつなぎ | LINE 公式アカウントと LINE Developers のチャネル作成、ドメインの DNS、メール配信のドメイン認証 |
| 10-2 無料動画3本 | 動画のアップロード、視聴ページの設定、観終えた知らせのつなぎ | 動画ファイルを用意する、視聴ページを置くサイト |
| 10-3 特別編 | 特別ページURL、リッチメニューの URL | 特別編の動画・ページの中身を決める |
| 10-4 期限付きの販売 | 販売の案件と値段の段、購入ページへのリンク | 決済（PAY.JP / PayPal）の申し込みとキーの登録、特定商取引法の表記ページ、値段と期間を決める |
| 10-5 購入後 | 購入メール、購入後シナリオ、会員サイトの教材、売り込みの停止 | 購入メールの文面・問い合わせ先の確認 |
| 10-6 アフィリエイト | 報酬・ランク・説明文 | 報酬額の了承、公開の判断 |
| 10-7 全自動AI集客システム | 記号・リッチメニュー・タグのつなぎ | 連携の申し込みと承認 |
| 10-8 確認 | チェックリストを順に確かめる | スマホで実際に通す、テスト購入 |

### 10-1 登録ページ（オプトインの案件・LP）
1. オプトインの案件を作る →「2. 案件を作る」。**この型で使う案件（オプトイン・販売の段すべて）は同じ `groupTag` にする**（「5. 報酬を決める」の `groupTag`）
2. LINE を連携する →「3. LINE を連携する」。LINE の配信を全自動AI集客システムで行うなら `sendLineWelcome: false`（「友だち追加されたときのあいさつ」）
3. メールでも登録を受けるなら →「4. メール配信の設定」「6. ステップメール」
4. LP に計測をつなぐ →「7. LP をつなぐ」

### 10-2 3本の無料動画（動画置き場と視聴ページ）
1. 動画を上げる →「動画（要 `videos:write`）」。3本とも `READY` になるまで待つ
2. 3本を載せた視聴ページ（LP と同じサイトに置く）を、オプトインの案件の `watchUrl` に入れる（`PATCH /api/bot/offers/{id}`）
3. LINE の配信には、応答の `integration.urls.lineWatch`（`https://liff.line.me/{LIFF ID}?state=watch:{オプトインのslug}`）を貼る。開くと本人確認して、視聴ページへ `?clickId=…&w=…` を付けて送ります
4. 視聴ページは、各動画を最後まで観たら `POST https://line-affi.com/api/watch/progress` に `{"w":"<受け取った w>","slug":"<オプトインのslug>","part":1,"total":3}`（2本目は `part:2`…）を送る（`Content-Type: text/plain` で可。`watchUrl` と同じサイトからだけ受けます）。3本そろうと、連携中なら全自動AI集客システムに `watched_all` を1回知らせます。早送りで観終えたことにさせない検査はページ側で行う

### 10-3 特別編（LIFF の special）
→「リッチメニューから特別ページへ渡す `state=special:{offerSlug}`」をそのまま行います。
- `specialPageUrl` はオプトインの案件に入れる（LP か視聴ページと同じホスト）。リッチメニューのボタンには `integration.urls.lineSpecial`
- 3本観た人にだけメニューに特別編を出すのは、全自動AI集客システム側のタグの規則（10-7）
- 特別編を観終えたら `kind:"special"` で知らせる → `watched_special`
- 特別ページから購入ページへのリンクは `embedScript` を置けば `clickId`・`sub`/`sig`・`lid`/`lsig` が自動で付きます

### 10-4 期限付きの販売（値段の段）
登録からの日数で値段が上がる商品は、**段ごとに販売の案件を作り**、同じ `groupTag` で `tierFromDay`（何日目から）と `tierToDay`（何日目の前日まで。最後の段は `null`＝それ以降ずっと）を持たせます。購入ページは、その人の日数に合う段へ自動で移します。

**人に頼むなら、管理画面が早いです**: 販売の案件の編集画面の下「値段の段」で、「何日目から・値段」の表を入れて保存すると、段の案件（slug は `元-2`, `元-3` …）を作り、日数を連続にそろえ、報酬・差出人・購入メール・購入後シナリオを1段目から写します。

API で組むとき:
1. 1段目（いちばん早い・安い段）の販売の案件を作る: `POST /api/bot/offers`（`offerType:"BACKEND"`・`price`・同じ `groupTag`・`purchasePercent`）。特定商取引法の表記（「有料の商品は、売り手の特定商取引法の表記を必ず設定する」）、`noticeFromName`・`purchaseEmailSubject`/`purchaseEmailBody`（「6. ステップメール」の通知メールの項）も入れる
2. 2段目以降を同じように作る。**報酬（`purchasePercent`・ランク別）・差出人・購入メール・`afterPurchaseSequenceId` は1段目と同じ値を送る**（API は写しません）
3. 各段に `PATCH /api/bot/offers/{id}` で `{"tierFromDay":0,"tierToDay":7}`、`{"tierFromDay":7,"tierToDay":21}`、`{"tierFromDay":21,"tierToDay":null}` のように入れる。登録した日が 0日目（日本時間の暦日）。**1段目は 0 から・前の段の `tierToDay` ＝ 次の段の `tierFromDay`・最後だけ `null`**。API は1件ずつしか確かめないので、隙間・重なりを作らないこと（合う段が無い人は定価の段になります）。入れたら `GET /api/bot/channels/{channelId}/offers` の `groups[].tiers` で並びを確かめる
4. 会員サイトの教材は1段目にだけ置く →「会員サイトの教材」
5. 月額の商品（`billingType:"MONTHLY"`）には使えません
6. 日数の起点（登録日）をリンクで渡す:
   - メールのステップ: 販売ページへのリンクに `?sub={{subscribed_ms}}&sig={{sub_sig}}`。書き忘れても、メールのリンク（/go/）が販売ページ・視聴ページ・特別ページ（案件の LP / watchUrl / specialPageUrl のドメインと、購入ページ）へ送るときに、その人のシナリオの開始日で自動で付けます
   - LINE の視聴ページ（`/liff?state=watch:`）・特別ページ（`special:`）: 友だち追加の日（とメール登録の早い方）で自動で付けます。LINE の本人の印 `lid` / `lsig` も付きます
   - 全自動AI集客システムの LINE: `{SALES_LINK:<販売の案件のslug>}` が自動で付ける（10-7）
   - ほかの外部の配信: →「値段の段の署名を外部の配信に渡す」
   - **登録日が分からない人も、いきなり定価の段にはしません**（どの案件でも）。購入ページは、署名つきの `lid` / `lsig` があれば友だち追加の日、それも無ければ**初めて販売ページに来た日**を登録日にして、1段目から数えます（初めて来た日はその端末に1年残す）
   - 自前の販売ページで `sub` / `sig` が無い人は、`https://line-affi.com/api/ladder/first-visit?o=<段の案件のslug>&r=<そのページのURL（エンコード）>` へ送ってください。初めて来た日の `sub` / `sig` を付けて戻します（戻り先は案件の LP・視聴ページ・特別ページのドメインと購入ページだけ。差し込み前の型や偽の `sub` / `sig` が付いていたら外して付け直します）。定価のページへ飛ばさないこと
7. 販売ページ: Line-Affi の購入ページなら `https://line-affi.com/purchase/{どの段のslugでも}?sub=…&sig=…` の1本でよい（その人の段に移り、その段の値段が出る）。**自前の販売ページ（外部 LP）は、embed.js の「値段の段のモード」を使ってください**（人向けの説明: `https://line-affi.com/guide/price-ladder`）。ページの JS で日数や期限を自前で計算しないこと
8. **段の報酬（購入の標準の％・ランク別）は、段をまとめて読み書きできます**（1つずつ案件を直さない）。`{id}` は段のどの案件でもよい。金額・率は必ず人に確かめてから
   ```bash
   curl -s https://line-affi.com/api/bot/offers/<段の案件のID>/ladder-rewards -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 要 offers:read。段ごとの purchasePercent と ranks・allSame
   # 全部の段に同じ値（ふつうはこれ）。省いた項目は今のまま。ranks の1件は rewardPercent / rewardYen / reset:true（上書きを消して標準に）のどれか1つ
   curl -s -X PUT https://line-affi.com/api/bot/offers/<段の案件のID>/ladder-rewards -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
     -d '{"all":{"purchasePercent":10,"ranks":[{"rankId":"<VIPのID>","rewardPercent":15},{"rankId":"<一般のID>","reset":true}]}}'   # 要 rewards:write
   # 段ごとに変えるとき: {"tiers":[{"offerId":"<1段目のID>","purchasePercent":10},{"offerId":"<2段目のID>","purchasePercent":8}]}
   ```
   管理画面では、値段の段の枠の「アフィリエイト報酬（購入・ランク別）」の表（行＝ランク・列＝段。「全部の段を同じにする」がオンなら1段目を直すだけ）

#### 自前の販売ページ（外部 LP）で値段の段を出す（embed.js の値段の段のモード）
ページの `</body>` の前に1行置き、値段・期限・残り時間・購入ボタンの場所に印を付けるだけです。`data-ladder` には、その値段の段のどれか1つの案件の slug を入れます。

```html
<p>今だけ <span data-la="price"></span>（<span data-la="deadline"></span> まで）</p>
<p>残り <span data-la="countdown"></span> ／ そのあとは <span data-la="next-price"></span></p>
<a data-la="buy" href="https://line-affi.com/purchase/<段の案件のslug>">申し込む</a>
<div data-la-show="tier:<1段目のslug>">1段目の人だけに見せる文</div>
<script src="https://line-affi.com/embed.js" data-ladder="<段の案件のslug>" async></script>
```
- 動き: URL の `sub` / `sig` を確かめる → 無い・型のまま・壊れていれば first-visit へ1回だけ送る（戻ると `lafv=1` が付く。何度も行き来しない）→ `https://line-affi.com/api/ladder/status` に「今どの段か」を聞き、`data-la="price"`（その人の段の値段）・`deadline`（その段の終わり。日本時間）・`countdown`（残り時間）・`next-price`（次の段の値段）を埋め（値段は10万円以上なら「33万円」「19万8000円」、10万円未満は「1,980円」。`data-la-format="yen"` を付けた要素だけ「¥330,000」の形）、`data-la-show="tier:<slug>"` の箱をその人の段のときだけ出し、`data-la="buy"` のリンクをその人の段の購入ページ（印つき）にする。ほかの購入ページへのリンクにも `sub`/`sig`・`ssid`/`ssig`・`lid`/`lsig`・`clickId`・`lref` を足す
- 自分で書くページ用に、結果を `document.addEventListener("lineaffi:ladder", e => e.detail)` でも受け取れます
- **守ること**（自前で JS を書く場合も同じ）:
  - 登録日の印は、URL の `sub` / `sig` だけを信じる。差し込み前の型（`{{subscribed_ms}}`）や壊れた値は使わない・保存しない
  - 印も期限も、ブラウザ（localStorage など）に保存しない。古い期限が残ると、登録したばかりの人を上の段へ送ってしまう
  - 印が無ければ first-visit へ。**定価（上の段）のページへ自分で送らない**
  - 値段・期限の表示は status API の値で出す。決済の値段は購入ページが決める（ページの表示と請求がずれない）
  - status API が答えないとき（通信の失敗など）は、ページをそのままにする

`GET https://line-affi.com/api/ladder/status?o=<段の案件のslug>&sub=&sig=&r=<戻り先>`（公開・個人情報なし。ブラウザからは、その案件の LP・視聴ページ・特別ページのドメインからだけ呼べる）:
- 署名が合う: `{ ok, verified:true, days（登録日から何日目・日本時間の暦日）, tier:{ slug, price, fromDay, toDay, endsAt（その段の終わり 23:59:59 JST・最後の段は null） }, next:{ slug, price, startsAt } | null, tiers:[…], purchaseUrl（その人の段の購入ページ・印つき）, handoff }`
- 署名が無い・合わない: `{ ok, verified:false, needFirstVisit:true, firstVisitUrl }`（段は返さない。一番高い段を返すことはありません）
- `handoff: true` は、その段が「初めて開いた日から数える次の段」につながる段（下の項）です。値段は購入ページで決まるので、購入ボタンは `purchaseUrl` に向けてください

#### ある段を「初めて開いた日」から数える次の段（例: マスター講座 99,000円 → 198,000円 → 330,000円）
登録から何日目で値段を上げると、案内のメールが無い段（例: 99,000円で買わなかった人の 198,000円）は、締切の案内も無いまま値段だけが上がります。そこで、その段を**初めて開いた日**を起点にできます（どのセンターでも使えます）。
- 段の案件に `PATCH /api/bot/offers/{id}` で次を入れる（要 `offers:write`。`null` で外す。どちらも空なら今までどおり）
  - `tierEntrySequenceId`: その段の購入ページを初めて開いた人に始めるシナリオ。メールの印（`ssid` / `ssig`）か LINE の印（`lid` / `lsig`）で、そのアカウントの購読者が分かる人だけに始めます。配信停止中の人・一度そのシナリオに入った人には始めません
  - `tierEntryHandoffGroupTag`: 初めて開いた日を登録日として数える、別の値段の段のグループ。入れると、その段の購入ページは `/api/ladder/tier-entry` を通って、送り先のグループの段へ移ります。**その段そのものでは買えなくなります**（期限を過ぎても旧い値段で買い続けられないように）
- 初めて開いた日は、人ごとに記録します（メールの印・LINE の印・印の無い人は端末の Cookie）。後から別の印で来ても、早い方の日にそろえます
- 案内のシナリオのリンクは、送り先のグループの購入ページ（どの段の slug でもよい）へ向けてください。メールのリンク（/go/）は、そのシナリオの開始日を `sub` に付けるので、同じ日から数えます
- 例（マスター講座）:
  1. 送り先のグループ（例: `kap-master-198-after`）に、198,000円（`tierFromDay:0`・`tierToDay:N`）と 330,000円（`tierFromDay:N`・`tierToDay:null`）の段を作る（N＝198,000円の日数）
  2. 198,000円の案内のシナリオ（案内 → 本日終了）を作る。リンクは手順1の購入ページへ
  3. 今の 198,000円の段（`kap2-master-front-198`）に `{"tierEntrySequenceId":"<手順2>","tierEntryHandoffGroupTag":"kap-master-198-after"}`
  4. 手順1の段にも、購入後シナリオ・買ったら止めるシナリオ・特商法の URL などを入れる

### 10-5 購入後（購入後シナリオ・会員サイト）
- 購入後の配信: ステップメールのシナリオを作り（「6. ステップメール」）、**全段の**販売の案件の `afterPurchaseSequenceId` に入れる
- **買ったら止める**: 購入すると、その案件と**同じ案件グループ**のシナリオ（無料講座・前の商品の購入後のメールなど）は自動で止まります。アップセルの段が**案件グループをまたぐ**とき（例: パッケージ → マスター講座 → ギルド → AI執事）は、次の段の案件に `stopSequenceIdsOnPurchase: ["<前の段のシナリオID>", …]`（`PATCH /api/bot/offers/{id}`・要 `offers:write`。指定すると置き換え・`[]` で無し・自分のアカウントのシナリオだけ・50件まで）を入れると、その案件を買った人の、そのシナリオも止まります（「購入で終了」になり、次のシナリオへは移りません。移す先の `afterPurchaseSequenceId` は止めません）。値段の段がある案件は、全段に同じ値を入れてください（管理画面の「段にそろえる」でも写ります）
  - 例（アップセルの順番で前の段を打ち切る）: 無料講座 →（パッケージを買う）→ パッケージの `afterPurchaseSequenceId` ＝「月額の案内」→（マスター講座を買う）→ マスター講座の `afterPurchaseSequenceId` ＝「ギルドの案内」、`stopSequenceIdsOnPurchase` ＝ [「月額の案内」・「パッケージ購入者向け」] →（ギルドを買う）→ ギルドの `stopSequenceIdsOnPurchase` ＝ [「ギルドの案内」…]
- 会員サイト: 1段目に章・教材 →「会員サイトの教材」。購入メールには受け取り方と問い合わせ先を書く（「6. ステップメール」の通知メールの項）
- 売り込みを止める: メールのステップは、買った人には同じグループの販売ページへ誘導する回を自動で止めます。LINE は、購入の知らせ（partner-purchase）で全自動AI集客システムがその人のステップ配信を止めます（10-7）

### 10-6 アフィリエイト（報酬・ランク・説明文）
- 報酬 →「5. 報酬を決める」。登録の報酬はオプトインの案件、購入の率は**全段の**販売の案件に同じ値
- ランク別の額と、説明文の差し込み（`{{報酬_LINE登録}}`・`{{報酬_バックエンド}}` など）→「ランクと差し込み」
- 公開 →「9. 公開する」。アフィリエイターに公開するのはオプトインの案件だけで足ります（販売の報酬はオプトインのカードに出ます）。段の案件を公開すると、同じ商品のカードが段の数だけ並びます
- 実機テストのアフィリエイターと紹介リンク →「8. 実機テスト」

### 10-7 全自動AI集客システム側とのつなぎ
- **連携の鍵**: line-affi の「設定 → 全自動AI集客システム」で「連携を申し込む」（API は `POST /api/bot/integrations/fullyautoai`、要 `line:connect`）。専用の鍵が自動で発行され、公式アカウントが向こうに登録されます。承認は向こう側で人が行い、承認されるまで下のどれも動きません（「LINE の公式アカウントに紐づく案件を引く」の最後の2項目）。LINE の通知の転送もこの連携で始まります
- **チャネルの案件一覧**: 向こうは `GET /api/bot/channels/{channelId}/offers` で案件とリンクを読みます →「LINE の公式アカウントに紐づく案件を引く」。オプトインの案件に `lineChannelId` があり、販売の段が同じ `groupTag` なら、何も足さずに一覧に入ります
- **LINE のステップの記号**（向こうのシナリオの本文に書く）: `{LP_LINK:<オプトインのslug>}`＝LP、`{GIFT_LINK:<オプトインのslug>}`＝メール登録ページ（その人の LINE の ID 付き）、`{SALES_LINK:<販売の案件のslug>}`＝販売ページ（紹介元 `clickId`・値段の段の `sub`/`sig`・購入の知らせ用の印 `lref` 付き）
- **リッチメニュー**: ボタンの uri に `integration.urls.lineWatch`（無料動画）と `integration.urls.lineSpecial`（特別編）
- **タグ**: 3本観ると `watched_all`（向こうでタグ「無料動画を最後まで視聴」）、特別編を観ると `watched_special`（「特別編を最後まで視聴」）。メニューの切り替え（特別編のボタンを出す）は、向こうのタグの規則で行う
- **カゴ落ちの追いかけ・後押し**（連携が承認されたら自動でオン・オフにもできる）: 購入ページを開いた人（`purchase_page_opened`）と、動画を1本見終えた人（`watched_part`）も知らせます。向こうは送る前に `GET /api/bot/purchase/status` で買っていないかを確かめる →「友だちの行動を知らせる」
- **購入の知らせで売り込みを止める**: 購入が確定すると line-affi が向こうに知らせ（`lref`、無ければ購入者の `lineUserId`）、向こうがその人の LINE のステップ配信を止めます。`{SALES_LINK}` 以外のリンクで売ると `lref` が付かないので、販売へのリンクは `{SALES_LINK}` か特別ページ経由にする

### 10-8 確認のチェックリスト（人と一緒に、スマホで）
1. テスト用アフィリエイターの紹介リンクを開く → LP に `?clickId=` が付く（「8. 実機テスト」）
2. LP から LINE を友だち追加（先にブロック → リンク → 追加 → ブロック解除）→ LINE 登録の報酬が付く
3. LINE の配信（またはメニュー）から視聴ページ → 3本を最後まで観る → 向こうでタグ「無料動画を最後まで視聴」
4. リッチメニューが特別編入りに切り替わる
5. 特別編を開いて最後まで観る → タグ「特別編を最後まで視聴」、管理画面の案件の編集画面の下「特別編を最後まで観た人」に出る
6. 特別ページ（または `{SALES_LINK}`）から購入ページ → その人の日数の段の値段が出る。別の日数も確かめるなら「値段の段の署名を外部の配信に渡す」で過去の時刻の署名をもらい、`/purchase/{slug}?sub=…&sig=…` を開く
7. テスト購入（決済のテストキー）→ 購入メール・会員サイトのログイン・購入後シナリオの1通目が届く
8. 売り込みが止まる: LINE の販売のステップが止まり、メールの販売の回も届かない
9. 報酬: テスト用アフィリエイターに購入報酬が付く（`GET /api/bot/affiliates?email=…`）。自動承認の条件は「5. 報酬を決める」
10. 後片付け: テスト購入の返金（報酬も取り消される）、テストのタグの削除。公開は人の判断で「9. 公開する」

## 公開後に任せられる運用
| やりたいこと | API |
|---|---|
| アフィリエイターへのお知らせメール（人数を確かめ、人の了承を得てから。予約配信・ランク／状態の絞り込み・配信前の編集と削除・エラーの再送） | `POST /api/bot/affiliates/announce`（`scheduledAt` `rankNames` `statuses`）、`GET/PATCH/DELETE /api/bot/affiliates/announce/{id}`、`POST …/{id}/retry` |
| アフィリエイターの登録・ランク変更・稼働停止・紹介リンク発行 | `/api/bot/affiliates`, `/api/bot/affiliates/{id}`, `/api/bot/affiliate-links` |
| 明らかな不正者の自動ブラック（同じ接続元の複数登録・バックエンド成約率が平均の1/5以下。ルールごとに「自動でブラック／通知だけ」） | `GET/PUT /api/bot/settings/auto-black`（GET は要 `affiliates:read`・PUT は要 `affiliates:write`。**人の了承を得てから**） |
| 新規登録時の自動不正チェック（既定オフ。他センターで不正と判定された人と一致したら、登録時に自動で保留） | `GET/PUT /api/bot/settings/signup-fraud-check`（`{"enabled":true,"action":"PAUSE"}`。GET は要 `affiliates:read`・PUT は要 `affiliates:write`。**人の了承を得てから**） |
| 手入力のブラックリスト（メール・電話・口座番号。登録時・出金時に一致したら保留） | `GET/POST /api/bot/blacklist`、`PATCH/DELETE /api/bot/blacklist/{id}`（`{"kind":"EMAIL","value":"…"}`。GET は要 `affiliates:read`・それ以外は要 `affiliates:write`。**人の了承を得てから**） |
| 怪しい IP からのクリック・登録を調べる（IP を入力したときだけ。保存しない） | `POST /api/bot/ip-lookup`（`{"ip":"203.0.113.5","days":30}`。要 `affiliates:read`） |
| 運営者情報（会社名・フリガナ・住所・電話番号。いまは保存と表示だけ） | `GET/PUT /api/bot/settings/operator-info`（`{"companyName":"…","companyNameKana":"…","address":"…","phone":"…"}`。GET は要 `offers:read`・PUT は要 `offers:write`。**正式な表記は人に確かめてから**） |
| アフィリエイターに自動で届くメール（招待・登録の確認・ログイン・新案件・新LP・報酬の確定）の件名・本文（既定に戻せる。**人に見せて了承を得てから**） | `GET /api/bot/settings/mail-templates`、`PUT/DELETE /api/bot/settings/mail-templates/{key}`（要 `email:settings`） |
| アフィリエイトセンターのデザイン（配色・ヘッダーの肩書き・ホームのあいさつ・ログイン画面の案内）とサポートサイトのURL（**文言は人に確かめてから**） | `GET/PUT /api/bot/settings/portal-design`（GET は要 `offers:read`・PUT は要 `offers:write`） |
| アフィリエイターのメール送信の状態（利用者許可／利用者拒否／エラー停止）と、お知らせメールの配信エラー | `GET /api/bot/affiliates` の各人の `mailStatus`（`ALLOWED`＝利用者許可・`EXCLUDED`＝利用者拒否・`ERROR_STOPPED`＝エラー停止）と `deliveryErrorCount`（件数）、`GET /api/bot/affiliates/{id}` の `mailStatus`・`emailStoppedReason`（`unsubscribe`＝メールの停止リンクで本人が止めた・`bounce`＝届かないアドレス・`complaint`＝迷惑メールの報告）・`emailStoppedAt`・`deliveryErrors`（件数と、新しい順に5件の内容）。変えるのは `PATCH` の `mailStatus: "ALLOWED"`（許可に戻す。止めた理由も消える）か `"EXCLUDED"`（利用者拒否にする）。`emailNotifications: true/false` でも同じ。エラー停止は自動で付くもので、手では付けられません。要 `affiliates:read`（変えるのは `affiliates:write`） |
| アフィリエイターの情報の編集（氏名・フリガナ・電話・住所・お知らせメールの許可・メールアドレス） | `PATCH /api/bot/affiliates/{id}`（`lastName` `firstName` `lastNameKana` `firstNameKana` `phone` `postalCode` `prefecture` `city` `address1` `address2` `emailNotifications`。空文字か `null` で未入力に戻す。要 `affiliates:write`）。**メールアドレス（`email`）を変えると、その人のログイン先が変わる**ので `"confirmEmailChange": true` を付けたときだけ変わる（人の了承を得てから）。他のセンターにも登録している人は変えられない（`SHARED_ACCOUNT`） |
| アフィリエイターの削除（論理削除。報酬・成果の記録は残る）と復元 | `DELETE /api/bot/affiliates/{id}`（要 `affiliates:write`。**人の了承を得てから**）、戻すのは `POST /api/bot/affiliates/{id}/restore`（停止中の状態で戻る）。削除した人は一覧に出ず（`GET /api/bot/affiliates?deleted=1` で削除済みだけ見られる）、ログインと新しい報酬の発生が止まる。BAN の人は BAN のまま。管理画面の編集・削除も `GET /api/bot/audit-log` に残る（`keyPrefix: admin`・項目名だけで値は残さない） |
| ランクの作成、案件×ランク別の報酬 | `/api/bot/ranks`, `/api/bot/offer-rank-rewards` |
| 特定のアフィリエイターだけの個別の報酬（ランク別より優先。履歴つき。**金額は人の指示どおりに**） | `GET/POST/PATCH /api/bot/affiliate-rewards`、`DELETE /api/bot/affiliate-rewards/{id}`（GET は要 `offers:read`・変更は要 `rewards:write`） |
| 自動ランクアップ（既定ON・1案件で承認済み30件で VIP・100件で SVIP。件数・ランクの変更、ON/OFF） | `GET/PUT /api/bot/settings/auto-rank`（PUT は要 `rewards:write`） |
| アフィリエイターの新規登録の受付（止める／再開。止めると登録ページと2ティア紹介リンクが使えなくなる） | `GET/PUT /api/bot/settings/affiliate-signup`（`{"enabled":false}`。GET は要 `affiliates:read`・PUT は要 `affiliates:write`） |
| 広告バナー（アフィリエイターが紹介リンク付きで貼る画像） | `/api/bot/offer-banners` |
| アップセル / ダウンセル | `/api/bot/upsells` |
| お客様の行動の数（案件ごとの 購入ページを開いた人数・買った人数・まだ買っていない人数、動画の何本目を見終えた人数） | `GET /api/bot/activity/summary?since=&offerId=`（要 `stats:read`。個人情報なし）→「お客様の行動の数を報告する」 |
| 販売ページの読まれ方（見出しごとに、ここまで読んだ人数・いちばん下まで来た割合・見ていた時間・申込ボタンを押した人・その後に購入ページを開いた／買った人数） | `GET /api/bot/reports/page-reading?page=&period=&source=`（要 `stats:read`。個人情報なし）→「販売ページの読まれ方を報告する」 |
| お客様の行動（購入ページを開いた・動画を見終えた）の一覧 | `GET /api/bot/activity?type=PURCHASE_PAGE_OPENED｜VIDEO_WATCHED&offerId=&since=`（要 `customers:read`。このアカウントの顧客だと分かる人＝LINE の lid/lsig・視聴ページの w・メールの ssid/ssig・会員のログインの人だけを記録。購入ページは同じ人・同じ案件で6時間に1回、動画は1本ごとに1回。全自動AI集客システムとの連携に関係なく残る。管理画面は「お客様の行動」） |
| 外部で決済された購入の計上（UTAGE 等） | `POST /api/bot/conversion`（`externalId`＝外部の注文ID・`amount`・`offerId` か `offerSlug`・`clickId` / `lineUserId` / `email` のどれか。要 `conversions:write`。同じ `externalId` の再送は二重に計上しない。メールだけでも、購入後シナリオへの移行と前の段の打ち切りは動く） |
| アフィリエイターの画面での案件の並び順 | `PATCH /api/bot/offers/{id}` の `portalSortOrder`（0 以上の整数・小さいほど上。同じなら無料オプトインが先・新しい順）。管理画面は案件一覧の「アフィリエイターの画面での並び順」 |
| VIP・SVIP の既定の単価 | 案件を作ると、ランク「VIP」「SVIP」があるセンターでは、空欄のランクに既定の単価が入ります（VIP: LINE 700円・メール 200円・購入 15%、SVIP: LINE 1,000円・メール 500円・購入 20%）。その成果に標準の報酬があり、既定のほうが高いときだけ。変えるのは `offer-rank-rewards` で |
| アフィリエイトセンターの案件一覧の一番上のお知らせ（ランク別の報酬の一覧・宣伝） | `GET/PUT /api/bot/settings/portal-notice`（`{"html":"…"}`。文章ならそのまま・HTML なら表も可。`null` で消す。PUT は要 `offers:write`）。自分のランク名はお知らせの上に自動で出ます |
| 外部で決済された購入の返金（報酬も取り消す） | `POST /api/bot/conversion/refund`（`externalId`＝計上したときの注文ID、`refundedAmount`＝返金額の累計・省略で全額。要 `conversions:write`。何度送っても同じ結果） |
| 登録者の一括取り込み | `POST /api/bot/leads` |
| アフィリエイト報酬集計（GS と同じ項目・絞り込み・CSV） | `GET /api/bot/reports/affiliate-rewards` |
| 成約経路分析・効果測定（購入までの流入元 utm・LP・紹介者。LP別・デバイス別・日別の アクセス→登録→購入） | `GET /api/bot/reports/funnel`（要 `stats:read`。個人情報は含まない） |
| 決済状況一覧（決済手段・状態・金額・返金で絞る。CSV） | `GET /api/bot/reports/payments`（要 `stats:read`。購入者のメール・名前は `customers:read` も持つキーだけ） |
| 申込一覧（無料案件／有料案件。GS と同じ検索項目・CSV） | `GET /api/bot/reports/orders`（要 `stats:read`。成約者のメール・名前は `customers:read` も持つキーだけ） |
| クリック数・ステップメールの実績 | `GET /api/bot/go-clicks`, `GET /api/bot/go-clicks/by-utm?linkCode=…` (紹介リンク1本の utm 別), `GET /api/bot/stepmail` |
| オートウェビナー（作成・スケジュール・台本・自動応答・AIチャット・視聴連動LINE・登録者） | `/api/bot/webinars` ほか（下の章） |
| 個別相談の予約枠、予約者の確認・キャンセル | `/api/bot/consultations`, `/api/bot/consultations/{id}/bookings`, `POST /api/bot/bookings/{id}/cancel` |
| 個別相談の担当者（複数人で受ける・Google カレンダー・Meet） | `/api/bot/consultation-staff`, `PUT /api/bot/consultations/{id}/staff` |
| クーポンの発行・停止・期間延長（人の了承を得てから） | `/api/bot/coupons` |
| LINE の自動応答 | `/api/bot/auto-replies` |
| 予備の LINE アカウント、凍結時の再接触メール | `/api/bot/line/channels`, `POST /api/bot/line/recontact` |
| 動画のアップロード・視聴連動ルール | `/api/bot/videos`, `/api/bot/videos/{id}/rules` |
| 会員サイトの章・教材（場面つき） | `/api/bot/member-content`（`/sections`, `/reorder`） |
| 会員だけが開けるファイル（PDF） | `/api/bot/member-files` |
| 会員だけが見られる1枚のツールのページ（HTML＋毎日の data） | `/api/bot/member-tool/{slug}` |
| 会員サイトの Q&A（一括で入れられる） | `/api/bot/member-faq`（`/{id}`, `/reorder`） |
| 特典の講座（バンドル）・特典だけの会員サイトの案内 | `PATCH /api/bot/offers/{id}` の `bonusOfferIds`・`bonusNoteHtml`、既存の購入者へ `POST /api/bot/offers/{id}/bonus-grant` |

### アフィリエイターのメール送信（利用者許可・利用者拒否・エラー停止）
- アフィリエイター向けのメール（新案件・追加LPのお知らせ、一斉メール、報酬の通知など）には、本人だけの「お知らせメールを止める」リンクが末尾に自動で付きます（ログイン不要。押した先の画面で「止める」を押したときだけ止まる）。Gmail などの「配信停止」ボタン用のヘッダー（List-Unsubscribe）も付きます。止まるのは、そのセンターのメールだけです
- 届かないアドレス（恒久的な不達）・迷惑メールの報告の通知を配信サービスから受け取ったら、その人のメールは自動で「エラー停止」になります（Resend は「届かないアドレス・迷惑メールの報告で自動で止める」の設定をしたアカウントだけ。Amazon SES は運営側の設定 `SES_WEBHOOK_ENABLED` が有効なときだけ）（送ったメールの記録から、どのセンターの誰かを引くので、ほかのセンターの同じ人は止まりません）
- 止まった人には、新案件・追加LPのお知らせも一斉メールも送りません。本人が「登録情報」でメール通知をオンにするか、運営者が `PATCH /api/bot/affiliates/{id}` の `mailStatus: "ALLOWED"` で戻せます

## アフィリエイト報酬集計を読む（`GET /api/bot/reports/affiliate-rewards`、要 `stats:read`）

管理画面「アフィリエイト報酬集計」（/admin/performance）と同じ数字です（GS アフィリエイトセンターの「アフィリエイト報酬集計」と同じ項目）。氏名・表示名・メールアドレスは `affiliates:read` も持つキーのときだけ返ります（無いときは null）。

- 絞り込み（任意）: `group`（案件グループ名）、`offer`（案件ID）、`lp`（1=メインLP、2以降=追加LP）、`device`（`PC` / `SP` / `TABLET` / `UNKNOWN`。端末は 2026/9/27 以降のクリックから記録）、`from` / `to`（日本時間の `YYYY-MM-DD`、両端を含む）、`affiliateId`
- 並べ替え: `sort`（code, name, access, regCount, regRate, optinApproved, optinRate, optinPurchases, optinAmount（無料オプト）, backApproved, backRate, backPurchases, backAmount（有料の商品の合計）, stage{N}Approved / stage{N}Rate / stage{N}Purchases / stage{N}Amount（売る順番 N ごと。1＝フロント商品、2＝バックエンド商品①…）, tier2Count, tier2Amount, unpaid, approvedAmount）と `dir`（asc / desc）。既定は approvedAmount の多い順。以前の frontApproved などは optin* として読みます
- ページ: `page`（1始まり）、`limit`（既定50・最大200）。`summary` は絞り込んだ全体の合計。`format=csv` で全員分を CSV（BOM付き UTF-8）
- 返り値: `summary`（access, regCount, purchaseCount, rewardAmount, approvedCount, approvedPurchases, approvedAmount, tier2Count, tier2Amount, unpaid, affWithAccess, affWithApplication, affWithApproved, affWithApprovedReward）、`activeAffiliates`、`stages[]`（{stage, label}。列に出す売る順番。1＝フロント商品は常に入る）、`rows[]`（rank, affiliateId, referralCode, access, regCount, regRate, optin{approved, rate, purchases, amount}（無料オプト）, paid{…}（有料の商品の合計）, stages[]（{stage, approved, rate, purchases, amount}。上の stages と同じ並び）, tier2{approved, amount}, unpaid。以前の front（＝無料オプト）・backend（＝有料の合計）も同じ値で残しています）。率は 0〜1、アクセス0のときは null
- 数え方: 全登録数＝取消以外、承認済＝承認・支払済。登録数＝メール登録と友だち追加の件数（同じ人が両方すると2件）。決済数＝購入の件数（返金の控除は件数に入れず金額には含める）。無料オプト＝オプトインの案件の報酬、フロント商品・バックエンド商品①②…＝有料の案件の報酬を「売る順番」（salesStage）ごとに分けたもの。未払額＝承認済みでまだ支払いに入っていない額（2ティアを含む）

例: `curl -s "https://line-affi.com/api/bot/reports/affiliate-rewards?from=2026-09-01&to=2026-09-30&sort=backAmount" -H "Authorization: Bearer $LINE_AFFI_API_KEY"`

## 申込一覧を読む（`GET /api/bot/reports/orders`、要 `stats:read`）

管理画面「申込一覧」（/admin/orders）と同じ検索です（GS アフィリエイトセンターの「申込一覧（無料案件／有料案件）」と同じ項目）。**1行 = 1件の直接報酬**（2ティアと返金の控除行は除く）。無料案件＝メール登録・LINE友だち追加、有料案件＝購入。成約者のメールアドレス・名前は `customers:read` も持つキーのときだけ返ります（無いときは null）。

- 絞り込み（任意）: `kind`（`free` / `paid`）、`group`、`offerName`（一部でも可）、`offer`（案件ID）、`lp`、`device`（`PC` / `SP` / `TABLET` / `UNKNOWN`）、`id`（成約ID）、`email`・`lastName`・`firstName`（成約者）、`from` / `to`（日本時間の `YYYY-MM-DD`）、`aff`（アフィリエイターID・紹介コード）、`tier2`（2ティアの紹介元ID）、`orderNo`、`mail`（`on`＝配信中 / `off`＝配信停止）、`verify`（`done`＝メールに反応あり / `none`＝反応なし）、`status`（`PENDING` / `CONFIRMED` / `PAID` / `CANCELLED`）、`fraud`（`clean` / `suspect`＝不正の疑い / `review`＝不正ではない確認待ち）、`link`（`line` / `click`）、`method`（`EMAIL_OPTIN` / `LINE_FOLLOW` / `PURCHASE`）
- ページ: `size`（20 / 50 / 100）、`page`。`total` と `rewardSum`（取消を除く報酬額の合計）は絞り込んだ全体。`format=csv` で条件に合う全件（上限5万件）
- 画面の「メール反応」（旧「メール認証」。API の項目名 `verify` は変わりません）は、登録後に送ったメールを開いた・リンクを押した人かどうかです（二重登録の確認メールは使っていないため）。`done` = 反応あり、`none` = 反応なし

例: `curl -s "https://line-affi.com/api/bot/reports/orders?kind=paid&status=PENDING&size=50" -H "Authorization: Bearer $LINE_AFFI_API_KEY"`

## 決済状況一覧を読む（`GET /api/bot/reports/payments`、要 `stats:read`）

管理画面「決済状況一覧」（/admin/payments）と同じ検索です。**1行 = 1件の購入**。購入者のメールアドレス・名前は `customers:read` も持つキーのときだけ返ります（無いときは null）。

- 絞り込み（任意）: `provider`（`payjp` / `paypal` / `bank`＝銀行振込 / `stripe` / `external`＝UTAGE など外部決済）、`status`（`COMPLETED` / `PENDING` / `REFUNDED` / `CANCELLED`）、`refund`（`none` / `partial` / `full`）、`min` / `max`（決済金額・円）、`from` / `to`（日本時間の `YYYY-MM-DD`）、`email`、`offerName`、`group`、`orderNo`
- ページ: `size`（20 / 50 / 100）、`page`。`summary` は絞り込んだ全体（件数、決済完了の金額、返金額、決済手段ごとの件数と金額）。`format=csv` で条件に合う全件（上限5万件）
- 銀行振込は、入金を確認して購入になったものが出ます（入金待ちは `GET /api/bot/bank-transfers`）

例: `curl -s "https://line-affi.com/api/bot/reports/payments?provider=bank&status=COMPLETED" -H "Authorization: Bearer $LINE_AFFI_API_KEY"`

## 成約経路分析・効果測定を読む（`GET /api/bot/reports/funnel`、要 `stats:read`）

管理画面「成約経路・効果測定」（/admin/funnel）と同じ数字です（簡易版）。個人情報は含みません（紹介者は表示名と紹介コードまで）。

- 絞り込み（任意）: `from` / `to`（日本時間の `YYYY-MM-DD`。既定は直近30日・最大365日）、`group`（案件グループ）、`offer`（入口の案件ID）
- `effects`（効果測定）: `total` と `byLp` / `byDevice` / `daily`（新しい日が先）。各行は `{ key, label, clicks, registrations, purchases, revenue, regRate, purchaseRate, conversionRate }`。率は 0〜1、分母0のときは null
- `paths`（成約経路分析）: `totalPurchases`・`totalRevenue` と、購入を `byUtmSource` / `byUtmMedium` / `byUtmCampaign` / `byLp` / `byAffiliate`（上位20人）に分けた `{ key, label, purchases, revenue, share }`。utm が無いクリックは「（なし）」
- 数え方: アクセス＝紹介リンクのクリック、登録＝そこから生まれたリード（メール登録・LINE友だち追加）、購入＝紹介者に報酬が付いた購入（取消・返金の控除を除く）。購入額は返金前。日別は「その日に起きた出来事」を並べたもので、同じ人の流れを追うものではありません

例: `curl -s "https://line-affi.com/api/bot/reports/funnel?from=2026-10-01&to=2026-10-31" -H "Authorization: Bearer $LINE_AFFI_API_KEY"`

## お客様の行動の数を報告する（`GET /api/bot/activity/summary`、要 `stats:read`）

「購入ページを開いた人が何人いて、何人が買ったか」「動画を何本目まで見た人が何人いるか」を、数字で人に報告するときに使います。個人情報は含みません（人ごとの一覧が要るときだけ `GET /api/bot/activity`・要 `customers:read`）。

- 絞り込み（任意）: `since`（ISO 8601。既定は7日前から・最長90日前まで）、`offerId`（その案件だけ）
- 返す値は案件ごと（`offers[]`）:
  - `purchasePage`: `opens`（開いた回数。同じ人・同じ案件は6時間に1回まで数える）・`people`（開いた人数）・`purchased`（そのうち期間内にその案件を買った人数）・`notPurchased`（開いたが買っていない人数＝カゴ落ち）
  - `videos[]`: `{ kind: "watch"（視聴ページ）| "special"（特別編）, part: 何本目, total: 全部で何本, people: 見終えた人数 }`
- 数えるのは、このアカウントのお客様だと分かる人だけです（LINE から来た人・メールのリンクから来た人・会員サイトにログイン中の人）。印の無い人（広告から直接来た人など）は数に入りません。なので「購入ページの実際のアクセス数」より少なく出ます。報告するときはそう添えてください

報告のしかた（例。週に1回、または人に聞かれたとき）:
1. `curl -s "https://line-affi.com/api/bot/activity/summary?since=<7日前の日時>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"`
2. 案件ごとに、次の形で短く伝える:
   - 「{案件名}: 購入ページを開いた {people} 人（{opens} 回）→ 購入 {purchased} 人・まだ買っていない {notPurchased} 人」
   - 「動画: 1本目 {people} 人 → 2本目 {people} 人 → 3本目 {people} 人 → 特別編 {people} 人」（前の本より大きく減った所が、離れている所）
3. 気づいたことを1つだけ添える（例: 「2本目から3本目で半分に減っています。3本目の冒頭か、2本目の終わりの案内を見直しませんか」）。数字を変える操作（配信・値段・案内の変更）は、人の OK をもらってから

## 販売ページの読まれ方を報告する（`GET /api/bot/reports/page-reading`、要 `stats:read`）

販売ページ（外部の LP も含む）を「どの見出しまで読んだか」「どこで離れたか」を、訪問ごとに数えています。
**ページに `<script src="https://line-affi.com/embed.js" defer></script> が入っていれば、それだけで記録されます**（値段の段・フォームと同じ embed.js）。止めたいページは `data-la-read="off"` を付けます。

- 区切りは、ページの `h2` の見出し。区切りを自分で決めたいときは、その要素に `data-la-section="名前"` を付けます（1つでもあれば、h2 ではなくそちらを使う。40個まで）
- 数えるもの（1回の訪問＝ブラウザのタブ1つ。1秒未満の訪問は数えない）: 画面に入った見出し・いちばん下まで行った位置（10% 刻み）・見ていた時間（画面に出ていた時間）・申込ボタン（line-affi の購入ページへのリンクか `data-la="buy"`）を押したか
- 受けるのは、そのセンターの案件に登録したページのホスト（LP・視聴ページ・特別編・サンキュー）と確認済みの計測ドメインだけ。line-affi の画面（line-affi.com）のページは `embed.js?o={ownerId}` を付けたときだけ
- 来た元: URL の `lid`/`lsig`（LINE の本人）か `lref`（全自動AI集客）→ LINE、`ssid`/`ssig`（メール）→ メール。署名を確かめてからお客様につなぎます（紹介の `clickId` からも）。ブラウザに個人の情報は残しません
- 管理画面は「販売ページの読まれ方」（`/admin/page-reading`）。読んだ人の一覧（お客様が分かる人だけ・紹介者つき）もここ

取り方:
1. `curl -s "https://line-affi.com/api/bot/reports/page-reading" -H "Authorization: Bearer $LINE_AFFI_API_KEY"` → `pages[]`（`page`＝ホスト＋パス・`visits`＝30日の訪問）
2. `curl -s "https://line-affi.com/api/bot/reports/page-reading?page=<pages の page>&period=7d&source=line" -H "Authorization: Bearer $LINE_AFFI_API_KEY"` → `summary`
   - `sections[]`: `{ idx, title, reached（その見出しが画面に入った訪問の数）, pct（訪問に対する %） }`（上から順）
   - `depth[]`: `{ depth: 0〜100, reached, pct }`（その位置まで来た訪問）・`medianSec`/`avgSec`（見ていた時間）・`ctaClicked`/`ctaPct`（申込ボタン）
   - `knownPeople`（お客様が分かる人数）・`purchasePageOpened`（そのうち読んだあとに購入ページを開いた人）・`purchased`（買った人）

報告のしかた（例）:
- 「{ページ}: 訪問 {visits}（LINE {line}・メール {email}）→ 申込ボタン {ctaClicked}人（{ctaPct}%）→ 購入ページ {purchasePageOpened}人 → 購入 {purchased}人」
- 「見出し: 1.{title} {pct}% → 2.{title} {pct}% → …」のうち、前の見出しから大きく減った所（例 20% 以上）を1〜2か所あげて、「ここで離れています。この見出しの前後を短く／ボタンを前に出す、を見直しませんか」と1つだけ提案する。ページを直すのは、人の OK をもらってから

## 管理画面の各機能も API で（ウェビナー・個別相談・クーポン・自動応答・予備アカウント・動画・会員サイト）

ここにある機能は、管理画面で出来ることがすべて API でも出来ます。権限が足りなければ `403 INSUFFICIENT_SCOPE` が返るので、人に「設定 → APIキー」でこのキーの「権限を変更」から付けてもらってください（キーは変わりません）。

### オートウェビナー（要 `webinars:write`。読むだけなら `offers:read`）

録画した動画を「15分おき」や「毎日 12:00 と 20:00」に自動で流すセミナーです。

```bash
curl -s https://line-affi.com/api/bot/webinars -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 一覧と、使えるアップロード済み動画（availableStreamVideos）
curl -s -X POST https://line-affi.com/api/bot/webinars \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"title":"30分で分かる○○講座","videoUrl":"https://youtu.be/XXXXXXXXXXX","videoDuration":1800,
       "scheduleType":"FIXED","fixedTimes":"12:00,20:00","requireRegistration":true,
       "ctaOffsetSec":1500,"ctaText":"特別価格で申し込む","ctaUrl":"https://example.com/lp","groupTag":"marketing"}'
```
- 返る `webinar.publicUrl` が登録ページです。LINE の配信や LP にはこの URL を貼ります
- `videoDuration` は**秒**（60分なら `3600`）。アップロード済み動画を使うなら `videoUrl` の代わりに `streamVideoId`（`videoDuration` は省略可）
- `scheduleType` は `INTERVAL`（`intervalMin` 10 / 15 / 20 / 30）か `FIXED`（`fixedTimes` に日本時間の `"HH:MM"` をカンマ区切り）
- `groupTag` を案件と同じにすると、申し込みボタンがその案件につながり、登録から購入まで紹介者が引き継がれます
- 作成はタイトルで冪等です（再送しても増えず `existed: true`）。変更は `PATCH /api/bot/webinars/{id}`、今の設定は `GET /api/bot/webinars/{id}`（台本・自動応答・プッシュも含む）。公開ページの URL（slug）は後から変えられません
- 視聴中のチャット:
  - 台本コメント `POST /api/bot/webinars/{id}/chat-scripts`（`{"items":[{"offsetSec":60,"displayName":"ゆみ","message":"楽しみにしてました！"}]}`）。**実在の視聴者の声ではありません。実績の誇張など誤解を招く文面にしないよう、人に確認してください**
  - キーワード自動応答 `POST /api/bot/webinars/{id}/auto-replies`（`trigger`: `KEYWORD` / `ANY`、`matchType`: `CONTAINS` / `EXACT`）
  - AI 返信は `PATCH` で `{"aiChatEnabled":true,"aiChatContext":"講座の概要・よくある質問…"}`
  - 削除は `DELETE …/chat-scripts?id=…`（本文で `{"ids":[…]}` / `{"all":true}` も可）。1件ずつの修正は出来ないので、消して足し直します
- 視聴時間に応じた LINE `POST /api/bot/webinars/{id}/push-rules`（`{"offsetSec":1500,"message":"…","emailFallback":true,"emailSubject":"…"}`）。**追加した時点から実際の視聴者に LINE（未連携の人にはメール）が届きます。文面とタイミングは人の了承を得てから**
- 登録者 `GET /api/bot/webinars/{id}/registrations`（**要 `customers:read`**。名前・メールアドレス入り。`attended` / `from` / `to` / `limit` / `offset`）
- 止めるだけなら `PATCH` で `{"isActive":false}`。`DELETE` は登録者ごと消えて戻せません（登録者がいるときは `{"confirmTitle":"<タイトル>"}` が必要）。人の了承なしに消さないこと

### 個別相談の予約枠（要 `consultations:write`）

お客様が空き時間から自分で予約できる相談枠です。

```bash
curl -s -X POST https://line-affi.com/api/bot/consultations \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"title":"無料個別相談（30分）","slug":"free-consult","weekdays":[1,2,3,4,5],"startTime":"10:00","endTime":"17:00","slotMinutes":30,"meetingUrl":"https://zoom.us/j/…","confirmSubject":"ご予約ありがとうございます","confirmBody":"{{name}} 様\n\n日時: {{datetime}}\nURL: {{meeting_url}}"}'
```
- 返る `consultation.urls.booking`（`/book/{slug}`）が予約ページです
- 省略した項目は既定値（平日 10:00〜17:00・30分枠・12時間前まで・30日先まで）。`weekdays` は 0=日〜6=土、時刻は日本時間。`slotMinutes` は 15 / 30 / 45 / 60 / 90
- **確認メールは `confirmSubject` と `confirmBody` の両方があるときだけ送られます**（差し込み `{{name}}` `{{datetime}}` `{{meeting_url}}`）。抜けは応答の `warnings` に出ます
- `groupTag` を案件と同じにすると、予約完了画面にその有料案件へのボタンが出ます（文言は `ctaLabel`）
- 作成は `slug`（省略時はタイトル）で冪等。変更は `PATCH /api/bot/consultations/{id}`（`slug` は変更不可）。受付を止めるだけなら `{"isActive":false}`
- 予約者 `GET /api/bot/consultations/{id}/bookings`（**要 `customers:read`**。氏名・メール・電話・相談内容。既定は今後の予約だけ）
- `DELETE` は予約ごと消え、お客様に通知は行きません。今後の予約があると `400 HAS_UPCOMING_BOOKINGS`。人の了承を得たときだけ `?force=true`
- 確認メールには、お客様が自分でキャンセルできるリンクが自動で付きます（本文の好きな場所に置くなら `{{cancel_url}}`）。担当者のいる予約枠では `{{staff_name}}`（担当者の名前）も使えます
- 予約のキャンセル `POST /api/bot/bookings/{id}/cancel`（`{"notifyCustomer":true}` でお客様にもメール）。担当者の Google カレンダーの予定も消え、担当者に知らせが届きます。**お客様の予約を取り消すので、人の了承を得てから**

### 個別相談の担当者（複数人で受ける。要 `consultations:write`。読むだけなら `offers:read`）

複数の担当者で予約を受けられます。予約枠に担当者を割り当てると:

- 1人でも空いていれば、その時間を予約ページに出します。予約は**割り当ての順で、空いている最初の人**に入ります
- Google カレンダーをつないだ担当者は、**カレンダーに予定がある時間は予約が入りません**（受けられない時間は、カレンダーに予定を入れてふさぎます）。終日の予定がある日も受けません（`allDayBlocks`）
- 予約が入ると、担当者のカレンダーに予定が入り、**Google Meet の URL が自動で作られ**、確認メール・リマインドの `{{meeting_url}}` になります。作れないときは担当者の `meetingUrl`、それも無ければ予約枠の `meetingUrl`
- 同じ担当者に、予約枠をまたいで同じ時間の予約は入りません。担当者には予約・キャンセルのたびにメールが届きます（`email`）
- 担当者がいない予約枠は、これまでどおり1人で受けます（同じ時刻に1件まで）

```bash
# 1) 担当者を追加（name で冪等）
curl -s -X POST https://line-affi.com/api/bot/consultation-staff \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"山田 花子","email":"hanako@example.com","photoUrl":"https://…/hanako.jpg","bio":"元銀行員。家計の見直しが得意です"}'
# 2) 予約枠に担当者を割り当てる（並び順 = 割り当ての優先順。[] で担当者なしに戻る）
curl -s -X PUT https://line-affi.com/api/bot/consultations/$CONSULTATION_ID/staff \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"staffIds":["'$STAFF_A'","'$STAFF_B'"]}'
```
- **Google カレンダーの連携は本人の同意が要るので API ではできません。** 応答の `staff.google.connectUrl`（7日有効）を担当者本人に送り、本人がボタンを押してつなぎます。つなぐまでは予約枠の受付時間どおりに予約を受けます。人には画像つきの手順 **`https://line-affi.com/guide/team-booking`** を案内する（Google の審査は通っているので、ふつう警告は出ない。出たときは「詳細」→「line-affi.com（安全ではないページ）に移動」で進む）
- 連携の状態は `staff.google.status`: `CONNECTED`（連携済み）/ `NOT_CONNECTED`（未連携）/ `ERROR`（連携が切れた。つなぎ直すまでこの人には予約が入らない。`lastError` に理由）/ `GOOGLE_NOT_CONFIGURED`（運営側の設定待ち）
- 見るカレンダーを変える: `GET /api/bot/consultation-staff/{id}/calendars` で一覧（`writable` が書き込めるもの）→ `PATCH /api/bot/consultation-staff/{id}` に `{"busyCalendarIds":["primary","…@group.calendar.google.com"],"writeCalendarId":"primary","allDayBlocks":true}`。自分のカレンダーは `"primary"`
- 変更は `PATCH /api/bot/consultation-staff/{id}`（POST と同じ項目。連携の解除は `{"disconnectGoogle":true}`）。受付を止めるだけなら `{"isActive":false}`。`DELETE` は今後の予約がある人には `400 HAS_UPCOMING_BOOKINGS`
- 予約枠の担当者は `GET /api/bot/consultations/{id}/staff`（`mode`: `TEAM` / `SOLO`）。予約一覧（`/bookings`）の各予約に `staff`・`meetUrl`（Meet）・`meetingUrl`（案内する参加URL）が付きます
- 担当者が1人の予約枠では、予約ページにその人の写真・名前・紹介文が出ます。複数なら予約完了の画面に、割り当てた人が出ます
- ウェビナーから個別相談へ誘導するときは、ウェビナーの `ctaUrl` を予約ページ（`/book/{slug}`）にします。紹介リンクから来た人は、ウェビナー → 予約 → 購入まで紹介者が引き継がれます（予約したメールアドレスで後から買われたときも）

### クーポン（要 `coupons:write` ⚠）

⚠ **お客様の支払額を直接変えます。** コード・割引額・期間・回数を人に見せ、了承を得てから発行してください。

```bash
curl -s -X POST https://line-affi.com/api/bot/coupons \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"code":"AUTUMN500","discountType":"AMOUNT","discountValue":500,"offerId":"'$OFFER_ID'","validUntil":"2026-10-31T23:59","usageLimit":100}'
```
- 月額の商品（`billingType: "MONTHLY"`）にはクーポンを使えません（購入ページに欄が出ず、申し込みでも受けません）
- `discountType`: `AMOUNT`（円引き）/ `PERCENT`（%引き・1〜100）。`offerId` を省略すると全案件で使えます。日時はタイムゾーン無しなら日本時間。`usageLimit` 省略で無制限
- 発行は `code` で冪等（同じコードがあれば作らず・変えずに既存を返す）
- 状態は `GET /api/bot/coupons` / `{id}` の `status`（`ACTIVE` / `INACTIVE` / `SCHEDULED` / `EXPIRED` / `USED_UP`）と `usedCount`
- 停止・再開・期間延長・上限変更は `PATCH /api/bot/coupons/{id}`（`isActive` / `validFrom` / `validUntil` / `usageLimit`）。**割引額・種類・コード・対象案件は変えられません**（配ったコードの意味が変わるため。止めて新しいコードで作る）
- 使い終わったものは削除より停止を勧めます（利用実績が残る）

### LINE の自動応答（要 `autoreply:write` ⚠。読むだけなら `offers:read`）

```bash
curl -s -X POST https://line-affi.com/api/bot/auto-replies -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"offerId":"'$OFFER_ID'","trigger":"KEYWORD","keyword":"料金","matchType":"CONTAINS","message":"料金表はこちらです…","isActive":false}'
```
- 友だちが送ったメッセージに、条件に合う最初のルールの `message` を返します。案件別が全体共通（`offerId` 省略）より優先、同じ中では `sortOrder` の小さい順
- **本物の友だちに LINE が届きます。** 特に `trigger:"ANY"` はどんなメッセージにも返します。`isActive:false` で作り、文面を人に見せて了承を得てから `PATCH` で `{"isActive":true}` にしてください
- 一覧 `GET /api/bot/auto-replies`（`?offerId=…｜global`・`&isActive=`）、変更・削除 `PATCH` / `DELETE /api/bot/auto-replies/{id}`。同じ内容の再送は増えません。返信文は 5,000 文字まで

### LINEバン対策: 予備アカウントと再接触メール

予備の LINE 公式アカウントを登録しておくと、毎日凍結をチェックし、凍結を検知したら運営者に警告メールが届きます。

```bash
# 登録（要 line:connect。同じ channelId なら更新。Secret は応答では伏せ字）
curl -s -X POST https://line-affi.com/api/bot/line/channels -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"label":"予備1","channelId":"1234567890","channelSecret":"'$SECRET'","liffId":"1234567890-AbCdEfGh"}'
# 登録したら必ず凍結チェック（result: OK / BANNED / UNKNOWN）
curl -s -X POST https://line-affi.com/api/bot/line/channels/$CHANNEL_ID/check -H "Authorization: Bearer $LINE_AFFI_API_KEY"
```
- `BANNED` は凍結か、Channel ID と Secret の取り違えです。取り違えなら `PATCH /api/bot/line/channels/{id}` で直して再チェック（`channelSecret` に伏せ字をそのまま送ると変更なし）
- 一覧 `GET /api/bot/line/channels`、削除 `DELETE /api/bot/line/channels/{id}`
- **プランの上限**: つなげる LINE 公式アカウント（チャネル）の数はプランで決まります（LP・料金表の「LINE連携 ◯チャンネル」。STANDARD は30・PRO は無制限。数え方は、案件に結びついたチャネル（`lineChannelId`）と、このプールのチャネルの和集合＝重複は1つ。`GET /api/bot/account` の `plan.limits.lineChannels`）。上限に達しているときに、まだつないでいない公式アカウントを**新しく**つなごうとすると `403 PLAN_LIMIT_REACHED`（`POST /api/bot/line/connect`（dryRun でも）・案件の作成・変更の `lineChannelId`・このプールの登録）。すでにつないでいるチャネルの再設定・別の案件への同じチャネル・認証情報の更新は、上限を超えていても断りません。プランの変更は人が管理画面で行います

**凍結されたら: 再接触メール `POST /api/bot/line/recontact`（要 `line:recontact` ⚠）** — メールアドレスのある**顧客全員**に、予備アカウントの友だち追加を案内します。**取り消せません。**
1. `confirm` を付けずに呼び、見本を受け取る（何も送られません。`dryRun:true` でも同じ）
```bash
curl -s -X POST https://line-affi.com/api/bot/line/recontact -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"backupChannelId":"'$CHANNEL_ID'","subject":"【重要】LINEアカウント変更のお知らせ","body":"{{name}} 様\n\nLINE公式アカウントを変更いたしました。\n下記より新しいアカウントを友だち追加してください。\n\n{{add_url}}"}'
```
2. 応答の `remaining`（送る人数）・`sample`（見本）・`warnings` を**そのまま人に見せ、了承を得る**
3. 了承を得たら**同じ内容**に `"confirm":true` を付けて**1回だけ**呼ぶ。送信ジョブができ、サーバーが裏で全員に送ります（顧客が何千人でも、呼ぶのは1回。送った人には二度届きません）。返り値の `jobId` で進み具合を `GET /api/bot/line/recontact/{jobId}` から確認できます（`status`: QUEUED → SENDING → DONE、`total` / `sent` / `failed`）
   - 差出人はアカウントのメール配信設定です。設定が無いと `400 EMAIL_NOT_CONFIGURED`
   - 送信中にもう一度 `confirm:true` で呼ぶと `409 RECONTACT_IN_PROGRESS`（終わるまで新しく送れません）
   - `status` が `STOPPED` のときは、配信サービス側の問題か、予備チャネルが使えなくなった（凍結など）ので止めています（`lastError` に理由）。直してから同じ内容でもう一度 `confirm:true` で呼ぶと、まだ送っていない人にだけ送ります
- 差し込みは `{{name}}`（無ければ「お客様」）と `{{add_url}}`（顧客ごとの友だち追加URL）。`backupChannelId` を省略すると候補（`activeChannels`）が返ります

### 動画（要 `videos:write`。読むだけなら `offers:read`）

ファイルはこのシステムを通さず、発行された**1回限りのアップロード先**へ直接送ります。**`sizeBytes`（ファイルの大きさ）を必ず付けてください。** 付けると分割アップロード（tus）になり、30GB まで送れて、途中で切れても続きから送れます（付けないと Cloudflare の仕様で 200MB まで）。

```bash
# 1. アップロード先を発行（requireSignedURLs は省略時 true = 限定公開）
SIZE=$(wc -c < /path/to/lecture01.mp4 | tr -d ' ')
curl -s -X POST https://line-affi.com/api/bot/videos \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"title":"第1回 講義","requireSignedURLs":true,"sizeBytes":'$SIZE'}'
# → { "videoId": "...", "uploadMethod": "tus", "uploadURL": "https://upload.cloudflarestream.com/tus/...", "upload": { "script": "..." } }

# 2. 応答の upload.script の FILE= をファイルの場所に書き換えて、そのまま実行する（APIキーは付けない）
#    50MB ずつ PATCH で送り、「届いた位置 / 全体」を表示します。途中で止まったら同じ script をもう一度実行すれば続きから送れます

# 3. 数十秒おきに状態を確認（status が READY になれば使える）
curl -s https://line-affi.com/api/bot/videos/<videoId> -H "Authorization: Bearer $LINE_AFFI_API_KEY"
```
- 尺は **2時間まで**。`uploadURL` は1回限りです
- 使わなかった枠は `UPLOADING` のまま残り、**上限100本**に数えられます。`DELETE /api/bot/videos/{id}` で消してください（上限到達は `403 VIDEO_LIMIT_REACHED`）。削除前に `GET` の `usedBy` で使われている教材・ウェビナーを確認
- **視聴連動ルール** `POST /api/bot/videos/{id}/rules`（`{"thresholdPct":50,"addTag":"見込み度高","startSequenceId":"<シナリオID>"}`）。**`startSequenceId` を入れると、条件を満たした実在のお客様にメールが届き始めます。人の了承を得てから**。一覧 `GET …/rules`、削除 `DELETE …/rules?ruleId=…`

### 動画をページに貼る（視聴の記録つき）・見た人でステップメールを送り分ける

販売者の講座ページ・LP（どのサイトでも）に、動画ライブラリの動画を**視聴の記録つき**で貼れます。コードは `GET /api/bot/videos/{id}?offer={案件slug}&part=1&total=3` の `video.embed.snippet` をそのまま渡してください（管理画面の動画詳細「ページに貼るコード」と同じ）。

```html
<script src="https://line-affi.com/embed/video.js" async></script>
<div data-lineaffi-video="{動画ID}" data-offer="{案件slug}" data-part="1" data-total="3"></div>
```
- 任意の属性: `data-threshold="80"`（「見た」とする割合。既定 90）／`data-kind="special"`（特別編。特別ページに貼るとき）／`data-reveal="#next-button"`（見終えたら表示する要素。ページ側で `hidden` にしておく）。GET のクエリ `threshold` `kind` `reveal` でも入ります
- **実際に再生した秒数**だけを数えます（早送り・同じところの見直しは数えない）。判定の割合に達すると、div から `lineaffi:watched` という CustomEvent（`detail.part` など）が出ます
- 記録されるのは、**LINE の視聴ページ（`/liff?state=watch:{slug}`）から来た人**（URL に `?w=` が付く人）だけです。`w` が無い人は再生と合図だけで、記録は送りません
- 記録できるのは、案件の**視聴ページの転送先（`watchUrl`。特別編は `specialPageUrl`）と同じサイト**に貼ったときだけです。別のサイトに貼るなら先に `PATCH /api/bot/offers/{id}` で `watchUrl` を合わせる
- 限定公開（`requireSignedURLs: true`）の動画は、案件を付けたコードを、その講座ページに貼ったときだけ再生できます
- 見た分は、その動画の**視聴連動ルール**（`/api/bot/videos/{id}/rules`）も動かします。LINE の人から顧客を引き、タグは付け、シナリオ開始はメールアドレスの分かる顧客だけです

**ステップメールの送り分け**（要 `stepmail:write`）: 各回に `sendCondition` を付けると、送る時刻に判定し、合わない人にはその回を送らずに次の回へ進みます。
```bash
curl -s -X PATCH https://line-affi.com/api/bot/stepmail/step \
  -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"sequenceId":"…","stepNumber":3,"sendCondition":{"type":"video_watched","videoId":"…","pct":90}}'
```
- `type`: `video_watched`（この動画を `pct`% 以上見た人だけ。既定 90）／`video_not_watched`（見ていない人だけ）／`all_watched`（案件 `offerSlug` の講座の動画 `total` 本を全部見た人だけ。既定 3）／`all_not_watched`（まだ全部は見ていない人だけ）。`null` で条件なし（全員に送る）
- `PUT` / `POST /api/bot/stepmail/step` でも `sendCondition` を一緒に渡せます（`PUT` で省略すると今の条件のまま）。今の条件は `GET /api/bot/stepmail` の `steps[].sendCondition`
- 判定は購読者のメールアドレスと同じ顧客の視聴（会員サイト・ウェビナー・埋め込み）で行います。**メールの分かる顧客がいない人は「見ていない」扱い**（見た人だけの回は届かない）。条件を付ける前に人に確認してください

### 会員サイトの教材（要 `members:write`。読むだけなら `offers:read`）

会員サイトは案件ごとにあり、購入者だけが見られます。章（section）の中に教材（content）が並びます。

```bash
curl -s "https://line-affi.com/api/bot/member-content?offerId=<offerId>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 今の構成とログインURL
curl -s -X POST https://line-affi.com/api/bot/member-content/sections -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' -d '{"offerId":"<offerId>","title":"第1章 はじめに"}'
curl -s -X POST https://line-affi.com/api/bot/member-content -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"offerId":"<offerId>","sectionId":"<sectionId>","title":"1-1 全体像","streamVideoId":"<READYの動画ID>","bodyHtml":"<p>この回のポイント…</p>"}'
```
- 章・教材とも同じタイトルなら増えません。動画は `READY` の `streamVideoId`（限定公開のまま会員だけが見られる）か、YouTube・Vimeo の `videoUrl`（`https://` から）
- 変更 `PATCH /api/bot/member-content/{id}`（`title` / `bodyHtml` / `videoUrl` / `streamVideoId` / `sectionId` / `isActive`。`isActive:false` で下書き）、章名 `PATCH …/sections/{id}`、並べ替え `POST /api/bot/member-content/reorder`（`sectionIds` と `contentIds` を全件、新しい順番で）
- 章を消しても中の教材は「章なし」に残ります。教材の `DELETE` は受講者の進捗記録も消えるので、人に確認してから
- 購入者への案内には、GET の応答の `offer.loginUrl` を使ってください
- **値段の階段の講座（`groupTag` と `tierFromDay` で段を分けた案件）は、一番下の段（`tierFromDay` が最小）にだけ教材を登録します。** どの段を買った人も、その会員サイトを共有して見られます（段ごとに同じ教材を登録しないでください）
- **会員サイトのロゴ**: `PATCH /api/bot/offers/{id}` に `{"memberLogoUrl":"https://…/logo.png"}`（要 `offers:write`。`http(s)` の画像URL、`""` / `null` で消す）。会員サイトのヘッダー（案件名の左）とログイン画面の上に、高さ 32〜40px・幅は自動で出ます。
- **購入ページの画像**: `PATCH /api/bot/offers/{id}` に `{"purchaseImageUrl":"https://…/img.webp"}`（要 `offers:write`。`http(s)` の画像URL か、センターに上げた画像の `/api/uploads/…`。`""` / `null` で消す）。購入ページ（`/purchase/{slug}`）の見出し・説明の下、値段の上に、幅いっぱい（最大 560px）で出ます。人は案件の設定画面「購入ページ設定」の「購入ページの画像」から上げられます（大きい画像は自動で縮めます）。値段の段がある案件は段ごとの案件に別々に入ります（新しく作る段には、元の案件の画像を写します）。値段や割引が描かれた画像は、その値段の案件だけに入れる（別の値段の段・通常プランに入れない）
  未設定ならアカウントのロゴ、それも無ければロゴなし。値段の階段の講座は、その段に無ければ一番下の段のロゴを使います。画像ファイルのアップロードは管理画面の案件編集（「会員サイトのロゴ」）で人が行います（API はURLの指定だけ）

#### 会員だけが開けるファイル（マニュアルの PDF など）

```bash
curl -s -X POST https://line-affi.com/api/bot/member-files -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -F offerId=<offerId> -F contentId=<講のID（任意）> -F "fileName=公式LINEの作り方.pdf" -F "file=@./manual.pdf;type=application/pdf"
curl -s "https://line-affi.com/api/bot/member-files?offerId=<offerId>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 一覧
curl -s -X DELETE "https://line-affi.com/api/bot/member-files?id=<id>" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 消す
```
- 置けるのはいまは **PDF だけ・1つ 9MB まで**（要 `members:write`。一覧は `offers:read`）。`fileName` は受講生がダウンロードしたときの名前
- 応答の `path`（`/members/{slug}/files/{id}`）は、**ログインした会員（その講座の有効な権利がある人）だけ**が開けます。ログインしていなければログイン画面へ。検索には出ません
- 講の本文には自動では入りません。`PATCH /api/bot/member-content/{id}` の `bodyHtml` にリンク（例 `<p>▶ <a href="/members/{slug}/files/{id}" target="_blank">公式LINEの作り方（マニュアル・PDF）</a></p>`）を書いてください
- 値段の階段の講座は、どの段の案件を指定しても教材のある一番下の段に置きます

#### 会員だけが見られる1枚のツールのページ（HTML＋data）

メール登録や購入をした会員だけが使える、1枚の Web ツール（例：毎日更新のネタ帳）を会員サイトの中に置けます。

```bash
# 置く・入れ替える（html と data は片方だけでもよい。送らなかった方はそのまま）
curl -s -X PUT https://line-affi.com/api/bot/member-tool/<slug> -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' --data-binary @- <<'JSON'
{"html":"<!doctype html><html><head><meta charset=\"utf-8\"></head><body>…</body></html>","data":{"days":[{"date":"2026-10-18","items":["…"]}],"updated":"2026-10-18T06:00:00+09:00"}}
JSON
# 毎日の更新は data だけ送る（python なら json.dumps({"data": data}) を PUT）
curl -s https://line-affi.com/api/bot/member-tool/<slug> -H "Authorization: Bearer $LINE_AFFI_API_KEY"            # 大きさ・更新日時
curl -s -X DELETE "https://line-affi.com/api/bot/member-tool/<slug>?which=data" -H "Authorization: Bearer $LINE_AFFI_API_KEY"  # 消す（which を省くと両方）
```
- 要 `members:write`（GET は `offers:read`）。キーのアカウントの案件だけ。`html` は **2MB まで**、`data` は JSON にして **5MB まで**
- 会員は `/members/{slug}/tool` で開きます（ログイン＋その講座の有効な権利がある人だけ。未ログインはログイン画面へ、ログイン後はこのページに戻る）。講座のトップには「ツールを開く」の入口が出ます。講が0件でツールだけの案件も「準備中」にはならず、登録完了メールに会員ログインの URL が入ります
- メールなどからツールへ直接案内するときは `/members/{slug}/login?to=tool` を使ってください
- `html` は1枚にまとめてください（CSS・JS は中に書く。外の CDN の読み込みは可）。line-affi は sandbox の iframe（`allow-scripts allow-popups`・別のオリジン扱い）で表示します。**Cookie・localStorage は使えない前提で**（使うなら try/catch で、無くても動くように）。リンクは `target="_blank"` で新しいタブに開けます
- `data` は表示のときに `<head>` の直後へ `window.NETA_DATA = <data>` として差し込みます。窓の中から相対パスの `fetch('./data.json')` はできないので、**`window.NETA_DATA` があればそれを使う**作りにしてください
- 値段の階段の講座は、どの段の slug を指定しても教材のある一番下の段に置きます

#### 講ごとの場面（動画の時刻へのジャンプ）と読むマニュアル

教材に `chapters`（場面）を入れると、会員サイトの動画の下に場面の一覧が出て、押すとその時刻から再生します（YouTube・Vimeo・Stream）。場面に `text`（説明）があれば、その下に「読むマニュアル」として場面ごとの見出し（時刻のボタン付き）と本文が出ます。

```bash
curl -s -X POST https://line-affi.com/api/bot/member-content -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' -d '{
  "offerId":"<offerId>","sectionId":"<sectionId>","title":"第1講 この講座で作るもの",
  "videoUrl":"https://www.youtube.com/watch?v=XXXXXXXXXXX",
  "bodyHtml":"<p>この講義のまとめ…</p><h3>次の講義までに</h3><p>…</p>",
  "chapters":[
    {"sid":"01-01","t":5,"title":"本を1冊出したら、あとは自動で回る","text":"この講座は…"},
    {"sid":"01-02","t":"0:29","title":"自己紹介"}
  ]}'
```
- 場面の形: `{ sid?, t, title, text? }`。`t` は秒（`125`）か `"2:05"` / `"1:02:05"`。**`start` も `t` と同じ意味、`scenes` も `chapters` と同じ意味で受け付けます**（取り込み元の JSON をそのまま渡せます）。`sid`（場面の ID）は同じ講の中で重ならないように。1講 300 場面まで、`text` は 1場面 5,000 字まで
- 直すときは `PATCH /api/bot/member-content/{id}` に `chapters`（全部置き換え。`[]` か `null` で消す）。POST は同じ章・同じ題名の教材があると作らずに `created:false` を返すので、そのときは PATCH で
- 本文（`bodyHtml`）の中に `[button:サポートLINEを友だち追加する](https://lin.ee/xxxx)` と書くと、緑の大きなボタンになります（リンク先は `http(s)` だけ。`style` で作ったボタンは背景などが消えることがあるので、この書き方を使ってください）
- 本文（`bodyHtml`）の中に `[2:05]` や `[▶ 2:05]` と書くと、押すとその時刻へ飛ぶボタンになります。`<h3 data-t="125">見出し</h3>` のように見出しに `data-t`（秒）を付けると、見出しの頭に時刻のボタンが付きます
- 会員サイトの URL に `?c=<教材ID>&t=<秒>` を付けると、その講のその時刻から開けます（Q&A の飛び先もこの形）

#### 計測リンク（講ページに置いたバナー・リンクのクリックを数える）`/api/bot/tracked-links`（要 `offers:write`。読むだけなら `offers:read`）

好きな URL（https のみ）へのリンクを作り、押された回数を数えます。リンクは `https://line-affi.com/l/{code}`。開くとクリックを記録して、登録した行き先へ転送します（行き先はリンクの付け足しでは変えられません）。

```bash
curl -s -X POST https://line-affi.com/api/bot/tracked-links -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"name":"KAP バナー","destinationUrl":"https://example.com/kap"}'
# → { "success": true, "existed": false, "link": { "id": "…", "code": "AbC12345", "url": "https://line-affi.com/l/AbC12345", "isActive": true, … } }
curl -s "https://line-affi.com/api/bot/tracked-links?from=2026-10-01T00:00:00%2B09:00" -H "Authorization: Bearer $LINE_AFFI_API_KEY"
# → links[].clicks: { total（全期間）, inPeriod（from〜to）, bySource: [{ fromTag, offerSlug, contentId, clicks }] }
curl -s -X PATCH https://line-affi.com/api/bot/tracked-links -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"linkId":"…","isActive":false}'   # 止める（/l/{code} は「見つかりません」になり転送しない）。name / destinationUrl も変えられる
```
- 同じ名前・同じ行き先で `POST` し直しても増えません（`existed: true`）
- 教材の本文（`bodyHtml`）には `[button:KAP を見る](https://line-affi.com/l/AbC12345)` か `<a href="https://line-affi.com/l/AbC12345" target="_blank">…</a>` で貼ります。会員サイトから押された分は、案件（`offerSlug`）と教材（`contentId`）が自動で記録されます
- 同じリンクを何か所かに置くときは `?from=lec06` のように付け足すと、場所ごとに分けて数えられます（英数字と `- _ .` の40文字まで）
- 機械のアクセス（検索エンジン・リンクの下見など）は数えません。会員としてログイン中の人は顧客も記録します（応答には出しません）。IP はハッシュでだけ残します

#### Q&A（よくある質問）

講座ごとの Q&A。会員サイトの「よくある質問」（`/members/{slug}/faq`。検索と「こんな時は」の入口）と、関係する講のページの下に出ます。答えから、関係する講の場面へ飛べます。

```bash
# 一括で入れる（最大500件。key（または id）が同じものは上書きなので、何度流しても重なりません）
curl -s -X POST https://line-affi.com/api/bot/member-faq -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' -d '{
  "offerId":"<offerId>",
  "items":[
    {"id":"qa-001","category":"本が売れない時","question":"出版したのに1冊も売れません","answer":"まず…\n\n次に…",
     "refs":[{"lecture":12,"sid":"12-04","start":431},{"sid":"20-02"}]}
  ]}'
curl -s "https://line-affi.com/api/bot/member-faq?offerId=<offerId>&q=表紙" -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 一覧・検索（offers:read）
```
- 1件の形: `key`（または `id`）、`question`、`answer`（普通の文章。改行は段落）か `answerHtml`、`scenario`（または `category`。「こんな時は」の入口の分類。無ければ「その他」）、`refs`（関係する講と場面。複数可）、`isActive`
- `refs` の講は `contentId` → `contentTitle`（題名が同じ教材）→ `sid`（その場面を持つ教材）→ `lecture`（題名が「第N講」で始まる教材）の順で探します。秒は `t` か `start`。**`sid` で指すと、あとで場面の秒を直しても Q&A の飛び先がずれません**。先に教材（場面つき）を入れてから Q&A を入れてください
- 1件でも間違い（見つからない講・sid など）があれば何も書かずに 400 を返し、どの `items[i]` のどこが悪いかを返します
- 直す `PATCH /api/bot/member-faq/{id}`（送った項目だけ。`refs` は置き換え）、消す `DELETE /api/bot/member-faq/{id}`、並べ替え `POST /api/bot/member-faq/reorder`（`{"offerId":"…","ids":[全件を新しい順に]}`）。並び順は入れた順
- 値段の階段の講座は、どの段の `offerId` を渡しても一番下の段に入ります

#### 特典の講座（バンドル。ある案件を買った人に、別の講座の会員サイトも見せる）

```bash
# 案件 A を買った人に、講座 B（同じアカウントの案件）の会員サイトも見せる（要 offers:write。置き換え。[] で無し）
curl -s -X PATCH https://line-affi.com/api/bot/offers/<A の offerId> -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"bonusOfferIds":["<B の offerId>"]}'
# 既に A を買った人にも付ける（要 members:write。何度呼んでも同じ）
curl -s -X POST https://line-affi.com/api/bot/offers/<A の offerId>/bonus-grant -H "Authorization: Bearer $LINE_AFFI_API_KEY"
```
- A の購入（1回払い・月額・アップセル）で B も見られるようになります。1回払いは期限なし、月額は A の契約の間だけ。A を返金すると B も外れます（B を自分で買った人の権利はそのまま）
- A の購入完了メールに「特典の講座」の入口（ログイン済みで開けるリンク）が自動で付きます。本文の好きな場所に出すなら `{{bonus_links}}` と書く。A に会員サイトの教材が無くても B の入口は付きます
- A の会員サイトの上にも「🎁 特典の講座」のリンクが出ます
- **A に教材が無い（特典だけ）とき**: A の会員サイトは「🎁 この講座も受講できます」のカード（B を開く大きなボタン）だけの表示になり、特典が1つなら購入者は B へそのまま移ります。
  そこに案内（サポートLINE・お願いごとなど）を出すなら、**教材を足さずに** `bonusNoteHtml`（特典の案内のコメント）を入れます。カードの上に出て、あると自動では移らず、コメントとボタンの画面を出します（`""` / `null` で外すと自動で移る表示に戻る）。
  文章（空行で段落）か HTML。`[button:文字](https://…)` で緑のボタン。サニタイズして保存します（要 `offers:write`。GET でも返ります）。管理画面は「会員コンテンツ」→ 案件 → 🎁 の枠の「💬 特典の案内のコメント」
  ```bash
  curl -s -X PATCH https://line-affi.com/api/bot/offers/<A の offerId> -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
    -d '{"bonusNoteHtml":"ご購入ありがとうございます。\nご不明な点はサポートLINEでどうぞ。\n\n[button:サポートLINEを友だち追加する](https://lin.ee/xxxx)"}'
  ```
- 設定を外しても、それまでに付いた B の権利は消えません（消すのは人に確認してから管理画面の顧客で）
- `bonus-grant` は、A の会員サイトを見られる人と、A の有効な1回払いの購入がある人（`/api/bot/conversion` で計上した外部の決済も）に付けます。**外部の決済の計上（`/api/bot/conversion`）では自動では付かない**ので、そのときは `bonus-grant` を呼んでください

- 手で追加した会員（下の `/api/bot/member-access`）には、特典の講座は付きません（`bonus-grant` でも付けません）

#### 会員を手で追加する（line-affi の外で買った人・招待したい人。要 `members:write`。一覧を見るだけなら `customers:read` か `offers:read`）

PayPal で直接買った人や、招待したい人を、その講座の会員にします。**購入（Purchase）と報酬は作りません。** 特典の講座には広げません（その講座だけ）。
顧客がいなければ作り、顧客の記録に「手で追加」のイベント（どの講座に・誰が・いつ）が残ります。管理画面は「会員コンテンツ」→ 講座 →「👥 会員を手で追加」。

```bash
# 1) まず確かめる（既定は dryRun: true。何も変えない）
curl -s -X POST https://line-affi.com/api/bot/member-access -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' -d '{
  "offerId":"<offerId>",
  "items":[{"email":"taro@example.com","name":"山田太郎"},{"email":"hanako@example.com"}],
  "sendInvite":false
}'
# → summary: { toAdd（追加する人数）, alreadyMembers（すでに会員）, invalid（形のおかしいアドレス）, duplicates（重複）, newCustomers }
# 2) 人に summary を見せて了承をもらってから、同じ内容に "dryRun": false を付けて送る
# 一覧（kind: manual=手で追加 / purchase=購入・登録 / bonus=特典。q で検索、page・perPage で送り）
curl -s "https://line-affi.com/api/bot/member-access?offerId=<offerId>&kind=manual" -H "Authorization: Bearer $LINE_AFFI_API_KEY"
# 外す（顧客・購入は残る。この講座の特典で付いた講座の権利も外れる）
curl -s -X DELETE https://line-affi.com/api/bot/member-access -H "Authorization: Bearer $LINE_AFFI_API_KEY" -H 'content-type: application/json' \
  -d '{"offerId":"<offerId>","emails":["taro@example.com"]}'
```
- 宛先は `emails`（文字列の配列）か `items`（`[{email,name}]`）。両方も可。1回 5,000 件まで（それ以上は分けて呼ぶ）。大文字小文字・前後の空白が違うだけのアドレスは同じ人（重複）として1人にまとめます
- すでに会員の人は何もしません。月額が切れて見られなくなっていた人は、期限なしで付け直します（`reactivated`）
- **案内メール** `"sendInvite": true`（既定は送らない ⚠ 人にメールが届きます）: 講座の名前・ログインページ（`/members/{slug}/login`）・ログインのしかた（購入時のメールアドレスを入れるとログイン用のリンクが届く）を、新しく会員にした人だけに送ります。
  文面を変えるなら `inviteSubject` / `inviteBody`（差し込み `{{name}}` `{{offer}}` `{{login_url}}`）。dryRun の応答の `invite.sample` で見本、`invite.warnings` で送れない理由（メール配信の設定が無い・差出人のドメインが未認証）を確かめてから。
  差出人は購入完了メールと同じ（案件の `noticeFromName`・返信先、アカウントのメール配信設定）。配信停止・不達の人には送らず、1日の上限を超えた分は翌日に送ります。送信はサーバーが裏で行うので、進み具合は `GET /api/bot/member-access?jobId=<応答の invite.jobId>`
- メール配信の設定が無いのに `sendInvite: true` で送ると、何も追加せずに `400 EMAIL_NOT_CONFIGURED` を返します（追加だけなら `sendInvite: false`）
- 外すときの応答の `withActiveSubscription` に出た人は、月額の契約が続いています。次の更新で見る権利が戻るので、止めるなら月額の契約を解約してください

## アフィリエイターにお知らせメールを送る（要 `affiliates:announce` ⚠）

新しい LP・キャンペーン・報酬アップなどを、アフィリエイターの受信箱に届けます（ポータルのお知らせにも出せます）。**人に届くので取り消せません。必ず先に `dryRun` で人数を確かめ、件名・本文・人数を人に見せて了承を得てから送ってください。**

```bash
# 1. 何人に届くか（何も作らない・送らない）
curl -s -X POST https://line-affi.com/api/bot/affiliates/announce -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"subject":"新しいLPを追加しました","body":"…","offerId":"'$OFFER_ID'","dryRun":true}'
# → { recipients: 120, skipped: [{ label: "メール通知をオフにしている", count: 3 }] }

# 2. 了承を得たら dryRun を外して送る。呼ぶのは1回だけ（何千人でも、サーバーが裏で最後まで送る）
curl -s -X POST https://line-affi.com/api/bot/affiliates/announce -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"subject":"新しいLPを追加しました","body":"…","offerId":"'$OFFER_ID'"}'
# → { announcementId, status: "QUEUED", recipients, skipped }

# 3. 進み具合（status: QUEUED → SENDING → DONE。sent / total、送れなかった人と理由。止まったときは STOPPED と error）
curl -s https://line-affi.com/api/bot/affiliates/announce/$ANNOUNCEMENT_ID -H "Authorization: Bearer $LINE_AFFI_API_KEY"
```
- `body` はテキスト（改行はそのまま）か簡単な HTML（リンク可。script などは消えます）。`subject` は150文字まで・改行不可
- `subject`・`body` には差し込み（`{{報酬_LINE登録:slug}}`、`{{ランク名}}`、`{{報酬一覧}}` など。上の「ランクと差し込み」）が使えます。宛先ごとに、その人のランクの金額で送ります。`dryRun` の応答の `rewardPreview` がランクごとの見え方、`warnings` が分からない差し込み
- `offerId` を付けると、その案件の紹介リンクを発行した人だけに送ります。無ければ有効なアフィリエイター全員
- `alsoPortal`（既定 true）で、ポータルのお知らせにも出します
- 送らない人: メール通知をオフにしている人（本人がポータルの「登録情報」で選ぶ）、メールアドレスの形が正しくない人
- 差出人はアカウントのメール配信設定。宛先ごとに別々のメールなので、宛先どうしでアドレスは見えません。末尾に誰から届いたかと止め方が自動で付きます
- **`status` が `STOPPED` のとき**は、配信サービス側の問題（キーの失効・送信元ドメインの未認証・障害など）で全部失敗したので止めています（誰にも「失敗」と記録していません）。`error` の原因を人に伝えて直してもらい、`POST /api/bot/affiliates/announce/{announcementId}/resume`（要 `affiliates:announce`）で再開すると、まだ送っていない人にだけ送ります
- 送信の状況は `GET /api/bot/affiliates/announce/{announcementId}`（要 `affiliates:read`）。1万人で数分〜十数分かかります。送り直す操作は要りません（止まっても毎分の処理が続きから送り、同じ人に二度は届きません）
- **予約配信**: `scheduledAt`（ISO 8601。日本時間なら `+09:00` を付ける。いまの1分後〜1年以内）を付けると、その時刻に自動で送り始めます（応答は `status: "SCHEDULED"`）。予約のあいだはポータルにも出ず、`alsoPortal` は送り始めるときに効きます。毎分の処理（`/api/cron/announcements`）が時刻を見て送信待ちにします
- **送る相手の絞り込み**: `rankIds`（ランクのID）または `rankNames`（ランク名。例 `["VIP"]`）でそのランクの人だけ（省略 = 全ランク。ランク未設定の人は、いちばん下のランクとして扱います）。`statuses` は `["ACTIVE"]` だけ指定できます（省略しても同じ。**停止中（PAUSED）・BAN の人には送りません**。指定すると `400 INVALID_STATUS`）。`dryRun` の人数にも効きます
- **送る相手の確認（`dryRun`）**: `dryRun: true` で何も送らずに、`recipients`（送信先の人数）・`skipped`（メール通知オフなどで送らない人の内訳）・`excluded`（停止中・BAN で除外される人数）・`sample`（届く人の先頭20人。`affiliateId` `name` `email`）・`page`/`pageCount`（`page` を指定すると次の20人）を返します。`search` にアフィリエイター一覧の検索条件（例 `"q=たなか&rank=ランクID&emailNotifications=on"`。一覧の画面の検索と同じ項目）を渡すと、その条件の人だけが対象です（削除した人・停止中・BAN の人は入りません）。管理画面は、一覧の「この条件の人に一斉メールを送る」から同じ条件で作成画面を開き、送信先の一覧と、自分だけに送る「テスト送信」が使えます（テスト送信は画面のみ。API はありません）
- **配信前の編集と削除**（要 `affiliates:announce`）: 予約中（`SCHEDULED`）か、メールで送らないお知らせは `PATCH /api/bot/affiliates/announce/{announcementId}` で直せます（`subject` `body` `offerId` `alsoPortal` `scheduledAt` `rankIds`/`rankNames` `statuses` のうち指定した項目だけ。`scheduledAt: null` で予約をやめて、すぐ送る＝取り消せないので人の了承を得てから）。送り始めたものは `400 ALREADY_SENDING`。`DELETE` で削除（予約中なら取り消し＝メールは送られません。送っている最中は `400 SENDING_NOW`）
- **エラーの確認と再送**: `GET …/announce/{announcementId}` の `failures[]`（`deliveryId`・`email`・`error`・`retryCount`・`maxRetries`・`canRetry`）が、送れなかった人と理由です。**再送は1人につき最大3回まで（初期値。`GET/PATCH /api/bot/affiliates/announce/settings` の `maxRetries` で 0〜10 に変更できます。PATCH は要 `affiliates:announce`。管理画面は「お知らせ管理」の上部）**（`canRetry:false` の人は管理画面で「再送できません」と出ます。全員が上限なら `400 RETRY_LIMIT`）。`POST /api/bot/affiliates/announce/{announcementId}/retry`（要 `affiliates:announce`。`{"deliveryIds":["…"]}` でその人だけ、省くと送れなかった全員）で送り直せます。**アドレスの間違いが原因のことが多いので、先に直してから**（`PATCH /api/bot/affiliates/{id}` の `email` は人の了承を得て）。管理画面は「お知らせ管理」の「配信エラー◯件」から
- **アカウントのメール配信設定（Resend など）が必要です。** 無いと `400 EMAIL_NOT_CONFIGURED`（共通の差出人では一斉送信しません）。配信サービスの月間・1日の送信上限にも注意してください

## アフィリエイター向けシステムメールの文面を直す（`/api/bot/settings/mail-templates`、要 `email:settings` ⚠）

アフィリエイターに**自動で届く**メール（招待・登録の確認・ログイン・新案件・新しいLP・報酬の確定）の、件名と本文をセンターごとに直せます。**直していないメールは、今までの文面のまま届きます。**人に届く文面が変わるので、**文面は人に見せて了承を得てから**保存してください。

```bash
curl -s https://line-affi.com/api/bot/settings/mail-templates -H "Authorization: Bearer $LINE_AFFI_API_KEY"     # 一覧（key・いつ届くか・使える差し込み・初期の文面・直した文面）
curl -s -X PUT https://line-affi.com/api/bot/settings/mail-templates/new_offer -H "Authorization: Bearer $LINE_AFFI_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"subject":"【新案件】{{案件名}} が始まります","body":"{{氏名}} さん\n\n新しい案件です。\n{{案件の説明}}"}'
# → { "success": true, "key": "new_offer", "preview": { "subject": "…", "body": "…" } }（例の値で置き換えた見本）
curl -s -X DELETE https://line-affi.com/api/bot/settings/mail-templates/new_offer -H "Authorization: Bearer $LINE_AFFI_API_KEY"   # 既定（今までの文面）に戻す
```
- `key`: `invitation`（運営者が招待したとき）／`signup_confirm`（公開登録・招待の登録確認）／`login`（このセンターのログイン）／`new_offer`（新案件）／`new_lp`（新しいLP）／`commission_confirmed`（報酬の確定）
- 本文は**文章**です（HTML は使えません。空の行で段落、URL は自動でリンク）。件名は1行・150文字まで、本文は5000文字まで
- 差し込みは `{{氏名}}` `{{センター名}}` `{{案件名}}` `{{報酬額}}` など、**種類ごとに決まっています**（一覧の `variables`）。使えない差し込みは `400 UNKNOWN_VARIABLE`
- **文面を直しても必ず付くもの**（一覧の `fixedParts`）: ログイン・登録・招待のボタン（リンク）、「案件を確認する」などのボタン、末尾の案内（誰からのメールか・止め方）。リンクを消してログインできなくなることはありません
- 次に送るメールから変わります（送ってしまったメールは変わりません）。一斉メール（お知らせ）は対象外で、送るたびに件名・本文を指定します。お客さん向けのメールは案件ごとの設定です
- 管理画面は「連携設定 → システムメール」

## 他のアフィリエイトセンターの不正情報（不正情報の共有）

このシステムを使う他のアフィリエイトセンターで**不正として停止（BANNED）された人**と、あなたのアフィリエイター（**このセンターで報酬が1件以上ある人**）が「ログインアカウント・メールアドレス・電話番号・振込先口座」のどれかで一致したら分かります。相手のセンター名や、相手側の氏名・連絡先・口座は返りません（あなたのセンターの情報も同じ扱い）。`GET /api/bot/account` の `next` に `shared_fraud` が出ていたら、該当者がいます。

- 一覧 `GET /api/bot/fraud/shared`（要 `affiliates:read`。「問題なし」にした人は除く。`?includeDismissed=true` で含める）→ `{ count, matches: [{ affiliateId, displayName, status, matchTypes: ["SAME_USER"|"EMAIL"|"PHONE"|"BANK_ACCOUNT"], bannedOn, reasons, centerCount }] }`
- 確認済み・問題なし `POST /api/bot/fraud/shared/{affiliateId}/dismiss`（要 `affiliates:write`）。後で別のセンターが新たに停止したら再び出ます
- このセンターでも停止 `POST /api/bot/fraud/shared/{affiliateId}/ban`（要 `affiliates:write`）。**未払いの報酬（保留・確定）もすべて取り消します。必ず人の了承を得てから**
- 別人でも一致することがあります（家族や会社で同じ口座・電話を使う場合など）。自動で停止せず、一覧を人に見せて判断してもらってください
- `PATCH /api/bot/affiliates/{id}` で status を BANNED にすると、停止日と理由の区分が他センターへ共有されます。ACTIVE / PAUSED に戻すと共有も止まります

## API では出来ないこと（人が管理画面で行う）
- **出金（アフィリエイターへの支払い）の申請** — 本人がポータルで行います（API では作れません）。申請から振り込み済み・却下までは `POST /api/bot/payout-requests/{id}` でも記録できますが、**人の了承を得てから**。最低支払額は `PUT /api/bot/settings/payout` で変えられます
- メール配信サービスのアカウント作成と、送信ドメインの DNS 認証（キーの登録とテスト送信は API で出来ます）。人には **`https://line-affi.com/guide/mail-setup`** を案内する
- 決済（Stripe / PAY.JP / PayPal）の設定。**決済の本番の秘密鍵は AI に渡さず、人が画面に直接貼る**。PAY.JP を使う人には、**紹介コード `9614137E-4F2E-447A-B6CE-63E23211381F`** を案内する（PAY.JP の管理画面 右上「ユーザー設定」→「契約情報の確認」→「クーポン適用」 `https://console.pay.jp/d/settings/plan` に入れると、翌月1日から決済手数料の引き下げ期間が始まる。コードと手順は管理画面の「決済設定」にも出ている）。PayPal の申し込みからキーの登録・テスト購入までは、人に **`https://line-affi.com/guide/paypal-setup`** を案内する
- 報酬の承認・却下、不正アラートの確認、プランの変更、APIキーの発行・失効・権限の変更

## やってはいけないこと
- テスト前に `showToAffiliates:true` にしない。人の了承なしに公開・報酬額の変更・メール文面の確定をしない
- LINE Developers の Webhook URL を、他のツールが使っているのに書き換えない（書き換えさせない）
- `403 INSUFFICIENT_SCOPE` を回避しようとしない
- APIキーやチャネルシークレットを、LP・リポジトリ・チャットのログに残さない
