REST API リファレンス
Manabu2 の学習コンテンツと学習進捗を、外部システムから読み書きするための REST API です。コースやレッスンを自社サービスに取り込む用途と、学習アプリから進捗を記録する用途の両方に対応します。
バージョニング
公開 API はすべて /api/v1 配下にあります。互換性を壊す変更が必要になった場合は /api/v2 を新設し、v1 は移行期間を設けたうえで維持します。フィールドの追加は破壊的変更とはみなしませんので、未知のフィールドは無視できるようにクライアントを実装してください。
スキーマ(OpenAPI / Swagger)
全エンドポイントの定義は機械可読な OpenAPI 3 ドキュメントとして公開しています。型付きクライアントの自動生成、Postman などへの取り込み、スキーマの差分確認にそのまま使えます。
| OpenAPI ドキュメント | https://api.manabu2.com/openapi/v1.json |
| ブラウザで試す | https://api.manabu2.com/scalar/v1 |
各オペレーションには x-required-scope 拡張が付いており、必要なスコープをドキュメントから直接読み取れます。この値は実際に認可を行っている属性から生成しているため、実装とずれることはありません。
クライアント生成の例(openapi-generator の場合):
npx @openapitools/openapi-generator-cli generate -i https://api.manabu2.com/openapi/v1.json -g typescript-fetch -o ./src/manabu2
1. 認証
すべてのエンドポイントに認証が必要です。資格情報は 2 種類あり、用途によって使い分けます。
受講者トークン (JWT)
学習アプリなど、受講者本人として操作する場合に使用します。本人の進捗の読み書きができます。
Authorization: Bearer <jwt>組織 API キー
サーバー間連携で使用します。組織単位で発行し、付与されたスコープの範囲内でのみ動作します。特定の個人には紐づきません。
X-Api-Key: mnb_live_…Authorization: Bearer mnb_live_…受講者トークンの取得
POST https://api.manabu2.com/api/v1/auth/login
Content-Type: application/json
{
"email": "learner@example.com",
"password": "…",
"clientId": "manabu2-learner-app"
}
{
"accessToken": "eyJhbGciOi…",
"expiresAt": "2026-07-30T15:00:00Z",
"refreshToken": "3pQ7…",
"scopes": ["read:catalog", "read:content", "read:progress", "write:progress"],
"user": { "id": "…", "email": "learner@example.com", "displayName": "…" }
}
ログインすると 2 つのトークンが返ります。accessToken は各リクエストに付与する短命なトークンで、有効期間は 60 分です。refreshToken は有効期間 30 日で、accessToken が切れたときに新しい組を取得するために使います。accessToken は失効させることができない(署名を検証するだけ)ため、あえて短くしてあります。セッションを止める手段は refreshToken 側にあります。
clientId(アプリケーションの識別)
clientId は「どのアプリがサインインしているか」を示します。アプリごとに要求できるスコープの上限が登録されており、発行されるトークンはその上限を超えません。
- clientId を省略した場合は、学習者向けの 4 スコープ(read:catalog / read:content / read:progress / write:progress)のみが付与されます。省略することで管理系スコープを得ることはできません。
- 未登録または無効化された clientId を指定した場合、スコープは一切付与されません(アプリの停止が実際に機能するためです)。ログイン時には 400 を返します。
- リフレッシュ時も同じアプリの上限が再適用されます。トークンを更新することで権限が広がることはありません。
- 現時点で clientId を発行できるのは自社アプリのみです(設定で登録します)。第三者向けのアプリ登録は、OAuth 認可フローの提供時に対応予定です。
パスワードを預からないサインイン(OAuth)
PKCE 付きの認可コードフローです。アプリはユーザーを manabu2.com のサインイン画面へ送り、短時間だけ有効な認可コードを受け取って、それをトークンと交換します。パスワードは当社の画面で入力され、アプリを経由することはありません。
1. PKCE のペアを生成します。verifier はアプリ内に保持し、送信するのはその SHA-256 ハッシュだけです。そのため、リダイレクトを傍受しただけではコードを引き換えられません。
code_verifier = base64url(random 32–64 bytes) // keep this code_challenge = base64url(sha256(code_verifier)) // send this
2. この URL を OS 標準のブラウザーで開きます(アプリ内 WebView は使わないでください。WebView はパスワードを読み取れてしまい、この方式の意味がなくなります)。
GET https://manabu2.com/connect/authorize ?client_id=manabu2-mobile &redirect_uri=manabu2%3A%2F%2Fauth%2Fcallback &response_type=code &scope=read%3Acatalog%20read%3Acontent%20read%3Aprogress%20write%3Aprogress &state=<random, checked on the way back> &code_challenge=<from step 1> &code_challenge_method=S256
3. ユーザーがサインインすると、アプリにリダイレクトで戻ります。まず state が送信した値と一致することを確認してください。
manabu2://auth/callback?code=f5kIhYnGah1I…&state=<the value you sent>
4. コードをトークンと交換します。HTTP クライアントの都合に合わせて JSON ボディでも送信できます。
POST https://api.manabu2.com/api/v1/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=f5kIhYnGah1I… &client_id=manabu2-mobile &redirect_uri=manabu2://auth/callback &code_verifier=<from step 1>
レスポンスは /auth/login と同じ形式です(accessToken、expiresAt、refreshToken、scopes、user)。更新方法も同じで、POST /api/v1/auth/refresh を使います。
実装前に押さえておきたい仕様
- redirect_uri は登録済みの値と完全に一致する必要があります(スキーム・ホスト・ポート・パス・末尾のスラッシュまで)。前方一致やワイルドカードは使えません。緩い一致は認可コードが盗まれる典型的な原因だからです。
- PKCE はすべてのクライアントで必須で、code_challenge_method は S256 のみです。plain は拒否されます。
- コードは 1 回だけ使えます。2 回目の提示は盗用とみなし、そのコードで作られたセッションを失効させ、ユーザーは再サインインが必要になります。このエラーが出た場合は、まず自アプリ側の再送処理を確認してください。
- コードの有効期限は 60 秒です。受け取ったらすぐに交換してください。
- 必要最小限のスコープを要求してください。実際に付与されるのは「要求したスコープ」「クライアントに許可されたスコープ」「ユーザーの権限」の三つの積集合です。余分に要求してもエラーにはならず、超過分は単に無視されます。
- トークンエンドポイントのエラーは、この API の他の部分で使っている problem+json ではなく、OAuth 標準のエラー形式({ error, error_description })を返します。OAuth クライアントが期待する形式に合わせるためです。
サインアップ不要のアカウント作成(デバイス登録)
会員登録画面が最初の体験としてふさわしくないアプリ向けの方式です。アプリがインストールを登録すると受講者アカウントが作られ、利用者はメールアドレスもパスワードも入力せずにすぐ使い始められます。
1. 初回起動時に 1 度だけ、インストールを登録します。
POST https://api.manabu2.com/api/v1/devices/register
Content-Type: application/json
{
"clientId": "wakaroute",
"deviceId": "<stable per-install identifier, 16+ chars>",
"platform": "ios"
}
{
"deviceSecret": "mnbd_…",
"isNewAccount": true,
"auth": {
"accessToken": "eyJhbGciOi…",
"refreshToken": "3pQ7…",
"scopes": ["read:catalog", "read:content", "read:progress", "write:progress"],
"user": { "id": "…", "email": "", "displayName": "" }
}
}
2. リフレッシュトークンを失い、シークレットだけが残っている場合に使います。リフレッシュトークンが手元にあるうちは POST /api/v1/auth/refresh を使ってください。
POST https://api.manabu2.com/api/v1/devices/token
{ "clientId": "wakaroute", "deviceId": "…", "deviceSecret": "mnbd_…" }
3. 任意です。学習履歴が貯まってきた頃に案内するのが適しています。メールアドレスを紐づけると機種変更後もアカウントを引き継げます。ユーザー ID は変わらないため、それまでの記録はすべてそのまま残ります。
POST https://api.manabu2.com/api/v1/me/link
Authorization: Bearer <accessToken>
{ "email": "student@example.com", "password": "…", "displayName": "…" }
- デバイス ID は識別のためのラベルであり、認証情報ではありません。認証するのはシークレットのほうです。デバイス ID だけを提示しても 401 になります。デバイス ID は端末から読み取れ、再インストールで変わるためです。
- 同じデバイス ID で再登録すると、新しいシークレットとともに同じアカウントが返ります。空のアカウントが増えることはないため、登録済みか不明なときも安全に呼べます。
- デバイス登録で作られた受講者は、組織の有料シートを消費しません。
- 登録は IP あたり 1 分 5 回までです。実際の端末は生涯に 1 度しか登録しないため、影響を受けるのはテストと不正利用だけです。
- 連携は 1 回だけ行えます。すでにメールアドレスがあるアカウントでは 409 already_linked が返ります。アドレスの変更は確認手順を伴う別の操作です。
アクセストークンの更新
refreshToken を送ると、新しい accessToken と新しい refreshToken が返ります。保存している refreshToken は必ず新しいものに置き換えてください。
POST https://api.manabu2.com/api/v1/auth/refresh
Content-Type: application/json
{ "refreshToken": "3pQ7…" }
ログアウト
そのセッションを終了します。accessToken の期限が切れた後でも実行できるよう認証は不要で、常に 204 を返します(トークンの存在確認に使われないようにするためです)。すでに発行済みの accessToken は失効せず、期限切れ(最大 60 分)まで有効なままである点にご注意ください。
POST https://api.manabu2.com/api/v1/auth/logout
Content-Type: application/json
{ "refreshToken": "3pQ7…" }
API キーの発行
API キーの発行・失効は、組織管理者または開発者のトークンでのみ行えます。API キー自身が別のキーを発行することはできません(キーが漏洩した場合に、後継キーを作られて失効が意味を失うのを防ぐためです)。
POST https://api.manabu2.com/api/v1/organizations/{organizationId}/api-keys
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{ "name": "Zapier integration", "scopes": ["read:catalog", "read:content"] }
{
"key": "mnb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"warning": "Store this key now. It is hashed on the server and cannot be shown again.",
"apiKey": { "id": "…", "maskedKey": "mnb_live_…a1b2", "scopes": ["read:catalog","read:content"] }
}
2. スコープ
スコープは、その資格情報が「何を試せるか」の上限です。スコープを満たしていても、参照できるのは自組織のコンテンツ(および公開コンテンツ)に限られます。
| スコープ | 許可される操作 | 受講者トークン |
|---|---|---|
read:catalog | コース・パス・修了証の一覧と詳細の取得 | ✔ |
read:content | レッスン本文・教材・小テスト問題の取得 | ✔ |
read:progress | 本人の進捗・アサイン・修了証の取得 | ✔ |
write:progress | レッスンの閲覧・完了の記録、小テストの提出 | ✔ |
read:members | 組織メンバーの取得 | — |
write:members | 組織メンバーの作成・更新・無効化 | — |
write:content | コースやレッスン、テストなどの教材を作成・編集する。カリキュラム管理者のロールから付与される。 | — |
read:reports | 集計レポート・インサイトの取得 | — |
webhooks:manage | Webhook 購読の作成・管理 | — |
webhooks:manage は定義済みですが、対応するエンドポイントは未提供です(付与しても現時点では効果はありません)。read:members / write:members / read:reports は上記の組織管理エンドポイントで利用できます。
スコープには二つの上限があります。ひとつは利用者本人のロール、もうひとつはクライアントに許可されたスコープです。実際に発行されるのはその積集合で、どちらかが欠けていれば付与されません。MCP のように利用者側で自動登録されるクライアントは受講者スコープが上限で、それ以上は組織管理者が「開発者 → アプリケーション → 接続済みのクライアント」から組織ごとに許可します。
3. 参照できる範囲
コンテンツの可視性は次のルールで決まります。マルチテナントの境界そのものなので、一読をおすすめします。
| 対象 | 参照できる条件 |
|---|---|
| コース | 公開済みであり、かつ「一般公開」または「自組織が所有」であること |
| 学習パス | 「一般公開」または「自組織が所有」であること |
| レッスン・教材 | 公開済みであり、かつ所属コースを参照できること |
| 修了証 | 自組織が所有していること(修了証に一般公開の概念はありません) |
- 下書き(未公開)のコースやレッスンは、所有組織のメンバーであっても API からは一切返りません。オーサリングは管理画面で行ってください。
- 404 は「存在しない」と「参照権限がない」の両方を意味します。存在の有無自体が情報になるため、意図的に区別していません。
- コンテンツは言語ごとに作成され、言語間のフォールバックはありません。ja-JP のコースは en-US では返りません。
4. エンドポイント
カタログ・コンテンツ(参照)
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| GET | /api/v1/courses | read:catalog | コース一覧(ページング・言語・全文検索に対応) |
| GET | /api/v1/courses/{id} | read:catalog | コース詳細(セクションと公開済みレッスンを含む) |
| GET | /api/v1/lessons/{id} | read:content | レッスン詳細(本文・動画・スライド・教材・小テスト) |
| GET | /api/v1/materials/{id} | read:content | 教材の取得 |
| GET | /api/v1/paths | read:catalog | 学習パス一覧 |
| GET | /api/v1/paths/{id} | read:catalog | 学習パス詳細(受講順のコース一覧) |
| GET | /api/v1/certificates | read:catalog | 自組織の修了証一覧 |
受講者の進捗(参照・記録)
これらは「本人」を前提とするため、受講者トークンが必要です。API キーで呼び出すと 403 を返します。
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| GET | /api/v1/me | read:progress | サインイン中の受講者と所属組織 |
| GET | /api/v1/me/progress | read:progress | 受講中コースごとの進捗 |
| GET | /api/v1/me/progress/{courseId} | read:progress | 指定コースの進捗(レッスン単位の内訳) |
| GET | /api/v1/me/quiz-attempts | read:progress | 自分の過去のクイズ受験結果(得点・合否・日時)。新しい順。from / to で日付範囲を指定できます(日本時間の暦日、両端を含む)。 |
| GET | /api/v1/me/test-attempts | read:progress | 自分の過去の確認テスト受験結果。新しい順。制限時間つきのテストでは、時間内に終わったかどうかも返します(判定できない場合は null)。 |
| GET | /api/v1/me/paths | read:progress | アサインされた学習パス |
| GET | /api/v1/me/certificates | read:progress | 取得済みの修了証 |
| GET | /api/v1/me/assignments | read:progress | アサインされたコース(直接またはパス経由) |
| GET | /api/v1/me/goals | read:progress | 学習者の目標(志望校・志望大学など)を志望順に取得 |
| PUT | /api/v1/me/goals | write:progress | 目標リストを丸ごと置き換える(並び順=志望順) |
| GET | /api/v1/me/target-schools | read:progress | 志望校を志望順に取得(ワカルート用の別名) |
| PUT | /api/v1/me/target-schools | write:progress | 志望校リストを丸ごと置き換える |
| POST | /api/v1/me/study-sessions/start | write:progress | 学習タイマーを開始(サーバーが時刻を打刻) |
| POST | /api/v1/me/study-sessions/{sessionId}/stop | write:progress | 学習タイマーを停止。上限超過分は abandoned として計上しない |
| POST | /api/v1/me/study-sessions | write:progress | 終了済みセッションを記録(オフライン再送用。clientSessionId で重複防止) |
| GET | /api/v1/me/study-sessions | read:progress | 学習履歴を新しい順に取得(何を・いつ) |
| GET | /api/v1/me/study-summary | read:progress | カレンダー用の日次・月次集計(0 の日も含む密な系列) |
| GET | /api/v1/me/study-streak | read:progress | 連続学習日数(端末を変えても保持) |
| DELETE | /api/v1/me | write:progress | アカウントを削除(App Store 5.1.1(v) 対応。端末登録も消え、次回起動で新規アカウントになる) |
| POST | /api/v1/lessons/{id}/view | write:progress | レッスンを閲覧したことを記録(冪等) |
| POST | /api/v1/lessons/{id}/complete | write:progress | レッスンを完了として記録し、更新後の進捗を返す |
| POST | /api/v1/lessons/{id}/quiz/submit | write:progress | 小テストの解答を提出。採点はサーバー側で行い、合格するとレッスンも完了になります |
認証・セッション
受講者トークンのライフサイクル。API キーには適用されません(API キーに期限切れやセッションの概念はなく、失効は組織管理者が行います)。
| メソッド | URL | 必要な資格情報 | 概要 |
|---|---|---|---|
| POST | /api/v1/auth/login | — | メールアドレスとパスワードで認証し、accessToken と refreshToken を返す |
| POST | /api/v1/auth/refresh | refreshToken | refreshToken を新しい accessToken + refreshToken に交換(1 回限り・再利用検知あり) |
| POST | /api/v1/auth/logout | refreshToken | そのセッションを終了(冪等、常に 204) |
| POST | /api/v1/auth/logout-all | accessToken | すべての端末のセッションを終了 |
| GET | /api/v1/auth/sessions | accessToken | 有効なセッションの一覧(作成日時・端末情報) |
組織管理(メンバー・部署・アサイン・レポート)
人事システムとの連携用です。組織管理者または HR の受講者トークン、あるいは該当スコープを付与した組織 API キーが必要です。いずれの場合も、ルートで指定した組織に対する権限が別途確認されます。
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| GET | /api/v1/organizations/{orgId}/members | read:members | メンバー一覧(招待中を含む) |
| GET | /api/v1/organizations/{orgId}/members/{userId} | read:members | メンバー 1 件の取得 |
| POST | /api/v1/organizations/{orgId}/members | write:members | メールアドレスで招待(シートを 1 つ消費) |
| PUT | /api/v1/organizations/{orgId}/members/{userId}/roles | write:members | ロールを一括更新(送信しなかったロールは解除されます) |
| DELETE | /api/v1/organizations/{orgId}/members/{userId} | write:members | メンバーを削除し、シートを解放 |
| GET | /api/v1/organizations/{orgId}/seats | read:members | シートの利用状況(used / total / available) |
| GET | /api/v1/organizations/{orgId}/departments | read:members | 部署一覧 |
| POST | /api/v1/organizations/{orgId}/departments | write:members | 部署の作成 |
| DELETE | /api/v1/organizations/{orgId}/departments/{deptId} | write:members | 部署の削除(メンバーは組織に残ります) |
| PUT / DELETE | …/departments/{deptId}/members/{userId} | write:members | 部署へのメンバー追加・削除 |
| PUT / DELETE | …/members/{userId}/paths/{pathId} | write:members | 学習パスを個人にアサイン・解除 |
| PUT / DELETE | …/departments/{deptId}/paths/{pathId} | write:members | 学習パスを部署にアサイン・解除(所属者全員に適用) |
| GET | /api/v1/organizations/{orgId}/progress | read:reports | 組織全体の進捗(メンバー別の明細と集計) |
API キー管理
組織管理者の受講者トークンが必要です。
| メソッド | URL | 概要 |
|---|---|---|
| GET | /api/v1/organizations/{orgId}/api-keys | 組織の API キー一覧(失効済みも含む。キー本体は返りません) |
| POST | /api/v1/organizations/{orgId}/api-keys | API キーを発行(キー本体はこの応答のみ) |
| DELETE | /api/v1/organizations/{orgId}/api-keys/{keyId} | API キーを即時失効 |
教材の作成・編集
いずれも write:content と、対象組織におけるカリキュラム管理者(または管理者)のロールが必要です。トークンにスコープがあるだけでは足りません。
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| POST | /api/v1/organizations/{organizationId}/courses | write:content | コースを作成する |
| PUT | /api/v1/organizations/{organizationId}/courses/{courseId} | write:content | コースを更新する |
| POST | /api/v1/organizations/{organizationId}/courses/{courseId}/publish | write:content | コースの公開状態を切り替える |
| DELETE | /api/v1/organizations/{organizationId}/courses/{courseId} | write:content | コースを削除する。配下がある場合は cascade=true が必要 |
| GET | /api/v1/organizations/{organizationId}/courses/{courseId}/sections | read:catalog | セクション一覧(下書きを含む) |
| POST | /api/v1/organizations/{organizationId}/courses/{courseId}/sections | write:content | セクションを追加する |
| PUT | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId} | write:content | セクションを更新する |
| DELETE | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId} | write:content | セクションを削除する。レッスンがある場合は cascade=true が必要 |
| GET | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId}/lessons | read:content | レッスン一覧(本文は含まない) |
| POST | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId}/lessons | write:content | レッスンを作成する |
| PUT | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId}/lessons/{lessonId} | write:content | レッスン本文を更新する |
| POST | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId}/lessons/{lessonId}/publish | write:content | レッスンの公開状態を切り替える |
| DELETE | /api/v1/organizations/{organizationId}/courses/{courseId}/sections/{sectionId}/lessons/{lessonId} | write:content | レッスンを削除する |
| GET | /api/v1/organizations/{organizationId}/materials | read:content | 教材一覧 |
| POST | /api/v1/organizations/{organizationId}/materials | write:content | 教材を作成する |
| PUT | /api/v1/organizations/{organizationId}/lessons/{lessonId}/materials | write:content | 教材をレッスンに紐づける |
テスト
受験用の取得では正解は返しません。正解を含むのは組織配下の編集用エンドポイントのみです。
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| GET | /api/v1/tests/{testId} | read:content | 受験用にテストを取得する(正解は含まない) |
| POST | /api/v1/tests/{testId}/submit | write:progress | 解答を送信し採点結果を受け取る |
| GET | /api/v1/me/tests | read:progress | 自分が受験できるテストと最新結果 |
| GET | /api/v1/organizations/{organizationId}/tests | read:content | 組織のテスト一覧 |
| GET | /api/v1/organizations/{organizationId}/tests/{testId} | write:content | 正解を含むテストを取得する(編集用) |
| POST | /api/v1/organizations/{organizationId}/tests | write:content | テストを作成する |
| POST | /api/v1/organizations/{organizationId}/tests/{testId}/questions | write:content | 設問と選択肢をまとめて追加する |
レポート・契約状況
すべて集計値です。個人単位の進捗は read:members が必要な既存のエンドポイントで提供します。契約情報は参照のみで、購入や変更の API はありません。
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| GET | /api/v1/organizations/{organizationId}/overview | read:reports | 組織の概況(席数・受講状況) |
| GET | /api/v1/organizations/{organizationId}/reports/completion | read:reports | コース別の修了状況 |
| GET | /api/v1/organizations/{organizationId}/reports/departments | read:reports | 部署別の修了状況 |
| GET | /api/v1/organizations/{organizationId}/reports/trend | read:reports | 月次の修了推移 |
| GET | /api/v1/organizations/{organizationId}/subscription | read:reports | 契約プランと席数(参照のみ) |
検索
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| GET | /api/v1/search?q= | read:catalog | コースとパスを全文検索する |
AI 生成
生成結果を返すだけで、保存はしません。内容を確認したうえで作成用エンドポイントに渡してください。呼び出しごとに課金され、読み取り系とは別のレート制限が適用されます。
| メソッド | URL | 必要スコープ | 概要 |
|---|---|---|---|
| POST | /api/v1/organizations/{organizationId}/ai/course-outline | write:content | コース構成案を生成する |
| POST | /api/v1/organizations/{organizationId}/ai/lesson-body | write:content | レッスン本文を生成する |
| POST | /api/v1/organizations/{organizationId}/ai/quiz-questions | write:content | 設問案を生成する(正解を含む) |
5. リクエスト例
API キーでコース一覧を取得する例です。
curl -H "X-Api-Key: mnb_live_…" \
"https://api.manabu2.com/api/v1/courses?culture=en-GB&pageSize=2"
{
"items": [
{
"id": "0c1f…",
"title": "AI リテラシー入門",
"culture": "en-GB",
"level": "Beginner",
"estimatedMinutes": 90,
"isPublic": true,
"sectionCount": 4,
"lessonCount": 12
}
],
"page": 1,
"pageSize": 2,
"totalItems": 137,
"totalPages": 69
}
6. ページングとフィルタ
一覧系エンドポイントは共通の形式で items / page / pageSize / totalItems / totalPages を返します。
page— ページ番号(1 始まり、既定 1)pageSize— 1 ページあたりの件数(既定 20、最大 100。超過分は拒否ではなく 100 に丸められます)culture— コンテンツの言語(ja-JP / en-US)organizationId— 参照できる組織のうち 1 つに絞り込みq— コース名・説明の全文検索
7. エラー
エラーは JSON で返り、error / message / status を含みます。スコープ不足の場合は必要なスコープ名も返します。
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://manabu2.com/ja-JP/docs/api#errors",
"title": "Forbidden",
"status": 403,
"detail": "This endpoint requires the 'write:progress' scope.",
"code": "insufficient_scope",
"requiredScope": "write:progress"
}
エラーコード一覧
code |
ステータス | 意味 |
|---|---|---|
invalid_request | 400 | リクエストの形式が不正(必須項目の欠落など) |
invalid_credentials | 401 | メールアドレスまたはパスワードが違います |
invalid_grant | 401 | refreshToken が無効、失効済み、または期限切れ |
refresh_token_reused | 401 | 使用済みの refreshToken が再送されました。セッション全体を失効させています |
unauthorized | 401 | 資格情報がない、または無効です |
insufficient_scope | 403 | スコープが不足しています(requiredScope に必要なスコープが入ります) |
user_credential_required | 403 | 受講者トークンが必要な操作を API キーで呼び出しました |
admin_required | 403 | 組織管理者のみ実行できます |
not_found | 404 | 存在しない、または参照権限がありません |
idempotency_key_reused | 409 | この Idempotency-Key は別のリクエストで既に使われています。新しいキーを使ってください |
idempotency_key_in_progress | 409 | このキーの同一リクエストがまだ処理中です。しばらくしてから再試行してください |
rate_limit_exceeded | 429 | レート制限を超過しました |
HTTP ステータス
| ステータス | 意味 |
|---|---|
400 | リクエストが不正(必須項目の欠落、スコープ指定が空など) |
401 | 資格情報がない、無効、失効済み、または期限切れ |
403 | 認証は成功したが権限が不足(スコープ不足、または受講者トークンが必要な操作) |
404 | 存在しない、または参照権限がない |
409 | 競合 — 既に記録されている内容と矛盾しています。多くは Idempotency-Key を別のリクエストで再利用した場合です |
429 | レート制限を超過(Retry-After ヘッダーの秒数だけ待ってから再試行してください) |
500 | サーバー内部エラー |
8. レート制限
レート制限は呼び出し元ごとに適用されます。API キー、次に利用者、いずれもない場合は IP アドレス単位です。同じ IP を共有していても、認証済みの呼び出しが匿名アクセスの枠に巻き込まれることはありません。
| 呼び出し元 | 上限 |
|---|---|
| API キー / 受講者トークン(認証済み) | 300 / min |
| 未認証(IP アドレス単位) | 60 / min |
| AI 生成 (/api/ai/*) | 10 / min |
上限を超えると 429 を返します。応答には Retry-After と X-RateLimit-* ヘッダーが含まれるため、待つべき秒数を推測する必要はありません。
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1785418481
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Retry in 60 seconds.",
"retryAfterSeconds": 60,
"status": 429
}
X-RateLimit-* ヘッダーは 429 応答にのみ付与されます。通常の応答に残り回数は含まれません。上限は 1 分単位の固定ウィンドウで、超過分はキューイングされず即座に拒否されます(待たせるより早く失敗させる方が、呼び出し側で扱いやすいためです)。
9. Webhook(イベント通知)
ポーリングの代わりに、イベント発生時に指定の URL へ POST します。組織単位で登録し、webhooks:manage スコープが必要です。
エンドポイントの登録
POST https://api.manabu2.com/api/v1/organizations/{organizationId}/webhooks
X-Api-Key: mnb_live_…
Content-Type: application/json
{
"url": "https://example.com/hooks/manabu2",
"eventTypes": ["lesson.completed", "certificate.issued"],
"description": "HR system sync"
}
{
"secret": "whsec_…",
"warning": "Store this signing secret now. It is not shown again.",
"webhook": { "id": "…", "url": "https://example.com/hooks/manabu2", "isActive": true }
}
イベントの種類
| イベント | 送信されるタイミング |
|---|---|
lesson.completed | 受講者がレッスンを初めて完了したとき(再完了では送信されません) |
certificate.issued | 修了証が発行されたとき(自動発行・手動発行の両方) |
ping | テスト送信。/test エンドポイントからのみ発生します |
course.started / member.added / subscription.activated は種類としては定義済みですが、まだ発生元が実装されていないため送信されません。eventTypes を省略すると「すべて」の購読となり、今後追加されるイベントも自動的に届きます。
配信されるリクエスト
POST https://example.com/hooks/manabu2
X-Manabu-Event: lesson.completed
X-Manabu-Delivery: 9f2c…
X-Manabu-Timestamp: 1785424800
X-Manabu-Signature: sha256=4a7f…
{
"id": "96ea88a005cb461798e4659e3d9cb9a8",
"type": "lesson.completed",
"createdAt": "2026-07-31T09:20:00Z",
"organizationId": "…",
"data": { "userId": "…", "lessonId": "…", "courseId": "…" }
}
署名の検証
署名は HMAC-SHA256 で、対象は「タイムスタンプ + ピリオド + ボディ」です(Stripe や GitHub と同じ形式)。URL を知っている人なら誰でも POST できてしまうため、必ず検証してから処理してください。
// Node.js
const crypto = require("crypto");
function verify(req, secret) {
const timestamp = req.headers["x-manabu-timestamp"];
const signature = req.headers["x-manabu-signature"];
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${req.rawBody}`) // the raw body, not the parsed object
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
リトライ
2xx 以外を返した場合、または応答がない場合は再送します。
- 最大 6 回、30 秒 → 2 分 → 10 分 → 30 分 → 1 時間と間隔を広げながら再送し、約 2 時間で打ち切ります。
- 成功と見なすのは 2xx のみです。処理に時間がかかる場合は、まず 200 を返してから非同期で処理してください(応答は 10 秒でタイムアウトします)。
- 同じイベントが複数回届く可能性があります。ペイロードの id は再送しても同一なので、これを使って重複処理を避けてください。
- 配信結果は /deliveries で確認でき、失敗したものは /replay で再送できます。
管理エンドポイント
| メソッド | URL | 概要 |
|---|---|---|
| GET | /api/v1/webhook-events | 購読可能なイベント種類の一覧 |
| GET | …/organizations/{orgId}/webhooks | 登録済みエンドポイントの一覧(シークレットは含まれません) |
| POST | …/organizations/{orgId}/webhooks | エンドポイントを登録し、署名シークレットを返す |
| DELETE | …/webhooks/{id} | エンドポイントと配信履歴を削除 |
| POST | …/webhooks/{id}/test | ping を送信して疎通確認(そのエンドポイントにのみ送信されます) |
| GET | …/webhooks/{id}/deliveries | 直近の配信履歴(試行回数・HTTP ステータス・次回再送予定) |
| POST | …/webhooks/deliveries/{id}/replay | 失敗した配信を再送 |
すべて webhooks:manage スコープが必要です。登録・削除・テスト・再送は組織管理者または HR の権限も必要です。
MCP(AI クライアントからの接続)
同じデータを、AI クライアントからツールとして扱えます。認証は OAuth のみで、API キーは使えません。
| エンドポイント | https://api.manabu2.com/mcp |
| 1. 認証 | Authorization: Bearer <jwt> |
Streamable HTTP(Mcp-Session-Id ヘッダー)と、従来の HTTP+SSE の両方に対応しています。クライアントが対応している方が使われます。
見えるツールはトークンのスコープで決まります。使えないツールは tools/list に出てきません。広告しておいて拒否するより、見せない方が確実です。
組織ごとの権限
スコープは「どこかで編集してよい」ことしか表しません。ある組織のカリキュラム管理者は他の組織でも write:content を持つため、変更を伴うツールは実行のたびに対象組織での役割を確認し、記録します。
エラーコード
ツールのエラーは JSON-RPC の error に data.code として安定した文字列を返します。
data.code | |
|---|---|
invalid_request | 引数が足りない、または値が不正です。 |
not_found | 存在しないか、権限がないため見えません。両者を区別しません。 |
forbidden | その組織での役割が足りません。 |
insufficient_scope | トークンにそのスコープがありません。 |
unknown_tool | そのツールはありません。 |
user_credential_required | API キーでは使えません。サインインして接続してください。 |
10. 冪等性(Idempotency)
リクエストがタイムアウトしても、サーバー側で処理が実行されたかどうかは分かりません。書き込み系のリクエストに Idempotency-Key ヘッダーを付けると、安全に再送できます。最初の呼び出しだけが実際に処理を行い、同じリクエストの再送には最初の応答がそのまま返されます。特に重要なのはクイズの提出で、これがないと再送のたびに採点対象の受験履歴が新たに記録されてしまいます。
POST https://api.manabu2.com/api/v1/lessons/{lessonId}/quiz/submit
Authorization: Bearer <jwt>
Idempotency-Key: 9f1c2d4e-7a3b-4c11-9e55-2b8d0f6a1c37
Content-Type: application/json
{ "answers": [ { "questionId": "q1", "choiceId": "c3" } ] }
再送時には、ステータスコードも本文も最初の応答とまったく同じものが返り、次のヘッダーが追加されます。
HTTP/1.1 200 OK Idempotency-Replayed: true
| 送信内容 | 結果 |
|---|---|
| 同じキー・同じリクエスト | 最初の応答(Idempotency-Replayed: true 付き) |
| 同じキーで本文やパスが異なる | 409 idempotency_key_reused |
| 最初の呼び出しの処理中に同じキーで再送 | 409 idempotency_key_in_progress |
| 最初の呼び出しが 5xx で失敗した | 結果は保存されないため、再送は実際に処理されます(一時的な障害が恒久的なエラーとして固定されることはありません) |
- 任意です。ヘッダーを付けない場合の動作はこれまでと変わりません。POST・PUT・PATCH で有効です。GET と DELETE はもともと再実行しても安全なため、指定しても無視されます。
- キーは 24 時間保持されます。それ以降は同じ値を別のリクエストに再利用できます。
- キーは認証情報ごとに管理されるため、他のお客様のキーと衝突することはありません。操作ごとに新しい値(UUID を推奨)を使ってください。
- /auth 系のエンドポイントでは利用できません。発行したトークンを保存しないためです。リフレッシュについては、トークンファミリーによる再利用検知ですでに保護されています。
11. 現時点で未提供の機能
探しても見つからないものを明記しておきます。
- プラン別のクォータ(無料 / 有料)と利用量の永続的な集計・課金連携は未実装です。上記のレート制限は適用されますが、月間の使用量に応じた制限はありません。
- Webhook を管理する画面はまだありません(API からのみ登録・削除できます)。また配信ワーカーは単一インスタンス前提のため、API をスケールアウトすると重複配信の可能性があります。
- 組織・メンバー管理エンドポイントは未提供です(スコープのみ予約済み)。
- URL にバージョンは含まれていません。バージョニング方針は検討中で、後から変更する場合は破壊的変更になります。
- 各言語向けの SDK と Postman コレクションは未提供です。OpenAPI JSON からの生成をご利用ください。