# Jev の REST API の使い方とエラーコード

Jev は POST /v1/systemone の1本で呼べる。curl と fetch での呼び出し、レスポンスの読み方、401・422・429・529 と実際に返ってきた 400 の扱い、再試行の書き方をまとめる。

- 正規URL: https://jevguide.jp/guides/rest-api/
- 公開: 2026-09-21T00:00:00.000Z
- 更新: 2026-09-21T00:00:00.000Z
- 最終確認: 2026-09-21T00:00:00.000Z
- 検証環境: jev-1.13.0（2026-09-21 に Node 26 の fetch で実行。正常系と 400 / 401 / 422 を確認）

Jev の判定 API は **`POST https://api.typesafe.ai/v1/systemone` の1本**です。`state`（材料）、`model`、`questions`（質問のマップ）を JSON で送ると、質問と同じキーで答えが返ります。SDK を使わなくても、`curl` や `fetch` だけで呼べます。

## 最小の呼び出し

API キーは環境変数に入れます。コードやコマンド履歴に直接書かないでください。

```bash
curl https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Help! My payouts have been failing for 3 days.",
    "model": "jev-latest",
    "questions": {
      "is_urgent": { "type": "noul", "instructions": "Does this convey urgency?" }
    }
  }'
```

```json
{
  "model": "jev-1.13.0",
  "answers": { "is_urgent": { "type": "noul", "noul": 0.95 } },
  "usage": { "input_tokens": 283, "output_tokens": 23 }
}
```

（2026-09-21 に実際に返ってきた値です。`usage` の数字は公式ドキュメントの例と少し違いました。）

## リクエスト

| 項目 | 型 | 説明 |
| --- | --- | --- |
| `state` | 文字列 / オブジェクト / 配列 | 判定の材料。→ [state の設計](/guides/designing-state/) |
| `model` | 文字列 | `jev-latest` か、`jev-1.13.0` のようなバージョン ID。→ [料金と制限](/guides/pricing-and-limits/) |
| `questions` | マップ | キーは自分で付ける名前。答えは同じキーで返る。キーはモデルには渡らず、判定に影響しない |

質問の書き方は [3つの質問タイプの書き方](/guides/question-types/) にまとめました。

## レスポンス

| 項目 | 説明 |
| --- | --- |
| `model` | 実際に答えたバージョン ID。エイリアスを指定しても、ここには `jev-1.13.0` のような実体が入る。**ログに残す** |
| `answers` | 質問と同じキーの答え。型ごとの形は下の表 |
| `usage` | `input_tokens` と `output_tokens`。課金は入力だけ |

| 型 | 答えの項目 |
| --- | --- |
| `noul` | `noul`（0〜1） |
| `choice` | `choice`、`probabilities`（合計 1）、`confidence` |
| `score` | `score`、`legend`、`probabilities`（合計 1）、`confidence` |

## Node.js（fetch）での例

3種類の質問を一度に送る例です。手元で実行し、約 0.7 秒で返りました（1回の計測）。

```js
const res = await fetch('https://api.typesafe.ai/v1/systemone', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    state: { message: '助けてください！3日前から入金がずっと失敗しています。', plan: 'business' },
    model: 'jev-latest',
    questions: {
      is_urgent: { type: 'noul', instructions: 'Does `message` convey urgency?' },
      department: {
        type: 'choice',
        instructions: 'Which team should handle `message`?',
        criteria: {
          billing: 'Payments, invoicing, refunds',
          technical: 'Bugs, outages, integrations',
          sales: 'Pricing, upgrades, new accounts',
        },
      },
      frustration: {
        type: 'score',
        instructions: 'How frustrated is the customer?',
        criteria: ['Calm', 'Frustrated', 'Very angry'],
      },
    },
  }),
});

if (!res.ok) throw new Error(`Jev API ${res.status}: ${await res.text()}`);
const { model, answers } = await res.json();

if (answers.is_urgent.noul > 0.8 && answers.department.confidence > 0.7) {
  console.log(`${answers.department.choice} の優先キューへ（${model}）`);
}
```

## エラー

公式の表にあるのは次の4つです。

| ステータス | 意味 | 対処 |
| --- | --- | --- |
| `401 Unauthorized` | API キーが無い・誤っている | `Authorization` ヘッダを確かめる。再試行しない |
| `422 Unprocessable Entity` | リクエストの形が不正 | 本文に問題の箇所が出る。直して送り直す。再試行しない |
| `429 Too Many Requests` | レート制限の超過 | 間隔を伸ばしながら再試行 |
| `529 Overloaded` | サーバー側の過負荷 | 間隔を伸ばしながら再試行 |

### 実際に返ってきたエラーの形（2026-09-21 に確認）

**401** — 誤ったキーで呼んだ場合:

```json
{ "detail": { "error_type": "authentication_error",
              "message": "Cannot authenticate with the server. Please check your API key and try again." } }
```

**422** — Choice に `criteria` を付け忘れた場合。`loc` が問題の場所を指します。

```json
{ "detail": [ { "type": "missing",
                "loc": ["body", "questions", "q", "choice", "criteria"],
                "msg": "Field required" } ] }
```

**400** — 存在しないモデル名を指定した場合。**公式のエラー表には無いステータス**ですが、実際にはこう返りました。

```json
{ "detail": { "error_type": "api_usage_error", "message": "Unknown model: jev-does-not-exist" } }
```

`detail` は、401 と 400 ではオブジェクト、422 では配列です。エラー処理で `detail.message` を決め打ちで読むと 422 で壊れるので、形を見てから読みます。

### 通ってしまう入力に注意

公式は「Score の段階は少なくとも2つ」としていますが、**段階が1つだけの Score を送っても 422 にはならず、`score: 0`、`confidence: 1` が 200 で返りました。** 配列を組み立てる処理の不具合で段階が1つになっても、API は止めてくれません。送る前に自分のコードで確かめてください。

## 再試行の書き方

再試行するのは `429` と `529`（と、ネットワークの一時的な失敗）だけです。`400` / `401` / `422` は何度送っても同じなので、すぐに失敗させます。

```js
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 0.5s, 1s, 2s, 4s … に揺らぎを足す
const backoff = (attempt) => 500 * 2 ** attempt + Math.random() * 250;

// Retry-After は「秒数」か「HTTP の日付」のどちらかで返る
function retryAfterMs(value) {
  if (!value) return null;
  const seconds = Number(value);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
  const date = Date.parse(value);
  return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
}

async function askJev(body, { retries = 4 } = {}) {
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await fetch('https://api.typesafe.ai/v1/systemone', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
      });
    } catch (err) {
      // 接続できない、途中で切れた、などの一時的な失敗
      if (attempt >= retries) throw err;
      await sleep(backoff(attempt));
      continue;
    }
    if (res.ok) return res.json();

    const retryable = res.status === 429 || res.status === 529;
    if (!retryable || attempt >= retries) {
      throw new Error(`Jev API ${res.status}: ${await res.text()}`);
    }
    await sleep(retryAfterMs(res.headers.get('retry-after')) ?? backoff(attempt));
  }
}
```

通信の失敗で再試行すると、1回目のリクエストが実はサーバーに届いていた場合、同じ判定を2回行うことになります。Jev の判定は何かを書き換える操作ではないので結果に害はありませんが、入力トークンは2回ぶん数えられる可能性があります。

公式 SDK（Python の `typesafe-sdk`、JS の `@typesafe-ai/sdk`）は、この再試行を既定で行い、`retry-after` ヘッダがあれば従う、と書かれています。自前で書く理由が無ければ SDK を使うほうが簡単です。

このコードで確かめたこと（2026-09-21）:

- 実際の API に対して: 正常に答えが返ること、`401` では再試行せずすぐに例外になること
- `fetch` を差し替えた模擬で: 「通信の失敗 → `429`（`Retry-After` が秒数）→ `529`（`Retry-After` が日付）→ `200`」の順に進んで答えが返ること、再試行の上限で諦めること、通信の失敗が続いたら元の例外を投げること、`Retry-After` が秒数・日付・過去の日付・壊れた値のそれぞれで期待どおりの待ち時間になること

**実際の API に `429` や `529` を起こさせることはしていません。** 本物の `Retry-After` がどちらの形式で返るかは未確認です。

## 使えるモデルの一覧

```bash
curl https://api.typesafe.ai/v1/models -H "Authorization: Bearer $TYPESAFE_API_KEY"
```

現在はエイリアス（`jev-latest`、`jev-preview`）が返ります。バージョン ID は一覧に無くても `model` に指定できます。

## 日本語で使うとき

`state` も `instructions` も日本語で送れますが、公式は英語が最も精度が高く、日本語を含む CJK は同等ではないと明記しています。選択肢のキー（`billing` など）はコードで分岐に使うので、英数字にしておくと扱いやすくなります。

## よくある質問

### エンドポイントはいくつありますか？

判定は POST https://api.typesafe.ai/v1/systemone の1本です。ほかに、使えるモデル名を返す GET https://api.typesafe.ai/v1/models があります。

### 429 や 529 が返ったらどうすればよいですか？

すぐに再送せず、待ち時間を指数的に伸ばしながら再試行します。429 はレート制限の超過、529 はサーバー側の過負荷です。公式 SDK は既定でこの再試行を行います。REST を直接呼ぶ場合は自分で実装します。

### ストリーミングはありますか？

公式の API リファレンスにストリーミングの記述はありません。Jev は文章を生成しないので、レスポンスは質問ごとの答えが入った JSON が1つ返るだけです。

## 出典

- [API reference](https://docs.typesafe.ai/api)
- [Models](https://docs.typesafe.ai/models)

---
typesafe.ai とは無関係の非公式ガイドです。
