# kalori Catalog API v1

外食・コンビニ商品の栄養データ（カロリー・たんぱく質・脂質・炭水化物）を参照する API です。
ベース URL: `https://catalog-api.kalori.jp/v1`

利用条件は [利用規約](https://console.kalori.jp/terms)、稼働の目安とサポート窓口は [サービスレベルの目安](https://console.kalori.jp/sla) にあります。

## クイックスタート

```
curl "https://catalog-api.kalori.jp/v1/search?q=%E3%81%8A%E3%81%AB%E3%81%8E%E3%82%8A&limit=5" \
  -H "Authorization: Bearer kdk_xxxxxxxx" \
  -H "X-Kalori-End-User: 5f1c0a9e2b7d4c3a"
```

- 鍵は [コンソール](https://console.kalori.jp/) で発行します（発行時に一度だけ表示）。
- `X-Kalori-End-User` は貴社サービスの**末端ユーザーごと**の値です（下記）。

## 認証

- `Authorization: Bearer kdk_…`（コンソールで発行した API 鍵）
- **`X-Kalori-End-User: <opaque>`** — データ系ルート（/shops, /search, /products/:id）で**必須**。
  貴社サービスの末端ユーザーごとに安定した識別子を、**貴社側で salt 付きハッシュ化**した値
  （`^[A-Za-z0-9_-]{8,64}$`）。メールアドレスや生の ID は送らないでください。
  kalori はこの値を逆引きせず、末端ユーザー単位の利用上限と月間アクティブ数（MAU）の計測にだけ使います。

### 末端ユーザー ID の作り方（例）

貴社だけが知る salt を鍵にした HMAC を、URL-safe な Base64 に変えて送ります。同じ利用者には常に同じ値になり、
値から利用者を逆算することはできません。

```
// Node.js
import { createHmac } from "node:crypto";
const endUser = createHmac("sha256", process.env.KALORI_END_USER_SALT)
  .update(String(userId))
  .digest("base64url");           // 43 文字・[A-Za-z0-9_-]
```

- **利用者ごとに別の値**にしてください。複数の利用者を 1 つの ID にまとめると、その ID が 1 日の上限に先に当たります。
  課金は呼び出し回数ではなく MAU なので、実際の人数をそのまま送るのが最も安くなります。
- ログインしていない利用者には、端末やセッション単位の安定した識別子をハッシュ化して使ってください。

## ルート

| ルート | 用途 |
|---|---|
| `GET /v1/me` | 契約状態・上限・本日の使用量・当月 MAU |
| `GET /v1/shops` | 掲載チェーン一覧 |
| `GET /v1/search?q=&shop=&limit=&offset=` | 商品検索（`q` または `shop` のどちらか必須） |
| `GET /v1/products/:id` | 商品 1 件 |

### /v1/search
- `q`: 商品名の検索語（100 文字まで）。`shop`: チェーン ID（/v1/shops の `id`）。
- `limit`: 1〜50（既定 20）。`offset`: 0〜200。
  これより深いページは取れません（全件取得のエンドポイントはありません）。
- 応答: `{ items: Product[], total: number, next_offset: number|null }`

### Product
```
{
  "id": 12345,
  "shop": { "id": "seven", "name": "セブン-イレブン" },
  "name": "商品名",
  "nutrition": { "calories": 512, "protein": 18.5, "fat": 12, "carbs": 70.2 },
  "source": "official",
  "url": "https://kalori.jp/ja/shops/seven/products/12345/"
}
```
- `nutrition` は **1 食あたり**。不明な項目は `null`。
- `source`: `official`（チェーン公表値）/ `operator`（事業者が kalori へ直接登録した公式値）/ `estimated`（kalori の推定値）/ `mixed`（項目ごとに混在）/ `null`。
- `url` は kalori.jp の商品ページです。**表示時は出典として kalori へのリンクを併記してください**（利用規約）。
  `estimated` / `mixed` の値を出すときは、推定値である旨も併せて表示してください。

## 上限と応答ヘッダ

すべての応答に `X-Quota-Used` / `X-Quota-Limit` / `X-Quota-Remaining` / `X-Quota-Reset` / `X-Quota-Scope`
（`developer_day` / `developer_hour` / `end_user_day`）が付きます。超過時は `429`。

- `X-Quota-Reset` は ISO 8601（UTC）の時刻です。**429 を受けたらこの時刻まで同じ窓の呼び出しを止めてください**
  （即時の再試行は上限を消費するだけで成功しません）。
- 日次の窓は日本時間の 0 時、時間の窓は毎正時（UTC）にリセットされます。
- 上限の値はご契約ごとに異なります。現在の値は `GET /v1/me` かコンソールで確認できます。

## データの保存（原則禁止）

- **データは表示のたびに API から取得してください。** 応答を貴社側に保存・蓄積すること（データベースへの取り込み・
  サーバ側の共有キャッシュ・CDN でのキャッシュを含む）は利用規約で禁止しています。
- 許されるのは、**同じ末端ユーザーの同じ画面を描くために必要な一時的な保持**（1 回の応答をその画面で使い回す程度）だけです。
- 末端ユーザーごとの 1 日の上限は、この前提（表示ごとに 1 回）で足りる値にしています。足りない場合は上限の引き上げを相談してください。

## エラー

`{ "error": { "code": "...", "message": "..." } }` + HTTP ステータス。

| code | HTTP | 意味 |
|---|---|---|
| `unauthorized` | 401 | `Authorization` ヘッダが無い |
| `invalid_format` | 401 | `Bearer kdk_…` の形式ではない |
| `unknown_key` | 401 | 鍵が存在しない |
| `key_revoked` | 401 | 失効した鍵（コンソールで再発行） |
| `key_expired` | 401 | 有効期限を過ぎた鍵 |
| `developer_suspended` | 403 | 契約が停止中 |
| `end_user_required` | 400 | `X-Kalori-End-User` が無い |
| `end_user_invalid` | 400 | `X-Kalori-End-User` が形式に合わない |
| `bad_request` | 400 | パラメータ不正（`q` / `shop` 欠落、`limit` / `offset` の範囲外など） |
| `unknown_shop` | 400 | `shop` が /v1/shops に無い |
| `not_found` | 404 | 商品が無い |
| `quota_exceeded` | 429 | 上限超過（`X-Quota-Scope` にどの窓かが出る） |

5xx はこちらの障害です。数秒おいて指数バックオフで再試行してください（再試行の上限は 3 回程度を推奨）。

## 版の運用

- パスの major（`/v1/`）だけが版です。
- **フィールドの追加は非破壊。削除・改名・意味変更・必須化は v2 として別パスで提供**し、v1 には
  `Deprecation` / `Sunset` ヘッダで期日を出します（廃止は最短でも 6 か月前に告知）。
- 変更履歴: [CHANGELOG](https://github.com/bananbo/kalori/blob/main/cloudflare/workers/developer-api/CHANGELOG.md)
