メインコンテンツへスキップ
Client SDK キー (osk_ 接頭辞) で認証。非同期 (即時返却) + ポーリング構造 — POST はラウンドを開始して即座に識別子を返し、クライアントはディテールエンドポイントで状態を確認します。

モデル

  • chat_id = 1 つの会話。最初のラウンドで発行され、以降同じ chat_id でフォローアップを累積。
  • round_id = ラウンド識別子。POST 1 回 = ラウンド 1 個。各ラウンドは prompt + 結果スナップショットを保持。
  • フォローアップコンテキスト (2026-05-19~): 同じ chat の 直前の COMPLETED ラウンドすべて(user prompt, assistant meal JSON) のペアとして自動的に llm-demo の conversation_history に積まれます。クライアントは {chat_id, prompt} を送るだけで OK — 「ワイン追加」「豚肉抜きで」のような短い精密化発話でも直前ラウンドの ingredients を基準点に精密化されます。meal を復元できないラウンドは静かにスキップ。
  • 応答言語: profile.locale 優先、無ければテナント既定言語、それも無ければ ko。メッセージ系テキスト (recipe_title / intro_message / recipe_description / recommendation_message / notes 等) がその言語で出力されます。サポート言語: ko / en / ja
  • reason フィールドは例外: テナント管理画面に通知として表示され、UI 言語切替時にデータ再取得なしで即座に表示言語を切り替える必要があるため、常に {ko, en, ja} オブジェクトで返ります (3 言語すべて充填済み)。
  • フォローアップ上限: 1 ラウンド内で refinement が 8 回を超えると 409 REFINEMENT_CAP_EXCEEDED。新ラウンド (POST) または新 chat でリセット。
  • 進行中ラウンド保護 (2026-05-20~): 直前ラウンドの step2 がまだ完了していない 状態でのみ 409 PRIOR_ROUND_IN_FLIGHTpipeline_progress.step2 == "done" (todos 充填済み) 以降は step3/step4 がバックグラウンド進行中でも follow-up を即時受付。ラウンドは step2 境界まで直列化され、step3/4 はラウンドごとに独立してバックグラウンド進行。

1. 推薦 kickoff

POST /v1/chat/{profile_id}/recommend
新規 chat または既存 chat へのフォローアップ。即時 200 + 識別子返却、結果はポーリング。

リクエスト

{
  "chat_id": "(Optional) UUID",
  "prompt":  "今夜のおかず何がいい?"
}
フィールド必須説明
chat_idstring×未指定なら新規 chat 作成、指定すれば既存 chat に新ラウンド追加
promptstringユーザーの自然言語クエリ
🧑 persona はプロフィールから自動注入 — リクエスト body には別途フィールドなし。サーバーが GET /v1/profile/{profile_id}data.persona を読み取り、値があれば user_query prelude に persona(ペルソナ: ...) として prepend。プロフィール登録/更新時に PUT /v1/profile/{profile_id}persona フィールド (自由記述文字列、nullable) で更新できます。
fire-and-forget — このエンドポイントは即時 (~100ms) で応答します。LLM 呼び出しはバックグラウンドで進行するため、結果はポーリングで取得します。RefinementCapExceeded のような LLM エラーも同期 4xx ではなく round.status=error + last_error として永続化 → ポーリングで検知。

レスポンス 202

{
  "success": true,
  "data": {
    "chat_id":  "a14b2f9c-3d5e-4f2a-9a8b-1c2d3e4f5a6b",
    "round_id": "8f53c2b0-..."
  }
}
その後 GET /v1/chat/{profile_id}/recommend/{chat_id} をポーリングし、最後のラウンドの pipeline_progress.step4 == "done" になったら完了。

2. チャットリスト (ページング)

GET /v1/chat/{profile_id}/recommend
クエリデフォルト説明
page00-indexed
limit100ページサイズ (最大 500)
orderdesccreated_at ソート、desc / asc

レスポンス 200

{
  "success": true,
  "data": {
    "items": [
      {
        "chat_id":      "...",
        "total_rounds": 2,
        "rounds": [
          { "round_no": 1, "round_id": "..." },
          { "round_no": 2, "round_id": "..." }
        ],
        "created_at":   "2026-04-29T05:28:08Z"
      },
      {
        "chat_id":      "...",
        "total_rounds": 1,
        "rounds": [
          { "round_no": 1, "round_id": "..." }
        ],
        "created_at":   "2026-04-28T22:11:00Z"
      }
    ],
    "pageInfo": {
      "total": 42,
      "page":  0,
      "limit": 100,
      "order": "desc"
    }
  }
}
各要素は chat_id + total_rounds + ラウンド識別子のミニ配列 ({round_no, round_id} のみ) + created_at。ラウンド本体 (prompt / 結果) はディテール / 単一ラウンドエンドポイントで取得。ページング情報は pageInfo に集約。

3. チャットディテール (rounds[])

GET /v1/chat/{profile_id}/recommend/{chat_id}
ポーリングターゲット。呼び出すたびに進行中 (KICKED_OFF) のラウンドを llm-demo に再問い合わせし、完了していれば DB にスナップショット保存。完了済みラウンドはそのまま返却。

レスポンス 200 — 進行中 (最後のラウンド未完了)

🔑 スキーマ一貫性 (2026-05-09~) — pipeline_result がまだ空でも、スナップショットのキー類は 常に存在 します。クライアントはキー有無ではなく 値 ベースの分岐 (pipeline_progress.stepN == "done"items.length > 0) で記述してください。
{
  "success": true,
  "data": {
    "chat_id":         "a14b2f9c-...",
    "profile_id":      "6b97957c-...",
    "profile_summary": { "name": "佐藤花子", "gender": "F", "age": 32, "locale": "ja" },
    "created_at":      "2026-04-29T05:28:08Z",
    "rounds": [
      {
        "round_no":         1,
        "round_id":         "8f53c2b0-...",
        "prompt":           "今夜のおかず何がいい?",
        "created_at":       "2026-04-29T05:28:08Z",
        "updated_at":       "2026-04-29T05:28:08Z",
        "status":           "in_progress",
        "back_list_status": null,
        "pipeline_progress": {
          "step1": "running",
          "step2": "pending",
          "step3": "pending",
          "step4": "pending"
        },
        "todos":             null,
        "recommended_items": [],
        "notifications":     { "ad": null, "coupon": [] }
      }
    ]
  }
}
各 step のデータ可用タイミング (pipeline_progress.stepN == "done" の時点から):
  • step1 (~5s): 専用キーは未充填 (LLM 応答完了 + meal 生成)
  • step2 (~10s): todos.recipe_title / intro_message / recipe_description / recommendation_message / ingredients[] / items[].product 充填
  • step3 (~15s): recommended_items[] 充填 (reason = カテゴリタグ、reason_detail = 分析文、reason_detail_marketing = 顧客向けコピー)
  • step4 (~20s): notifications.ad.items (サイネージ) + notifications.coupon[] (アプリ内クーポン) 充填
  • notifications.coupon_spot[] は step 非依存 — 店舗のクーポンスポットマスターを毎ポーリング store-API から取得 (LLM 推薦ではない)
{
  "success": true,
  "data": {
    "chat_id":      "...",
    "profile_id":   "...",
    "profile_summary": { ... },
    "created_at":   "...",
    "rounds": [
      {
        "round_no":         1,
        "round_id":         "...",
        "prompt":           "今夜のおかず何がいい?",
        "created_at":       "...",
        "updated_at":       "...",
        "status":           "completed",
        "back_list_status": "completed",
        "todos": {
          "recipe_title":           "ピリ辛ビーフカレー",
          "intro_message":          "今夜はピリ辛ビーフカレーはいかがですか?...",
          "recipe_description":     "日本式カレー...",
          "recommendation_message": "今日のような日にぴったりのメニューです。プロフィール情報を反映したパーソナライズ推薦理由が入ります。",
          "ingredients": [
            {
              "name":          { "ko": "두부",  "en": "tofu",         "jp": "豆腐" },
              "category":      "Pantry",
              "quantity":      1,
              "quantity_unit": { "ko": "모",    "en": "block",        "jp": "丁" }
            },
            {
              "name":          { "ko": "배추",  "en": "napa cabbage", "jp": "白菜" },
              "category":      "Produce",
              "quantity":      0.25,
              "quantity_unit": { "ko": "포기",  "en": "head",         "jp": "玉" }
            }
          ],
          "items": [
            {
              "quantity": 1,
              "product":  { "id": 196, "name": "...", "price": 5900, "price_unit": "₩", ... }
            }
          ],
          "total": 5900
        },
        "recommended_items": [
          {
            "rec_id":                   "uuid-...",
            "reason":                   "cross-sell",
            "reason_detail":            { "ko": "Cross-sell ...", "en": "Cross-sell ...", "ja": "Cross-sell ..." },
            "reason_detail_marketing":  { "ko": "今晩の食卓をぐっと豊かに。", "en": "今晩の食卓をぐっと豊かに。", "ja": "今晩の食卓をぐっと豊かに。" },
            "product":                  { "id": 512, "name": "カベルネ・ソーヴィニヨン 750ml", ... }
          }
        ],
        "notifications": {
          "ad": {
            "signage": { "x": 4, "y": 1, "radius": 2 },
            "items": [
              {
                "item_id":                196,
                "rec_id":                 "uuid-...",
                "item_name":              "ヒゲタしょうゆ 1L",
                "item_name_en":           "Higeta Soy Sauce 1L",
                "brand":                  "Higeta",
                "zone_id":                12,
                "zone_name":              "Pantry",
                "type":                   "ads",
                "discount_pct":           5,
                "applied_score":          0.85,
                "priority_score":         0.95,
                "method":                 "copurchase_lift",
                "reason_type":            "cross-sell",
                "reason_detail":          { "ko": "Cross-sell ...", "en": "Cross-sell ...", "ja": "Cross-sell ..." },
                "reason_detail_marketing":{ "ko": "カレーと一緒にいかがですか。", "en": "カレーと一緒にいかがですか。", "ja": "カレーと一緒にいかがですか。" },
                "rank":                   1,
                "price":                  860,
                "price_unit":             "¥",
                "product":                { "id": 196, "name": "...", "price": 860, "price_unit": "¥", "location": { "x": 4, "y": 1, "radius": 2 }, "...": "..." }
              }
            ]
          },
          "coupon": [
            {
              "item_id":      881,
              "rec_id":       "uuid-...",
              "item_name":    "...",
              "type":         "coupon",
              "zone_id":      76,
              "discount_pct": 10,
              "price":        3600,
              "price_unit":   "¥",
              "...":          "...",
              "product": {
                "id":           881,
                "name":         "深煎りごまドレッシング 380ml",
                "brand":        "キユーピー",
                "barcode":      "4906028184943",
                "price":        3600,
                "price_unit":   "¥",
                "src_url":      "https://cdn.ones1ght.com/store/product/no_image.png",
                "quantity":     100,
                "section_code": "PA",
                "section_name": "食料品",
                "location":     { "x": 28, "y": 22.794, "radius": 0.5 }
              },
              "location":     { "x": 28, "y": 22.794, "radius": 0.5 }
            }
          ],
          "coupon_spot": [
            {
              "item_id":                42,
              "rec_id":                 null,
              "item_name":              "プロテインバー 50g",
              "item_name_en":           "Protein Bar 50g",
              "brand":                  "...",
              "zone_id":                null,
              "zone_name":              null,
              "type":                   "coupon",
              "discount_pct":           10,
              "applied_score":          null,
              "priority_score":         null,
              "method":                 null,
              "reason_type":            null,
              "reason_detail":          null,
              "reason_detail_marketing":null,
              "rank":                   1,
              "price":                  1200,
              "price_unit":             "¥",
              "product":                { "id": 42, "name": "...", "price": 1200, "...": "..." },
              "location":               { "x": 12.3, "y": 4.5, "radius": 1.0 }
            }
          ]
        }
      },
      {
        "round_no": 2,
        "round_id": "...",
        "prompt":   "ワイン以外のおすすめは?",
        "...": "フォローアップ — 直前 prompt がコンテキストとして送られる"
      }
    ]
  }
}
ラウンドフィールド説明
round_no1-indexed の順番。何番目の質問へのラウンドか (最後の要素の round_no が総ラウンド数と一致)
statusin_progress / completed / error
back_list_statusllm-demo の not_started / in_progress / completed (スナップショット未取得時は null)
todos買い物リスト + recipe メタ。各要素 {quantity, product}total = Σ(product.price × quantity)
todos.recommendation_messagestep1 (meal) のパーソナライズ推薦理由 — プロフィール (name / gender / age / extra) シグナルを反映した人間向けコメント。recipe_description (メニュー一般説明) とは別物。profile locale で出力
todos.items店内ピックアップ動線順にソート (2026-05-20~) — product.section_code 基準の安定ソート。順序: 青果(PR) → デリ(DL) → ベーカリー/製菓(BK/SW/SN) → 酒類(AL) → 乳製品/飲料(DY/DR) → 食料品(PA) → 精肉(MT) → 水産(FI) → 冷凍(FR)。ピッカーがカテゴリーを一筆書きで回れるように。表に無いコード (EC/HH/CK 等) は末尾。同カテゴリー内は元の順序維持
recommended_itemsメニューに合う cross-sell。reason = カテゴリタグ ("cross-sell" / "upsell" / "impulse")、reason_detail = 運用者/ダッシュボード向け分析文 (lift / CVR / co_count)、reason_detail_marketing = 顧客向けコピー、product = store マージ結果。reason_detail / reason_detail_marketing は常に {ko, en, ja} オブジェクト — upstream が単一 locale のみ返す間は同じ値を 3 キーにコピー (shape のみ一貫)
notifications.adサイネージ ads バンドル { signage, items }signage = サイネージ 1 台の {x, y, radius} (なければ null)、items = type=ads 推薦リスト (top 3)。priority_score 降順 1-indexed rank
notifications.couponアプリ内クーポン項目配列。各項目の location {x, y, radius}item_id から store の product → Section 座標を解決して埋め込み (未マッチ時 null)
notifications.coupon_spot店舗管理側のクーポンスポットマスター (LLM 推薦ではなく、毎ポーリング store-API から取得)。coupon[]完全に同じ 19 フィールド schema — iOS が同じデコーダで両者を処理可能。type = "coupon"rec_id / reason_* / applied_score / priority_score / method 等推薦シグナルはすべて nulldiscount_pct ← 管理画面 discount_valuerankspot_no ASC インデックス。location は product.location ではなく スポット自身の座標 {x, y, radius}
notifications.*[].item_idstore カタログの product id。llm-demo が付与できない場合 nullitem_name / item_name_en でカード表示は可能
notifications.*[].productstore DB マッチ結果のオブジェクト一式 — id / name / brand / barcode / price / price_unit / src_url(画像) / location / section_code / section_name / quantity (未マッチ時 null)
notifications.*[].price / price_unitproduct.price / product.price_unit の平坦化 — カード価格 hot path 用 (未マッチ時いずれも null)。旧 price_yen は廃止 — 店舗 DB を単一ソース化
notifications.*[].type"ads" (サイネージ) / "coupon" (アプリ内)。グループキーと同じだが項目単位処理時にも使用
notifications.*[].rec_id推薦イベント追跡 id — /trigger/signageitems[].rec_id / /trigger/couponrec_id として返す
notifications.*[].reason_type / reason_detail / reason_detail_marketingstep4 推薦理由 3 種 — reason_type = カテゴリタグ ("cross-sell" / "zone_efficiency" 等)、reason_detail = 運用者/ダッシュボード向け分析文、reason_detail_marketing = サイネージ/クーポンに表示する顧客向けコピー。reason_detail / reason_detail_marketing{ko, en, ja} オブジェクト (upstream が単一 locale の間は 3 キーで同じ値)
last_errorstatus: error 時のみ (例: meal_suggest failed: upstream timeout / upstream chat session expired)

4. 単一ラウンド取得

GET /v1/chat/{profile_id}/recommend/{chat_id}/{round_id}
ラウンド 1 個だけ取得。ディテールと同じ自動リフレッシュ動作 (KICKED_OFF / CREATED なら llm-demo に再問い合わせて完了/エラー遷移) — ラウンド単位でポーリングしたい時に。 レスポンス shape はディテールの rounds[i] と同一 (chat メタ / profile_summary なし、該当ラウンドのみ):
{
  "success": true,
  "data": {
    "round_no":         2,
    "round_id":         "...",
    "prompt":           "...",
    "created_at":       "...",
    "updated_at":       "...",
    "status":           "completed",
    "back_list_status": "completed",
    "todos":            { ... },
    "recommended_items":  [ ... ],
    "notifications":    { "ad": { "signage": ..., "items": [...] }, "coupon": [...] }
  }
}
所有権 (tenant + api_key + profile + chat → round) のいずれかが不一致なら NOT_FOUND 404。

5. サイネージ ads トリガー送信 — POST /trigger/signage

iOS sdk が UWB によりサイネージの location radius に 進入 / 離脱 した際にそれぞれ呼び出します。action フィールドで分岐 (default "enter")。
  • enter: ラウンドの notifications.ad.items をサイネージへ forward → RECOGNIZED 画面遷移。サーバー側で (1) item ごとに sdk_chat_event 永続化 (2) テナント管理者 SSE 配信 (3) store-API へ forward。
  • exit: サイネージ IDLE 即時復帰。永続化 / SSE はスキップ、store-API forward のみ。

サイネージ番号別の画面表示ポリシー

Gateway は items[] 全体を store-API へ forward し、どの項目をどう表示するかはサイネージ番号ごとのクライアントが決定します。
サイネージIDLE 画面ACTIVE (RECOGNIZED) 画面のデータソース
#1 (入場サイネージ)管理画面で登録した 1~4 スライド (クロスフェード)itemsdiscount_pct 降順でソートし top 1 を単一商品カードで表示。横/縦自動切替。reason_detail を訴求コピーとして使用
#2 (店内サイネージ)9 スロット (3 スライド × 商品 3 個)3 セクション構成 — ① forgotten[0] (買い忘れ商品) ② items[1] || items[0] (広告 1 件、インデックス基準) ③ store-API の signage_event から enabled=true の 1 件をランダム
データのないセクションは自動非表示 (例: forgotten 未添付ならその領域を省略)。サイネージ #1 は単一カード設計のため discount_pct の並びが意味を持ち、#2 は広告横に「買い忘れ / イベント」を同時表示する設計のため index 1 を優先 (index 0 はより強いシグナルとして他の用途に予約)。
POST /v1/chat/{profile_id}/trigger/signage
Authorization: Bearer {SDK_KEY}
Content-Type: application/json

Request — Enter (エリア進入)

最小ペイロードは chat_id + round_id のみ。action 省略時は "enter" が default。Gateway がラウンドの永続化された notifications.ad.items を lookup して ADS_TRIGGERED を発行します。
{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe"
}

Request — Exit (エリア離脱)

{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "action":   "exit"
}
応答: { "success": true, "data": { "event_ids": [], "accepted": true, "action": "exit" } }

Request — Enter (items override + forgotten)

items を明示的に渡すと、その値が優先されます (ライブデータ / 部分セット用)。forgotten でユーザーが店舗内で買い忘れた商品リストも添付可能 (2026-05-20~):
{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "items": [
    {
      "rec_id":        "rec_a1b2c3",
      "item_id":       196,
      "item_name":     "スパゲッティーニ",
      "item_name_en":  "Spaghettini",
      "brand":         "De Cecco",
      "zone_id":       12,
      "zone_name":     "Pantry",
      "price_yen":     4900,
      "discount_pct":  17,
      "applied_score": 0.85,
      "priority_score":0.95,
      "method":        "copurchase_lift",
      "reason_detail": "...",
      "reason_detail_marketing": "..."
    }
  ],
  "forgotten": [
    { "item_id": 882, "name": "玉ねぎ", "quantity": 1 },
    { "any": "shape", "is": "fine" }
  ]
}
フィールド必須説明
chat_id推薦を受けた chat
round_idその round
itemsoptional省略 / 空配列 → Gateway がラウンドの永続データから自動 lookup。指定すると優先
items[].rec_id✅ (items 送信時)各項目の推薦イベント追跡 id
items[].item_id / item_name / item_name_en / brand / zone_id / zone_name / price_yen / discount_pct / applied_score / priority_score / method / reason_type / reason_detail / reason_detail_marketingoptionalFIREHOSE / サイネージ表示用。表示ポリシーは signage_no 別 — 上記「サイネージ番号別の画面表示ポリシー」表を参照 (#1 = discount_pct top 1 単一カード、#2 = items[1] || items[0] 広告 + forgotten + event の 3 セクション)
forgottenoptionalユーザーが店舗内で買い忘れた商品リスト。iOS sdk が該当する todos.items を そのまま添付。各要素の schema は自由 (any) — Gateway は加工せず、永続化 payload・管理者 SSE 配信・store-API forward すべてに root 階層でそのまま透過。空配列 / 省略時はキー自体不在。exit 時は無視

Response 202

{ "success": true, "data": { "event_ids": ["evt_..."], "accepted": true } }
event_ids は永続化された項目数だけ返却。呼び出し頻度 = location 進入回数 (1 ラウンド + 1 サイネージ = 1 呼び出し)。 サーバーは item ごとに step4 の store_reasoning.analyst_comment を raw pipeline から抽出し、analyst_comment キーで SSE payload に付与します — 管理画面 user-analytics のサイネージイベントの「推薦根拠」として表示。

6. クーポン発行/使用送信 — POST /trigger/coupon

iOS sdk が notifications.coupon[] の項目をアプリ内で発行/使用/拒否する際に項目 1 件ずつ呼び出します。サイネージ非関連のため (1) 永続化 (2) テナント管理者 SSE 配信 のみ。
POST /v1/chat/{profile_id}/trigger/coupon
Authorization: Bearer {SDK_KEY}
Content-Type: application/json

Request

最小ペイロードは chat_id + round_id + rec_id のみで OK。Gateway がラウンドの永続化された notifications.coupon[] から rec_id で照合してメタを自動補完します。
{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "rec_id":   "rec_x9y8z7"
}
メタを全て指定することも可能。指定値が優先されます:
{
  "chat_id":       "c_91ab",
  "round_id":      "r_77fe",
  "rec_id":        "rec_x9y8z7",
  "kind":          "issued",
  "item_id":       512,
  "item_name":     "オリーブオイル",
  "item_name_en":  "Olive Oil",
  "brand":         "Colavita",
  "zone_id":       76,
  "zone_name":     "Pantry",
  "price_yen":     9900,
  "discount_pct":  20,
  "applied_score": 0.78,
  "priority_score":0.72,
  "method":        "copurchase_lift",
  "reason_detail": "...",
  "reason_detail_marketing": "..."
}
フィールド必須説明
chat_id / round_id上記と同じ
rec_id推薦イベント追跡 id (ラウンドのスナップショットとの照合キー)
kindoptional"issued" / "redeemed" / "dismissed" (デフォルト issued)
その他optional省略時はマッチした項目から自動補完。指定時はその値が優先

Response 202

{ "success": true, "data": { "event_id": "evt_...", "accepted": true } }

7. 個別推薦の表示ログ — POST /trigger/recommend

iOS sdk が recommended_items[] (step3) の 1 項目をアプリ内で表示/タップした際に 1 回限りのログ として呼び出します。サイネージ / store-API 非関連 — (1) sdk_chat_event 永続化 (2) テナント管理者 SSE 配信 のみ。
POST /v1/chat/{profile_id}/trigger/recommend
Authorization: Bearer {SDK_KEY}
Content-Type: application/json

Request

{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "rec_id":   "rec_a1b2c3"
}
フィールド必須説明
chat_id / round_id上記と同じ
rec_id推薦イベント追跡 id (recommended_items[].rec_id または id)
Gateway がラウンドの recommended_items[] から rec_id でマッチングし、item_name / price / reason_detail / reason_detail_marketing 等のメタを自動 lookup して payload に埋め込みます。step3 の surface_reasoning.analyst_comment も raw pipeline から抽出して analyst_comment キーで付与 — 管理画面 user-analytics の個別推薦イベントの「推薦根拠」として表示。

Response 202

{ "success": true, "data": { "event_id": "evt_...", "accepted": true } }

8. ラウンドのバーコード一括取得 — GET /recommend/{chat_id}/{round_id}/barcodes

ラウンド内の product をカテゴリ別にまとめ、EAN-13 PNG (base64) と共に返します。管理画面 / デモ用 — store-API 再呼び出しなし (ラウンドの平坦化データ + ZXing でその場 PNG 生成)。
GET /v1/chat/{profile_id}/recommend/{chat_id}/{round_id}/barcodes
Authorization: Bearer {SDK_KEY}

Response 200

{
  "success": true,
  "data": {
    "chat_id":  "...",
    "round_id": "...",
    "todos":         [ /* 買い物リストの product */ ],
    "recommends":    [ /* 個別推薦の product */ ],
    "notifications": {
      "ads":    [ /* サイネージ ads の product */ ],
      "coupon": [ /* クーポンの product */ ]
    }
  }
}
各 product エントリ — 5 フィールドのみ:
{
  "name":          "豆腐",
  "barcode":       "8801234567890",
  "price":         1980,
  "price_unit":    "¥",
  "barcode_image": "data:image/png;base64,iVBORw0KGgo..."
}
  • barcode が 13 桁の数字でない、または checksum エラーの場合は barcode_image: null — クライアント側は null ガードのみ実装。
  • 同一 product が複数カテゴリに現れることがあります。

ポーリングガイド

  • 推奨間隔: ~2 秒 (フルパイプライン平均 ~46s)
  • rounds 配列の最後の要素の status のみチェック — 過去ラウンドは常に completed (または error) で固定
  • フォローアップ POST 後にポーリングすると配列長 +1、最後の要素が in_progress から再開
  • error / TTL 期限切れ: 新規 chat 作成 (chat_id なしで POST) を推奨

エラーコード

HTTPcode原因
400INVALID_REQUESTbody 検証失敗 (prompt 欠落等)
404NOT_FOUNDprofile / chat 不存在または所有権不一致
402INSUFFICIENT_CREDITテナントクレジット残高不足
409REFINEMENT_CAP_EXCEEDED同一ラウンドでフォローアップが 8 回を超過 — 新ラウンド/新 chat でリセット
409PRIOR_ROUND_IN_FLIGHT直前ラウンドの step2 がまだ未完了 — pipeline_progress.step2 == "done" 確認後に再試行。step3/4 進行中でも step2 さえ終われば follow-up OK
500INTERNAL_ERROR上流障害等

全体例

SDK_KEY="osk_..."
PROFILE_ID="..."

# 1. 初回ラウンド — chat_id なし
RESP=$(curl -s -X POST https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend \
  -H "Authorization: Bearer $SDK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"今夜のおかず何がいい?"}')
CHAT_ID=$(echo "$RESP" | jq -r '.data.chat_id')

# 2. ディテールをポーリング (2 秒) — 最後のラウンドが completed まで
while true; do
  DETAIL=$(curl -s https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend/$CHAT_ID \
    -H "Authorization: Bearer $SDK_KEY")
  LAST_STATUS=$(echo "$DETAIL" | jq -r '.data.rounds[-1].status')
  [ "$LAST_STATUS" = "completed" ] && { echo "$DETAIL" | jq '.data.rounds[-1]'; break; }
  [ "$LAST_STATUS" = "error" ]     && { echo "$DETAIL" | jq '.data.rounds[-1].last_error'; break; }
  sleep 2
done

# 3. 同じ chat_id でフォローアップ
curl -X POST https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend \
  -H "Authorization: Bearer $SDK_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"chat_id\":\"$CHAT_ID\",\"prompt\":\"ワイン以外のおすすめは?\"}"

# 4. 累積リスト
curl https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend?page=0\&limit=20 \
  -H "Authorization: Bearer $SDK_KEY"