# Kalori External API

外部AI (Claude, ChatGPT等) から Kalori の食事データ・店舗・商品情報を取得するための API です。
ユーザーは iOS アプリで一時トークンを発行し、AI に貼り付けて使います。

## ベースURL
https://app.kalori.jp/api/v1/ext

## 認証
すべてのデータエンドポイントで HTTP ヘッダ `Authorization: Bearer ext_xxxxxx` が必要です。
`ext_` で始まる長い文字列がトークンです。

## プラン別機能
このAPIは Free / Premium で機能差があります。

| 観点 | Free | Premium |
|---|---|---|
| トークン TTL | 15分 (固定) | 1h / 6h / 24h から発行時に選択 |
| 同時アクティブトークン | 1本 | 5本 |
| 呼び出し上限 | 10 calls/day (無料枠) | 100 calls/hour |
| カタログ検索 (`/search`, `/shops`) | ✅ | ✅ |
| **記録データの読み書き（食事・栄養・運動・体重・お気に入り・味評価）** | ❌ | ✅ |
| 範囲取得 (`/meals?startDate=`) | — | 14日 |
| 範囲取得 (`/nutrition?startDate=`) | — | 90日 |
| **セット提案 (POST `/sets`)** | ❌ | ✅ |

線引きは「**カタログ = 公共財（無料）／自分の記録データへの AI アクセス = Premium**」です。
Free は1日 10 calls まで（カタログ検索が対象。超えると HTTP 429）。
記録データのエンドポイントは Free では HTTP 403 と `upgrade` フィールドを返します。
外部連携の Premium API が**初めて成功した時点**から、連続7日間（168時間）はすべて利用できます。
カタログ検索・チェーン一覧・`/me` では始まりません。パラメータ不正 (400) や枠切れ (429) で
失敗した呼び出しでも始まらないので、書式が不安なら遠慮なく試してください。
お試しが始まると、そのとき使っているトークンは Premium の既定 TTL (1時間) まで自動で延長されます（再発行は不要）。
アップグレードはアプリ内から行えます。

## エンドポイント一覧

| Method | Path | 用途 | Plan |
|---|---|---|---|
| GET | `/help` | 本ドキュメント (認証不要) | All |
| GET | `/me` | 自分の tier / 外部連携お試し / 利用可能機能 / クオータ（表示名・目標は Premium / お試し中のみ） | All |
| GET | `/meals?date=` | 指定日の食事 (`&detail=full` で内訳・店舗・量調整つき) | **Premium** |
| GET | `/meals?startDate=&endDate=` | 範囲の食事 (最大 14日) | **Premium** |
| GET | `/meals/search?q=` | **全履歴**の食事名検索 (「あの豚汁いつ？」「いつもの」用・期間上限なし) | **Premium** |
| GET | `/foods/mine?q=` | 自分のマイ食品の検索 (foodId を POST /meals へ渡すと再記録) | **Premium** |
| **POST** | **`/meals`** | **食事作成 (単品 or バッチ最大10件。productId / dishId / 手動 + components 内訳)** | **Premium** |
| **POST** | **`/analyze-text`** | **食事の自由記述を kalori の栄養エンジンで分析→マイ食品化+記録まで一気に (詳細27栄養素つき)** | **Premium** |
| **PUT** | **`/meals/:id`** | **食事修正 (portionRatio / quantity / mealType / mealDate / components / shopId 店舗後付け)** | **Premium** |
| **PUT** | **`/meals/:id/photo`** | **写真の添付 (サーバ側で最大500pxに正規化)** | **Premium** |
| **DELETE** | **`/meals/:id`** | **食事削除** | **Premium** |
| GET | `/search?q=` | **統合カタログ検索** (チェーン商品 約23,000点 + 料理名 約4,000点。iOS と同一エンジン) | All |
| GET | `/catalog/items?ids=` | カタログ id (`p_123`/`g_45`) の一括 hydrate (最大10件)。会話中に持ち回った数値を確定値に取り直す用 | All |
| GET | `/nutrition?date=` | 指定日の目標+消費+残量+運動消費+ストリーク+**スロット別予算 (slots)** | **Premium** |
| GET | `/nutrition?startDate=&endDate=` | 範囲の日次サマリー (最大 90日) | **Premium** |
| GET | `/nutrition/detailed` | 27栄養素の集計+推奨摂取量 (鉄分/食物繊維/塩分/ビタミン等。「◯◯足りてる？」用) | **Premium** |
| GET | `/weights?date=` / `?startDate=&endDate=` | 体重の取得 (最大 90日) | **Premium** |
| POST | `/weights` | 体重の記録 (1日1件 UPSERT) | **Premium** |
| GET | `/exercises?date=` | 指定日の運動記録一覧 + 合計消費 | **Premium** |
| **POST** | **`/exercises`** | **運動の記録 (cal + 種類 + 時間)** | **Premium** |
| **DELETE** | **`/exercises/:id`** | **運動記録の削除 (manual のみ)** | **Premium** |
| GET | `/favorites` | お気に入り一覧 (栄養値つき) | **Premium** |
| POST | `/favorites` | お気に入り追加 (productId or dishId、任意 slots=時間ごと) | **Premium** |
| DELETE | `/favorites/:type/:id` | お気に入り削除 (type=product/dish) | **Premium** |
| PUT | `/ratings` | 食事の味評価 (mealId + 1〜9、UPSERT) | **Premium** |
| DELETE | `/ratings/:mealId` | 味評価の取り消し | **Premium** |
| **PUT** | **`/budgets/slot`** | **食事スロット別カスタム予算の設定 (UPSERT)** | **Premium** |
| **DELETE** | **`/budgets/slot/:date/:slot`** | **スロット予算の解除** | **Premium** |
| **PUT** | **`/budgets/day`** | **1日の目標オーバーライド (その日だけ・null でクリア)** | **Premium** |
| **DELETE** | **`/budgets/day/:date`** | **日別オーバーライドの全解除** | **Premium** |
| **PUT** | **`/goals`** | **目標の変更 (減量/維持/増量・目標体重・期間・活動量・配分比。カロリー値は指定不可＝サーバが導出)** | **Premium** |
| GET | `/shops` | チェーン店マスタ一覧 | All |
| **POST** | **`/sets`** | **残りPFCに合うセット提案** | **Premium** |
| POST | `/feedback` | 開発チームへのフィードバック送信 (感想・要望・不具合。クオータ消費なし) | All |

> 旧 `GET /products` / `GET /dishes` は 2026-07-04 に `GET /search` へ統合されました。

---

## GET /me
自分の tier / 外部連携お試し / 利用可能機能 / クオータ消費状況を返す。AI が「自分が何をできるか」を呼び出し前に判定するための自己紹介エンドポイント。`displayName` と `goal` は個人データなので、Premium または外部連携お試し中だけ値が入り、それ以外は `null`。

`features.canStartTrial: true` は「7日の無料お試しがまだ手つかず」という意味です。この場合 `canReadPersonalData` は `false` ですが、**記録データのエンドポイントをそのまま呼んで構いません** — 最初の呼び出しでお試しが始まり、そのリクエスト自体も成功します（`/me` を見ただけでは始まりません）。ユーザーに「Premium が必要です」と伝える前に、まず一度呼んでみてください。

`metabolism` は**その人の記録から逆算した実際の代謝**（観測 TDEE）です。公式や推定式ではなく、記録した摂取カロリーと体重の傾きからエネルギー保存則で逆算しています。目標カロリーの根拠でもあるので「なぜこのカロリーなのか」「減量ペースは適正か」に答えられます。

- `observedTdeeKcal` … 運動を除いた 1 日の消費 (基礎代謝 + 生活活動 + 食事誘発性熱産生)。運動ぶんは別枠で加算される
- `trusted` … `true` なら目標カロリーの計算に実際に使われている。**`false` のときは確度が低いので数値を断定的に言わない**（「まだ推定中」と伝える）
- `weightTrendKgPerWeek` … 平滑化した体重の傾き（減量中は負）。日々の水分変動を均した値なので、体重計の増減そのものより信頼できる
- `null` … データ不足でまだ推定できていない。体重と食事の記録が増えれば出る

`goal.source` が `manual` の目標は、本人がアプリで数値を固定したもの（記録が増えても自動追従しません）。

```json
{
  "data": {
    "tier": "premium",
    "entitlementSource": "external_trial",
    "externalTrial": {
      "state": "active",
      "active": true,
      "startedAt": "2026-08-01T03:00:00.000Z",
      "endsAt": "2026-08-08T03:00:00.000Z",
      "daysRemaining": 7
    },
    "displayName": "あいうえお",
    "goal": { "cal": 2200, "protein": 110, "fat": 70, "carbs": 275, "goalType": "maintain",
              "targetWeightKg": 62, "startWeightKg": 68, "goalStartedAt": "2026-05-01", "durationDays": 120,
              "mealRatios": { "breakfast": 0.25, "lunch": 0.35, "snack": 0.10, "dinner": 0.30 },
              "source": "auto" },
    "metabolism": {
      "observedTdeeKcal": 2150, "confidence": 0.82, "trusted": true,
      "weightTrendKgPerWeek": -0.42, "intakeMeanKcal": 1890, "avgExerciseKcal": 120,
      "windowStart": "2026-07-25", "windowEnd": "2026-08-07",
      "computedAt": "2026-08-08T00:12:00.000Z"
    },
    "features": {
      "canReadPersonalData": true,
      "canStartTrial": false,
      "canWrite": true,
      "canSetProposal": true,
      "rangeMaxDays": { "meals": 14, "nutrition": 90 }
    },
    "quota": {
      "period": "hour", "limit": 100,
      "used": 23, "remaining": 77,
      "resetAt": "2026-05-07T15:00:00.000Z"
    }
  }
}
```

## クオータ可視化 (X-Quota-* レスポンスヘッダ)
データエンドポイント (`/me` 含む) のレスポンスには毎回以下のヘッダが付きます。AI は呼び出し前に `/me` で確認するか、各リクエストのヘッダを見て残量判断ができます。

```
X-Quota-Used: 23
X-Quota-Limit: 100
X-Quota-Remaining: 77
X-Quota-Period: hour          # day (Free) または hour (Premium)
X-Quota-Reset: 2026-05-07T15:00:00.000Z
```

429 を受け取ったら次のリセット時刻まで待ってから再試行してください。

## GET /meals
単日: `?date=YYYY-MM-DD`、範囲: `?startDate=&endDate=`。**どちらも省略すると JST 当日**を返します。レスポンス形は両方とも `{startDate, endDate, totals, days}` で統一 (単日でも `days` 配列1件)。食事ない日も `meals: []` で返ります。

`&detail=full` を付けると各 meal に `portionRatio / shopId / productId / dishId / nutritionSource / confidence / hasPhoto / imageUrl / baseCal / components`（内訳）が追加されます。修正 (`PUT /meals/:id`) や振り返り分析の前はこちらを使ってください。

```bash
# 単日
curl "https://app.kalori.jp/api/v1/ext/meals?date=2026-05-07" \
  -H "Authorization: Bearer ext_xxxxxx"

# 範囲 (最大 14日)
curl "https://app.kalori.jp/api/v1/ext/meals?startDate=2026-05-01&endDate=2026-05-07" \
  -H "Authorization: Bearer ext_xxxxxx"
```

```json
{
  "data": {
    "startDate": "2026-05-07", "endDate": "2026-05-07",
    "timezone": "Asia/Tokyo",
    "totals": { "cal": 1850, "protein": 95, "fat": 62, "carbs": 220 },
    "days": [
      {
        "date": "2026-05-07",
        "totals": { "cal": 1850, "protein": 95, "fat": 62, "carbs": 220 },
        "meals": [
          { "id": "abc-123", "mealType": "breakfast", "mealName": "サラダチキン",
            "cal": 113, "protein": 24, "fat": 1.5, "carbs": 0.4,
            "quantity": 1, "unit": null, "source": "product_db",
            "createdAt": "2026-05-07T08:30:00Z" }
        ]
      }
    ]
  }
}
```

## POST /meals (Premium 限定)
食事記録を作成。Content-Type: application/json 必須。

**単品** (`{...}`) または **バッチ** (`{"items": [...]}`) を受理。バッチは最大10件で1リクエスト=1クオータ消費。検証エラーは全件ロールバック。

### 登録は productId / dishId ベースを基本に
登録には3つの経路があります。**まず `GET /search` で検索して、結果の type に応じた id を渡す方法を基本**にしてください。栄養情報がDBから自動で取られるため、AI 側で PFC を推定する必要がなく、データの一貫性も保てます。

| 経路 | 必須項目 | 用途 |
|---|---|---|
| **A. productId 指定 (推奨)** | `productId` のみ (`/search` の type=product 結果) | コンビニ・チェーン店の商品 (DB登録あり。**店舗も自動で紐づく**) |
| **B. dishId 指定 (推奨)** | `dishId` のみ (`/search` の type=dish 結果) | 一般的な料理 (ラーメン・親子丼等 約4,000品。標準栄養+内訳が自動付与) |
| **C. foodId 指定** | `foodId` のみ (`/foods/mine` の結果) | 自分のマイ食品の再記録 (栄養+内訳+店舗が食品から自動付与) |
| D. 手動入力 (フォールバック) | `mealName` + `cal` (P/F/C は任意) | A〜C に存在しないもの (初めての自家製・特殊なメニュー等) |

A → B → C → D の順で試してください。

### リクエストボディ
フィールド名は kalori アプリの API / DB と同語彙（`mealName` / `mealDate` / `cal`）です。
- `productId`: 商品ID (`/search` の type=product 結果から)
- `dishId`: 料理ID (`/search` の type=dish 結果から。`mealName` を併用すると表示名を上書き)
- `foodId`: マイ食品ID (`/foods/mine` の結果から。栄養+内訳+店舗が食品から自動付与。`mealName` で表示名上書き可)
- `mealName`: 食事名 (productId/dishId 未指定時に必須)
- `cal`: kcal (手動入力では必須)
- `protein`, `fat`, `carbs`: g (任意、未指定時 0)
- `quantity`: 数量倍率 (任意、デフォルト 1.0。ハーフ=0.5、2個=2)
- `portionRatio`: 食べた割合 0〜1 (任意、デフォルト 1.0。「半分残した」= 0.5)
- `unit`: 単位表記 (任意。例: 個・杯・g)
- `mealDate`: YYYY-MM-DD (任意、省略時は JST 当日)。**過去日 = 記録の引っ越し / 未来日 = 食事計画**（未来日に入れた食事はアプリの計画グリッドの該当セルにそのまま並ぶ。「明日の夕食プランを立てて」はこれで完結）
- `mealType`: 食事スロット (任意、省略時は JST 時刻から**朝食/昼食/夕食のいずれかに**自動推定)
- `shopId`: チェーン店ID (任意。`GET /shops` の id。**productId 指定時は未指定でも商品のチェーンが自動で付きます**)
- `storeId`: 物理店舗ID (任意。通常は iOS アプリが使う Apple Place ID)
- `components`: 内訳の配列 (任意)。写真・文章から食事を分解できた場合は付けてください。アプリ側で量調整できる形で保存されます。各要素: `{name, cal?, protein?, fat?, carbs?, quantity?, unit?}`（cal 等は1個あたり、quantity が個数）。内訳がある場合、食事全体の栄養は内訳の合計 × quantity から自動計算されます

### mealType の値 (7種類)
省略時は JST 時刻から**主要3スロット（朝食/昼食/夕食）のいずれか**に自動推定されます:
4〜10時 = `breakfast` / 11〜15時 = `lunch` / 16時〜翌3時 = `dinner`。
早朝・午前・間食・夜食のスロットは自動では選ばれません — 「間食で記録して」のように
ユーザーが意図した場合だけ明示指定してください。

| 値 | 用途 | 自動推定 |
|---|---|---|
| `breakfast` | 朝食 | ✓ (4〜10時) |
| `lunch` | 昼食 | ✓ (11〜15時) |
| `dinner` | 夕食 | ✓ (16時〜翌3時) |
| `early_morning` | 早朝 | 明示指定のみ |
| `mid_morning` | 朝食と昼食の間 | 明示指定のみ |
| `afternoon` | おやつ・間食 | 明示指定のみ |
| `late_night` | 夜食 | 明示指定のみ |

「昨日の夜のラーメン」のように後から登録する場合は `mealDate` と `mealType` を明示してください。

### curl 例

```bash
# A. 検索 → productId で登録 (推奨)
curl -X POST https://app.kalori.jp/api/v1/ext/meals \
  -H "Authorization: Bearer ext_xxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"productId": 12345, "mealType": "lunch"}'

# B. 手動入力 (DBにない場合)
curl -X POST https://app.kalori.jp/api/v1/ext/meals \
  -H "Authorization: Bearer ext_xxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"mealName": "自家製サラダ", "cal": 250, "protein": 15, "fat": 8, "carbs": 25, "mealType": "lunch"}'

# 数量指定 (ハーフサイズ)
curl -X POST https://app.kalori.jp/api/v1/ext/meals \
  -H "Authorization: Bearer ext_xxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"productId": 12345, "quantity": 0.5}'
```

バッチ:
```bash
curl -X POST https://app.kalori.jp/api/v1/ext/meals \
  -H "Authorization: Bearer ext_xxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"items": [
    {"productId": 12345, "mealType": "breakfast"},
    {"mealName": "コーヒー", "cal": 5, "mealType": "breakfast"}
  ]}'
```

レスポンス (単品もバッチも常に同じ形 `{count, items[]}`):
```json
{
  "data": {
    "count": 1,
    "items": [
      { "id": "meal-uuid", "mealDate": "2026-05-07", "mealType": "lunch",
        "mealName": "サラダチキン プレーン",
        "cal": 113, "protein": 24.3, "fat": 1.5, "carbs": 0.4,
        "quantity": 1, "source": "product_db", "shopId": "seven-eleven" }
    ]
  }
}
```

制限: 1日30件まで、1バッチ最大10件 (上限到達時 429)。

## POST /analyze-text — AI 分析つき記録 (Premium 限定・2026-07-29)

食事の自由記述テキストを kalori の栄養推定エンジンで分析し、**マイ食品として保存 + 食事記録まで一気に**行います。kcal/PFC に加えて鉄分・食物繊維・ビタミン等の**詳細27栄養素**も保存され、`GET /nutrition/detailed` の充足集計に乗ります（POST /meals の手動値では詳細栄養素は付きません — 使い分けの基準）。

**写真からの記録（推奨ワークフロー）**: この API は画像を受け取りません。チャットAI・クライアント側で写真を観察し、内容を詳細なテキストに起こして `query` に渡してください — 品目と個数、量の目安（ご飯茶碗1杯・唐揚げ5個 など）、調理法、パッケージの栄養成分表示が写っていればその数値の転記。kalori の実測では分析精度のボトルネックは画像の解像度ではなく記述の具体性です。**チェーン商品と特定できる場合は `GET /search` → `POST /meals` (productId) が最優先**（公式栄養値になります）。

- `query`: 食事の描写 (必須、最大300字)
- `mealDate` / `mealType`: 省略時は今日 / 時刻から朝食・昼食・夕食に自動判定
- `shopId` / `storeName`: 店の文脈 (任意。推定精度が上がり店リンクも付く)
- `locale`: "en" で英語名も生成 (任意)
- `replaceMealId`: **訂正のとき**に、作り直す既存記録の id（下記）

応答: `foodId` / `mealId` / 推定栄養 (cal/protein/fat/carbs) / `components` (内訳) / `confidence` / `catalogMatches` (カタログ近似候補 — 確度が高ければ productId での記録し直しを提案可能) / `creditBalance`。

課金・制限は**写真アプリの AI 分析と同じ枠**: AI壁 (無料プランは 403)・日次上限・クレジット・レート制限 3回/分。

### 訂正は「消して作り直す」ではなく `replaceMealId`

「さっきのは違った、実は◯◯だった」は **`DELETE` してから記録し直さないでください**。
`replaceMealId` にその記録の id を渡すと、**同じ記録の中身だけが差し替わります**。

```bash
curl -X POST https://app.kalori.jp/api/v1/ext/analyze-text \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"query":"カツカレー 大盛り","replaceMealId":"meal-uuid"}'
```

- **id が変わらない** ので、味の評価・添付した実物写真・記録日（連続記録の判定に使う）・記録経路がそのまま残ります
- **記録日と食事スロットも引き継ぎます**（訂正した時刻ではなく、元の食事の枠のまま）
- 削除して作り直すとこれらが毎回失われ、さらに AI が古い id を掴んだまま二度目の削除で 404 を踏みます
- 存在しない id を渡すと **404**（AI 分析は走らないのでクレジットも減りません）
- 応答には `replaced: true` が付きます

## DELETE /meals/:id (Premium 限定)
誤登録の取消し用。自分の meal のみ削除可能。

```bash
curl -X DELETE https://app.kalori.jp/api/v1/ext/meals/meal-uuid \
  -H "Authorization: Bearer ext_xxxxxx"
```

```json
{ "success": true, "data": { "id": "meal-uuid", "deleted": true } }
```

存在しない / 他ユーザー所有 → 404 (`Use GET /meals?date=YYYY-MM-DD to look up the correct id.` を返す)。

AI へのアドバイス: 記録そのものを**取り消す**（無かったことにする）ときだけ DELETE を使ってください。
**「やっぱり違った、実は◯◯だった」= 内容の訂正は DELETE ではなく** `POST /analyze-text` の
`replaceMealId`（AI 推定で入れ直す）か `PUT /meals/:id`（量・スロット・内訳だけの修正）です。

## PUT /meals/:id (Premium 限定)
記録済みの食事を修正します。栄養値（effective）はサーバ側で自動再計算されます。

- `portionRatio` (0〜1): 食べた割合。「半分残した」= 0.5
- `quantity` (0〜99): 数量。「2人前だった」= 2
- `mealType` / `mealDate`: スロット・日付の付け替え
- `components`: 内訳の差し替え（`null` でクリア → base 値ベースの計算に戻る）

```bash
curl -X PUT https://app.kalori.jp/api/v1/ext/meals/meal-uuid \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"portionRatio": 0.5}'
```

## PUT /budgets/slot + PUT /budgets/day — 予算の編集 (Premium 限定)
アプリの計画グリッドの「枠」を書きます。書いた値は `/nutrition` 単日の `slots`（source=custom）と `goal` に即反映されるので、**「予算を設定 → slots を確認 → `/search` で枠に合う食事を探す」のループが閉じます**。

### スロット予算（「明日の夜は会食だから夕食に800kcal残したい」）
- PUT `/budgets/slot`: `{"mealDate": "2026-07-05", "mealSlot": "dinner", "cal": 800, "protein": 45, "fat": 25, "carbs": 100}`（mealDate 省略時は今日。同じ日/スロットへの再設定は上書き）
- DELETE `/budgets/slot/:date/:slot` で解除（自動配分に戻る）

### 日別オーバーライド（「土曜はチートデイだから2500kcalで」）
- PUT `/budgets/day`: `{"date": "2026-07-06", "cal": 2500}`（base の目標は変えずにその日だけ上書き。指定しない軸は base のまま、`null` を送ると個別クリア）
- DELETE `/budgets/day/:date` で全解除

```bash
curl -X PUT https://app.kalori.jp/api/v1/ext/budgets/slot \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"mealDate": "2026-07-05", "mealSlot": "dinner", "cal": 800, "protein": 45, "fat": 25, "carbs": 100}'
```

> 毎日の既定値そのもの（base 目標）を変えるのは下の `PUT /goals` です。**その日限りの数値指定はこちら（`/budgets/day`）を使ってください。**

## PUT /goals — 目標の変更 (Premium 限定)
「減量に切り替えたい」「目標体重を60kgに」「もっとゆっくりで」「朝を軽くして夜を多めに」を書き込みます。**指定した項目だけが変わり、残りは今の目標のまま**です。

**この目標が管理している量は `pace`**（週あたり体重の何%を動かすか）です。目標体重は「どこまで行くか」＝到達見込みを出すためのもので、**変えても 1 日の目標カロリーは動きません**。「速すぎる」「もっと早く」はペースを変えてください。

| フィールド | 型 | 説明 |
|---|---|---|
| `goalType` | `"lose"` / `"maintain"` / `"gain"` | 減量 / 維持 / 増量 |
| `targetWeightKg` | number / null | 目標体重。`null` で「設定なし」に戻す（到達見込みだけに効く） |
| `pace` | `"gentle"` / `"standard"` / `"firm"` | 変えていくペース。週 0.25% / 0.5%（推奨） / 0.75% |
| `durationDays` | number / null | ⚠️ legacy。期限で言われたときだけ（14〜1080日。3ヶ月=90）。送ると期限から逆算したペースで `pace` が上書きされます |
| `activityLevel` | 1〜5 | 1=ほぼ運動しない 〜 5=非常に活発 |
| `breakfastRatio` / `lunchRatio` / `snackRatio` / `dinnerRatio` | number | 食事スロットの配分比。**変えるときは4つとも指定し、合計を 1.0 にする** |

### ⚠️ 目標カロリー・PFC の数値は指定できません
サーバがその人の**観測代謝**（`/me` の `metabolism`）から自動で導出します。記録が溜まるほど目標が実態へ追従する仕組みで、外から数値を固定するとその追従が止まるため、意図的に受け付けていません。**その日だけ数値を決めたい場合は `PUT /budgets/day`** を使ってください。

応答には変更前 (`previous`) と根拠 (`derivation`) が入るので、「1,900 → 1,750 kcal になりました。観測代謝 2,150 kcal に対して 1 日 400 kcal の赤字です」と説明できます。

```bash
curl -X PUT https://app.kalori.jp/api/v1/ext/goals \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"goalType": "lose", "targetWeightKg": 60, "durationDays": 90}'
```

```json
{
  "data": {
    "goal": { "cal": 1750, "protein": 105, "fat": 52, "carbs": 210, "goalType": "lose",
              "activityLevel": 2, "targetWeightKg": 60, "durationDays": 90,
              "mealRatios": { "breakfast": 0.25, "lunch": 0.35, "snack": 0.10, "dinner": 0.30 },
              "source": "auto" },
    "previous": { "cal": 1900, "protein": 95, "fat": 58, "carbs": 240, "source": "auto" },
    "derivation": { "baseTdeeKcal": 2150, "baseTdeeSource": "observed",
                    "dailyDeficitKcal": -400, "clamped": false, "remainingDays": 90 },
    "switchedFromManual": false
  }
}
```

- `baseTdeeSource`: `observed` = 本人の記録から逆算した実測代謝 / `mifflin` = 身長体重年齢からの推定式（記録が足りないとき）
- `remainingDays`: 目標体重までの到達見込み日数。**実際の目標カロリー（安全クランプ適用後）**から計算するので、`clamped: true` のときは選んだペースの名目日数より長い。0 = 目標体重なし / 到達済み / いまの予算では近づかない — 0 のときは日数を案内しないこと
- `manualPreserved: true` … 目標がアプリで手動固定されているため、**指定した意図（目標体重・配分など）は保存しましたが数値（cal/PFC）は固定値のまま**です。固定の解除はアプリの目標画面「自動計算に戻す」からのみ。derivation は自動計算に戻した場合のプレビュー（`switchedFromManual` は旧互換フィールドで常に false）
- 400 `reason: "no_active_goal"` … 目標が未設定。アプリでの初期設定を案内する
- 400 `reason: "profile_incomplete"` … 身長・体重・生年月日・性別のどれかが未入力（`missing` に不足項目）。カロリー計算に必要なのでアプリでの入力を案内する

## GET /weights + POST /weights
体重の取得と記録。**Premium / 外部連携お試し中のみ利用可能**。

- GET: 単日 `?date=` または範囲 `?startDate=&endDate=` (最大 90日)。どちらも省略すると JST 当日
- POST: `{"weightKg": 65.2, "bodyFatPct": 23.4, "date": "2026-07-03", "note": "起床後"}`（date 省略時は JST 当日。1日1件で同日は上書き。bodyFatPct は任意 — 省略時は既存値を維持）

```bash
curl -X POST https://app.kalori.jp/api/v1/ext/weights \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"weightKg": 65.2}'
```

## GET /nutrition
単日: `?date=YYYY-MM-DD`、範囲: `?startDate=&endDate=`。**目標+消費+残量+運動消費** を返します (食事一覧は `/meals` を使う)。レスポンス形は両方とも `{days[]}` で統一。単日リクエストのみ `streak` を含みます。

```bash
# 単日
curl "https://app.kalori.jp/api/v1/ext/nutrition?date=2026-05-07" \
  -H "Authorization: Bearer ext_xxxxxx"

# 範囲 (週次/月次傾向分析向け、最大 90日)
curl "https://app.kalori.jp/api/v1/ext/nutrition?startDate=2026-04-30&endDate=2026-05-07" \
  -H "Authorization: Bearer ext_xxxxxx"
```

単日レスポンス:
```json
{
  "data": {
    "startDate": "2026-05-07", "endDate": "2026-05-07",
    "timezone": "Asia/Tokyo",
    "streak": 12,
    "days": [
      { "date": "2026-05-07",
        "goal":      { "cal": 2200, "protein": 110, "fat": 70, "carbs": 275 },
        "consumed":  { "cal": 1850, "protein": 95,  "fat": 62, "carbs": 220 },
        "remaining": { "cal": 350,  "protein": 15,  "fat": 8,  "carbs": 55 },
        "exerciseBurn": 0 }
    ]
  }
}
```

`goal` は当日の運動消費 (`exerciseBurn`) を加算した有効値。`goal: null` ならユーザー未設定。範囲時は `streak` 非含有。

### slots — 食事スロット別の予算（単日のみ）
単日レスポンスには `slots`（7スロット別の予算配分）が付きます。**アプリの計画グリッドと同じ配分計算**（ユーザーの食事配分比 × 当日有効目標、記録済みスロットは実績で置換）。

```json
"slots": [
  { "slot": "breakfast", "source": "meal", "cal": 248, "protein": 6.3, "fat": 11.9, "carbs": 29.1 },
  { "slot": "lunch",     "source": "auto", "cal": 630, "protein": 38,  "fat": 16.8, "carbs": 93.3 },
  { "slot": "dinner",    "source": "auto", "cal": 720, "protein": 43.5, "fat": 19.2, "carbs": 106.7 }
]
```

- `source`: `meal`=記録済み実績 / `custom`=手動予算 / `auto`=未記録スロットの自動配分 / `none`=配分なしの間食枠
- **「夕食に合うものを探して」の正規動線**: `slots` の dinner（source=auto）の値を `GET /search?targetCal=720&targetProtein=43.5&...` にそのまま渡す

## GET /nutrition/detailed — 27栄養素の詳細集計
食物繊維・塩分・鉄分・カルシウム・ビタミン群など27項目の期間集計と、性別・年齢に基づく推奨摂取量/上限（kcal/PFC は個人目標で上書き）。**iOS 統計タブと同じ計算** = 「鉄分足りてる？」「塩分摂りすぎ？」にアプリと同じ数字で答えられます。

- 単日 `?date=` / 範囲 `?startDate=&endDate=`（上限は /nutrition と同じ）。どちらも省略すると JST 当日
- `totals`（期間合計）+ `targets`（推奨量辞書）+ `mealCount`/`missingDetailedCount`（詳細栄養なし食事の母数 — 手動記録は集計から漏れる点に注意）
- `includeDaily=true` で日別内訳も返す（トークンが嵩むため既定は省略）

```bash
curl "https://app.kalori.jp/api/v1/ext/nutrition/detailed?startDate=2026-06-28&endDate=2026-07-04" \
  -H "Authorization: Bearer ext_xxxxxx"
```

## PUT /meals/:id/photo — 写真の添付 (Premium 限定)
記録済みの食事に写真を添付します。**画像はサーバ側で最大 500px の WebP（+200px サムネ）に正規化**されるため、送信側の圧縮品質を気にする必要はありません（入力上限 4MB・JPEG/PNG/WebP/HEIC）。添付した写真はアプリの食事カード・アルバムにそのまま表示されます。

- ボディ: `{"imageBase64": "<base64>"}`（data URI プレフィックスなしの生 base64）
- 同じ食事への再添付は上書き（旧画像は自動削除）
- AI 分析は行いません（栄養推定つきで登録したい場合は、チャットAI 自身が写真を見て `components` 付きで `POST /meals` するのが推奨フロー）

```bash
curl -X PUT https://app.kalori.jp/api/v1/ext/meals/meal-uuid/photo \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d "{\"imageBase64\": \"$(base64 -i photo.jpg)\"}"
```

> 注: MCP コネクタ（Claude/ChatGPT）はチャット添付画像のバイト列をツールに渡せないため、このエンドポイントは curl / カスタム連携向けです。

## GET /meals/search — 全履歴の食事名検索
`q`（食事名の部分一致）で**全期間**から検索（範囲取得の日数上限とは別枠）。「先週食べたあの豚汁いつ？」「いつもの朝食をもう一度記録して」用。結果は新しい順・各 item に `mealDate` と detail 相当のフィールド付き（`foodId` があれば POST /meals でそのまま再記録可）。

- `q` (必須・50字まで) / `limit` (1-50、既定20)

```bash
curl "https://app.kalori.jp/api/v1/ext/meals/search?q=豚汁" \
  -H "Authorization: Bearer ext_xxxxxx"
```

## GET /foods/mine — 自分のマイ食品
アプリで写真/文章分析したり手動登録した**自分の食品**の検索（他人の私有食品は見えません）。結果の `foodId` を `POST /meals` に渡すと、栄養・内訳・店舗が食品から自動付与されて再記録できます。

- `q` (任意・食品名の部分一致。省略時は新しい順の一覧) / `limit` (1-50、既定20)
- レスポンス item: `{foodId, name, cal, protein, fat, carbs, source, hasPhoto, componentsCount, createdAt}`

```bash
curl "https://app.kalori.jp/api/v1/ext/foods/mine?q=スムージー" \
  -H "Authorization: Bearer ext_xxxxxx"
```

## GET /exercises?date=YYYY-MM-DD

date 省略時は JST 当日。
指定日 (JST) の運動記録一覧 + 合計消費カロリー。

```bash
curl "https://app.kalori.jp/api/v1/ext/exercises?date=2026-05-07" \
  -H "Authorization: Bearer ext_xxxxxx"
```

```json
{
  "data": {
    "date": "2026-05-07",
    "timezone": "Asia/Tokyo",
    "totalBurn": 250,
    "exercises": [
      { "id": "abc-123", "mealSlot": "afternoon",
        "type": "running", "cal": 250, "durationMin": 30, "notes": null,
        "createdAt": "2026-05-07T15:30:00Z" }
    ]
  }
}
```

`mealSlot` は食間 4 枠のみ (`early_morning` / `mid_morning` / `afternoon` / `late_night`)。breakfast/lunch/dinner の食事スロットには紐付かない設計。
`type`: `walking` / `running` / `gym` / `cycling` / `swimming` / `custom` / null。

`/nutrition?date=` の `exerciseBurn` と `totalBurn` は同じ値。内訳まで欲しい時だけ `/exercises` を叩いてください。

## POST /exercises (Premium 限定)
運動の手動記録。「30分走った、300kcal くらい」をそのまま記録できます。

- `cal` (必須): 消費カロリー kcal (0〜5000 の整数)
- `exerciseType`: `walking` / `running` / `gym` / `cycling` / `swimming` / `yoga` / `sports` / `custom` (任意)
- `durationMin`: 運動時間 分 (任意)
- `exerciseDate`: YYYY-MM-DD (任意、省略時は JST 当日)
- `mealSlot`: 食間スロット (任意、省略時は現在時刻から自動推定)
- `notes`: メモ (任意、500字まで)

```bash
curl -X POST https://app.kalori.jp/api/v1/ext/exercises \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"cal": 300, "exerciseType": "running", "durationMin": 30}'
```

記録した消費カロリーは当日の `/nutrition` の `goal`（有効目標）に自動反映されます。
HealthKit 同期の行とは独立（この API が作るのは `source: "manual"` の行のみ）。

## DELETE /exercises/:id (Premium 限定)
誤記録の取消し。`source='manual'` の行のみ削除可（HealthKit 由来は保護）。id は `POST /exercises` のレスポンスか `GET /exercises?date=` から。

## GET /favorites + POST /favorites + DELETE /favorites/:type/:id
お気に入りの取得・追加・削除。**Premium / 外部連携お試し中のみ利用可能**。上限はアプリと共通の 200 件。

- GET: 栄養値つき一覧。`items[].type` = `product` / `dish` / `food`（food はアプリで作ったユーザー食品 = 表示のみ）。**「お気に入りから今日の夕食を選んで」の材料に使い、選んだ item の productId / dishId をそのまま `POST /meals` へ**
- POST: `{"productId": 12345}` または `{"dishId": 678}`（`/search` の結果 id）
- DELETE: `/favorites/product/12345` または `/favorites/dish/678`

```bash
curl -X POST https://app.kalori.jp/api/v1/ext/favorites \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"productId": 12345}'
```

## PUT /ratings + DELETE /ratings/:mealId — 食事の味評価
記録済みの食事（meal）に**自分専用**の味評価を付けます。9段階（1=いまいち 〜 9=最高）、再評価は上書き。アプリの気づきタブ「ベスト食事」に反映されます。**Premium / 外部連携お試し中のみ利用可能**。

- PUT: `{"mealId": "meal-uuid", "rating": 7, "note": "スープが好み"}`（mealId は `GET /meals` / `POST /meals` の結果から）
- DELETE: `/ratings/{mealId}` で取り消し

```bash
curl -X PUT https://app.kalori.jp/api/v1/ext/ratings \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"mealId": "meal-uuid", "rating": 7}'
```

## GET /shops
Kalori が栄養データを保有するチェーン店の一覧。コンビニ・牛丼・カフェ等。

```json
{
  "data": {
    "count": 60,
    "shops": [
      { "id": "lawson", "name": "ローソン", "nameEn": "Lawson",
        "type": "RETAIL_STORE", "category": "convenience",
        "websiteUrl": "https://www.lawson.co.jp",
        "aliases": ["ローソン", "lawson"] }
    ]
  }
}
```

`type`: `RETAIL_STORE` (コンビニ等) / `RESTAURANT` (外食チェーン) / `FOOD_MANUFACTURER` (食品メーカー)
`category`: `convenience`, `gyudon`, `burger`, `cafe`, `ramen`, `sushi` 等の業態タグ

## POST /sets (Premium 限定)
**指定した店の中で**、残りPFC に合うセット提案 (main + side + 必要なら追加 1〜2 品)。

リクエストボディ:
- `target`: `{cal, protein, fat, carbs}` (必須、すべて非負数)
- `shopId`: **必須**。店舗 id (例: "lawson")。分からなければ先に `GET /shops` で取得してください
- `mealType`: 食間/朝のヒューリスティクス (任意)
- `maxItems`: 1セットの最大品数 (1〜5、デフォルト 3)
- `minItems`: 1セットの最小品数 (1〜5、デフォルト 2)
- `setLimit`: 返すセット数 (1〜5、デフォルト 3)

レスポンスの `seed` を控えておくと、同じセットを後から再現できます（不具合報告に添えてください）。

```bash
curl -X POST https://app.kalori.jp/api/v1/ext/sets \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"target": {"cal": 700, "protein": 35, "fat": 20, "carbs": 80}, "shopId": "lawson"}'
```

レスポンス:
```json
{
  "data": {
    "sets": [
      {
        "shopId": "lawson", "shopName": "ローソン", "score": 87,
        "totals": { "cal": 685, "protein": 36, "fat": 18, "carbs": 82 },
        "items": [
          { "id": 12345, "name": "サラダチキン", "cal": 113, "protein": 24, ... },
          { "id": 23456, "name": "玄米おにぎり", "cal": 200, "protein": 4, ... }
        ]
      }
    ],
    "target": { "cal": 700, "protein": 35, "fat": 20, "carbs": 80 },
    "candidatesEvaluated": 1234
  }
}
```

## GET /search — 統合カタログ検索
チェーン商品（約23,000点）と一般的な料理名（ラーメン・親子丼等 約4,000点）を横断して 1 クエリで検索します。**iOS アプリと同一の検索エンジン**（かな/ローマ字/同義語対応・人気度順・意味検索フォールバック・除外ルール）を通るため、アプリで出る結果と同じものが返ります。Web 公開データなのでユーザー固有情報なし。

クエリ (q / shop / genre / レンジ / target* / sort の少なくとも1つは必須):
- `q`: 検索語（商品名・店名・料理名。日本語推奨。かな・ローマ字でも可）
- `shop`: ショップID (カンマ区切り可) — `/shops` の `id`
- `genre`: ジャンルID (カンマ区切り可)
- `calMin` / `calMax` / `proteinMin` / `proteinMax` / `fatMin` / `fatMax` / `carbsMin` / `carbsMax`: 数値のハードリミット（例:「糖質20g以下でタンパク30g以上」= `carbsMax=20&proteinMin=30`）
- `targetCal` / `targetProtein` / `targetFat` / `targetCarbs`: **PFCターゲット検索**。「残り予算に合う」順で返す（1日分は `/nutrition` の `remaining`、**食事1回分は `/nutrition` 単日の `slots` の該当スロット**をそのまま渡すのが定石）。マッチ粒度は全プラン 2g バケット（旧 Free 5g の tier 差は 2026-07-26 撤廃）
- `mealType`: 食事時間帯ヒント（朝食 ≤600kcal / 間食 ≤250kcal に絞る）
- `sort`: 並び順（既定 `score`=関連度）。`fits-budget` / `protein-desc` / `cal-asc` / `cal-desc` / `price-asc` / `popularity-desc` / `date`、テーマランキング `score-protein` / `score-low-cal` / `score-low-fat` / `score-balance` / `score-night` / `score-morning` 等
- `include`: `all`（既定 = 商品+料理名） / `products` / `dishes`
- `limit`: 1〜50 (デフォルト 20) / `offset`: ページネーション

```bash
# ローソンで 500kcal 以下のサラダチキン系
curl "https://app.kalori.jp/api/v1/ext/search?q=サラダチキン&calMax=500&shop=lawson&limit=10" \
  -H "Authorization: Bearer ext_xxxxxx"

# 残り予算 (500kcal / P30g) に合うものを全店舗+料理名から提案
curl "https://app.kalori.jp/api/v1/ext/search?targetCal=500&targetProtein=30" \
  -H "Authorization: Bearer ext_xxxxxx"

# 高タンパクランキング
curl "https://app.kalori.jp/api/v1/ext/search?sort=score-protein&limit=10" \
  -H "Authorization: Bearer ext_xxxxxx"
```

レスポンスの `items[]` は `type` で2種類。**type=product は `productId` を、type=dish は `dishId` を `POST /meals` に渡す**:
```json
{
  "data": {
    "total": 23, "returned": 10, "limit": 10, "offset": 0, "hasMore": true,
    "items": [
      { "type": "product", "productId": 12345, "name": "サラダチキン プレーン",
        "shopId": "lawson", "shopName": "ローソン",
        "cal": 113, "protein": 24.3, "fat": 1.5, "carbs": 0.4,
        "price": 268, "category": "high-protein", "productType": "main",
        "pageUrl": "https://www.lawson.co.jp/recommend/...",
        "imageUrl": "https://assets.kalori.jp/products/lawson/generated/g/98765_400.webp",
        "score": 95 },
      { "type": "dish", "dishId": 678, "name": "醤油ラーメン", "category": "麺類",
        "cal": 470, "protein": 21, "fat": 8, "carbs": 65,
        "imageUrl": "https://assets.kalori.jp/generic-menus/generated/g/4321_400.webp",
        "score": 88 }
    ]
  }
}
```

`imageUrl` は商品/料理のイメージ画像（AI生成・公開CDN・認証不要、未生成は null）。提案を提示するときに
リンクとして添えると、ユーザーがタップして見た目を確認できます（`POST /sets` の items にも同フィールドあり）。

商品の Web 詳細ページ: `https://kalori.jp/ja/shops/{shopId}/products/{productId}/`

---

## POST /search — 複数語の一括検索（献立をまとめて調べる）

**献立や買い物リストのように「複数の品を一度に調べる」ときは、`GET /search` を並べずにこちらを使ってください。**
1 リクエストで最大 10 語（無料プランは 3 語）を検索し、**クォータの消費は 1 回分**です。

⚠️ **複数の料理を 1 本の `q` に詰めないでください**（`q=回鍋肉 酢豚 麻婆豆腐` のような形）。
検索エンジンは 1 つの料理名として解釈するため、**個別に引けば全部あるのに 0 件で返ります**。
料理が複数あるなら `queries` に 1 語ずつ分けて渡すのが正しい形です。

- `queries`: 検索語の配列（1〜10 語 / 無料プランは 3 語まで。超えると 400 = 黙って切り捨てない）
- `limitPerQuery`: 1 語あたりの件数（1〜10・既定 5）
- フィルタ（`shop` / `genre` / レンジ / `target*` / `mealType` / `include` / `sort`）は `GET /search` と同じで、**全語に共通で掛かります**

```bash
curl -X POST "https://app.kalori.jp/api/v1/ext/search" \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"queries":["回鍋肉","酢豚","麻婆豆腐","青椒肉絲"],"limitPerQuery":3}'
```

```json
{
  "data": {
    "limitPerQuery": 3,
    "notFound": ["青椒肉絲"],
    "results": [
      { "query": "回鍋肉", "total": 4, "returned": 3, "items": [ /* GET /search と同じ item 形 */ ] },
      { "query": "酢豚", "total": 4, "returned": 3, "items": [ ... ] }
    ]
  }
}
```

`notFound` は 1 件も当たらなかった語です。**ここに載った品は推測で数値を作らず**、ユーザーに
「カタログに無いので手入力で記録しますか」と聞くか、`POST /analyze-text` で推定してください。

---

## GET /catalog/items — カタログ id の一括 hydrate

`GET /search` の結果 id（`p_{productId}` / `g_{dishId}`）をカンマ区切りで渡すと、`/search` と同形の item を**入力順のまま**返します（最大10件）。会話の途中で持ち回った数値を確定値に取り直したいときや、献立を組んで合計を出すときに使ってください。

```bash
curl -H "Authorization: Bearer ext_..." \
  "https://app.kalori.jp/api/v1/ext/catalog/items?ids=p_12345,g_42"
```

見つからない id（販売終了・非公開・存在しない）は `data.notFound` に入ります。**Free でも使えます**（カタログは公共財）。

---

## POST /billing/portal — 契約の管理（解約・支払い方法・領収書）

ユーザーが「解約したい」「支払い方法を変えたい」「領収書が欲しい」と言ったら、この
エンドポイントで管理ページの URL を発行して案内してください。**Free でも使え、クオータも
消費しません**（解約したい人が Premium 切れで解約できない、を作らないため）。

- 対象は **Stripe で購入した Premium**（web またはチャット経由の直販）だけです。
- **iOS の App Store で購入した Premium は 409** が返ります — その場合は
  「設定 → Apple ID → サブスクリプション」から解約するようユーザーへ案内してください
  （販売者が Apple なので、kalori 側からは操作できません）。
- 契約が無い場合も 409 です。

```bash
curl -s -X POST https://app.kalori.jp/api/v1/ext/billing/portal \
  -H "Authorization: Bearer $TOKEN"
# → { "success": true, "data": { "url": "https://billing.stripe.com/..." } }
```

URL を発行したら**そのままユーザーに渡してください**（決済・解約の操作は本人がブラウザで
行います。AI が代行することはできません）。

## POST /feedback — 開発チームへのフィードバック

チャットからの kalori 連携は**開発中の機能**で、意見を募集しています。ユーザーが感想・要望・
不具合を口にしたら、このエンドポイントで送ってください。**Free でも使え、クオータも消費しません**
（枠を使い切った状態でも送れます）。上限は24時間に 5 件。

記録系レスポンス（`POST /meals` / `POST /analyze-text`）には、まだ一度も声を送っていない
ユーザーに限り `notice` フィールドが付きます。中身は「開発中である旨（ユーザーに伝える本文）」と
`instruction`（扱い方）です。**会話の中で一度だけ**添えてください。

```bash
curl -X POST "https://app.kalori.jp/api/v1/ext/feedback" \
  -H "Authorization: Bearer ext_xxxxxx" -H "Content-Type: application/json" \
  -d '{"message": "コンビニの新商品がまだ出てこないことがある", "category": "feature"}'
```

- `message`: 必須 (1〜2000文字)。**送信前に本文をユーザーに読み上げて同意を取ること**。
  ユーザーの言葉をそのまま活かし、要約しすぎない
- `category`: 任意。`feature` (機能要望) / `bug` (不具合) / `usability` (使い勝手) /
  `question` (質問) / `impression` (感謝・応援) / `other`

送信先はアプリと同じ問い合わせスレッドです。**運営からの返信は kalori アプリの
マイページ→お問い合わせに届きます**（このチャットには返りません）— ユーザーにそう伝えてください。

## 値の単位
- カロリー: kcal
- タンパク質 / 脂質 / 炭水化物 (P/F/C): g
- 価格: 円 (税込)

## レート制限
- IP 単位: 60 req/min (共通)
- Free ユーザー: 10 calls/day (カタログ無料枠、user 単位、JST 0時リセット)
- Premium ユーザー: 100 calls/hour (user 単位、UTC 毎時リセット)

## エラー
- 401: トークン無効・失効・期限切れ
- 400: リクエストパラメータ不正
- 403: Premium 限定機能を Free で叩いた
- 429: レート制限 / 日次クオータ超過
- 500: サーバーエラー

## エラー応答の追加情報
- 429 のレスポンスボディには `quota: { used, limit, remaining, period, resetAt }` が含まれます
- 403 のレスポンスボディには `upgrade: { feature, requiredTier }` が含まれます（**Premium の壁による 403 だけ**。地域制限など壁以外の 403 には入りません）
- 失敗の理由は `code` で機械的に判別できます（**文言 `error` で分岐しないでください** — 文言は改善のたびに変わります）。主な値: `premium_required`（上位プランなら通る・`upgrade` と対）/ `forbidden`（払っても通らない）/ `quota_exceeded`（枠切れ・`quota` と対）/ `rate_limited`（待てば通る）/ `insufficient_credits` / `invalid_request` / `not_found` / `unavailable`。⚠️ **知らない `code` は「原因不明の失敗」として扱ってください**（値は今後増えます）
- 404 のメッセージには次に試すべきアクション (例: `Use GET /meals?date=...`) のヒントが含まれます
- `userMessage` が入っている応答は、その文言を**そのままユーザーに見せてよい**日本語です
  (`error` はログ集計用の短い識別子)。両方あるときは `userMessage` を優先してください

## 推奨ワークフロー (AI向け)
1. 最初に `/me` で tier / quota / 目標（PFC・体重目標・スロット配分比）を確認
2. ユーザーの質問に応じて並列で必要データを取得 (`/meals` + `/nutrition` 等)
3. 「次の一品」は `/nutrition` の `remaining`（1日分）か `slots` の該当スロット（食事1回分）を `/search?targetCal=…&targetProtein=…` に渡して探す
4. 微量栄養素の相談（鉄分/塩分/食物繊維など）は `/nutrition/detailed` で
5. 食事計画の相談は「枠 → 中身」の2段: `/budgets` で予算を組み → `/search` で枠に合う候補を探し → 未来日の `POST /meals` で計画として投入
6. 「何を食べたか・残り何が必要か・次の一品の提案」を簡潔な日本語で
7. ユーザーが感想・要望・不具合を口にしたら `POST /feedback`（この連携は開発中で意見募集中。同意を取ってから送る）

## 初回接続時のふるまい（AI向けオンボーディング台本）
接続直後に `/me` を呼び、**`goal` が `null` なら「接続したてでプロフィール未設定のユーザー」**です。次の順で進めてください:

1. **いきなり目標設定を迫らない。** まず「今日食べたものを教えてください。まとめて記録します」から入る（記録の成功体験が最優先）
2. 聞き取った食事を `/search` で探して `POST /meals` へ（見つからなければ内訳つき手動記録）
3. 記録できたら今日のサマリー（合計 kcal・PFC）を返す — これが最初の成功体験
4. そのあとで一言だけ案内する:
   「目標（1日のカロリー・PFC）を設定すると、残り予算に合わせた提案ができるようになります。kalori アプリなら3分で設定できます → https://apps.apple.com/jp/app/id6761054819」
5. 目標が未設定の間も、検索・記録・体重・お気に入り・味評価はすべて使えます。`/nutrition` の `goal` は `null` になるだけで、摂取合計は正しく返ります

目標が設定済みのユーザーには通常どおり「残り予算・slots」を軸に会話してください。

短い質問にはダラダラ答えず、データに基づいた具体的な提案を返してください。
