Android アプリおよび Web フロントエンドから共通で利用する REST API の仕様です。
| 項目 | 内容 |
|---|---|
| ベースURL | 開発: http://localhost:3001 / 本番: デプロイ先のホスト |
| API プレフィックス | すべて /api 配下(例: /api/events) |
| Content-Type | リクエスト・レスポンスとも application/json |
| 文字コード | UTF-8 |
| 日時形式 | RFC3339(例: 2025-02-03T10:00:00Z) |
- すべてのオリジンから許可(
Access-Control-Allow-Origin: *) - 許可メソッド:
GET,POST,PUT,DELETE,PATCH,OPTIONS - 許可ヘッダー:
Content-Type,Authorization,X-Admin-Keyなど
以下のいずれかで認証します。
- Authorization ヘッダー:
Authorization: Bearer <JWT>- ログイン後に取得した JWT を付与。Android ではトークンを安全に保存し、各リクエストに付与すること。
エラー時は HTTP ステータスコード 4xx/5xx とともに JSON で error が返ります。
{
"error": "エラーメッセージ(文字列)"
}| HTTP | 意味 |
|---|---|
| 400 | Bad Request(パラメータ不正・バリデーションエラー) |
| 401 | Unauthorized(未認証 or トークン無効) |
| 403 | Forbidden(権限不足) |
| 404 | Not Found(リソース不存在) |
| 500 | Internal Server Error(サーバーエラー) |
- EventStatus:
"draft"|"published"|"ongoing"|"completed"|"cancelled"
- TaskStatus:
"todo"|"in_progress"|"completed"|"cancelled"
- BudgetType:
"income"|"expense"
- InvitationStatus:
"pending"|"accepted"|"declined"
- すべて RFC3339 文字列(例:
2025-02-03T09:00:00+09:00)
| メソッド | パス | 説明 |
|---|---|---|
| GET | /api/health |
サーバー稼働確認 |
レスポンス例 (200)
{
"status": "ok",
"message": "Sherpa Backend API is running"
}| メソッド | パス | 説明 |
|---|---|---|
| GET | /api/auth/google |
Google ログイン画面へリダイレクト。Cookie で state を保持するため、ブラウザ/WebView で開く必要あり(同一オリジンで Cookie が使えること)。 |
- レスポンス: 302 で Google の認証 URL へリダイレクト
- Android 用:
?redirect_uri=sherpa://auth/callbackを付与すると、認証成功後にsherpa://auth/callback?token=<JWT>へリダイレクトされる。Android アプリはこの URL を Intent で受信し、トークンを保存して API にAuthorization: Bearer <token>を付与する。
| メソッド | パス | 説明 |
|---|---|---|
| GET | /api/auth/callback?code=...&state=... |
Google 認証後のコールバック。成功時は FRONTEND_URL/auth/callback?token=<JWT> へリダイレクト。 |
- Android: カスタムスキームや App Links で
auth/callback?token=xxxを受け取り、tokenを保存して以降の API でAuthorization: Bearer <token>に使用する。
| メソッド | パス | 説明 |
|---|---|---|
| GET | /api/auth/me |
リクエストの JWT から現在のユーザーを返す。要認証(Authorization: Bearer)。 |
レスポンス例 (200)
{
"user": {
"id": 1,
"name": "山田太郎",
"email": "yamada@example.com",
"avatar_url": "https://...",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}User オブジェクト
| フィールド | 型 | 説明 |
|---|---|---|
| id | number | ユーザーID |
| name | string | 表示名 |
| string | メールアドレス | |
| avatar_url | string | null | アバターURL(任意) |
| created_at | string | 作成日時 (RFC3339) |
| updated_at | string | 更新日時 (RFC3339) |
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events |
不要 |
レスポンス (200)
{
"events": [
{
"id": 1,
"organization_id": 1,
"title": "技術勉強会",
"start_at": "2025-03-01T10:00:00Z",
"end_at": "2025-03-01T12:00:00Z",
"location": "東京都渋谷区",
"status": "draft",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"organization": { "id": 1, "name": "...", "description": null }
}
]
}| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events/:id |
不要 |
- パスパラメータ:
id— イベントID(数値)
レスポンス (200)
イベント単体 + Organization, EventStaffs.User, Tasks(Assignees.User 含む), Budgets を Preload した形で返却。
{
"event": {
"id": 1,
"organization_id": 1,
"title": "技術勉強会",
"start_at": "2025-03-01T10:00:00Z",
"end_at": "2025-03-01T12:00:00Z",
"location": "東京都渋谷区",
"status": "draft",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"organization": { ... },
"event_staffs": [ { "id": 1, "event_id": 1, "user_id": 1, "role": "Admin", "user": { ... } } ],
"tasks": [ ... ],
"budgets": [ ... ]
}
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/events |
不要 |
リクエスト Body
{
"organization_id": 1,
"title": "技術勉強会",
"start_at": "2025-03-01T10:00:00Z",
"end_at": "2025-03-01T12:00:00Z",
"location": "東京都渋谷区",
"status": "draft",
"user_id": 1
}| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| organization_id | number | ○ | 組織ID |
| title | string | ○ | タイトル |
| start_at | string | ○ | 開始日時 (RFC3339) |
| end_at | string | ○ | 終了日時 (RFC3339) |
| location | string | - | 場所 |
| status | string | - | 上記 EventStatus。省略時は draft |
| user_id | number | - | 作成者ユーザーID。指定時は EventStaff として Admin で登録 |
レスポンス (201)
{
"event": { "id": 2, "organization_id": 1, "title": "...", ... }
}| メソッド | パス | 認証 |
|---|---|---|
| PUT | /api/events/:id |
不要 |
リクエスト Body
更新したいフィールドを Event オブジェクトの形で送る(部分更新ではなくモデル単位で送る実装のため、必要なら現状の event を取得してから変更して PUT)。
- 例:
title,start_at,end_at,location,statusなど
レスポンス (200)
更新後の event オブジェクト。
| メソッド | パス | 認証 |
|---|---|---|
| DELETE | /api/events/:id |
不要 |
レスポンス (200)
{
"message": "Event deleted successfully"
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/events/create-chat |
不要 |
リクエスト Body
{
"message": "3月に勉強会をやりたい",
"history": [
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "..." }
]
}history: 省略可。空配列でよい。
レスポンス (200)
{
"reply": "AIの返答テキスト",
"suggestedEvent": {
"title": "技術勉強会",
"start_at": "2025-03-01T10:00:00Z",
"end_at": "2025-03-01T12:00:00Z",
"location": "東京都渋谷区"
}
}suggestedEventは、AI がイベント案を確定した場合のみ含まれる。ない場合はキーごと無し。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events/:id/tasks |
不要 |
- パスパラメータ:
id— イベントID
レスポンス (200)
{
"tasks": [
{
"id": 1,
"event_id": 1,
"assignee_id": null,
"title": "会場予約",
"link": "",
"links": [],
"start_at": null,
"deadline": "2025-02-10T23:59:59Z",
"recurrence": null,
"status": "todo",
"is_ai_generated": false,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"assignee": null,
"assignees": [
{ "task_id": 1, "user_id": 2, "user": { "id": 2, "name": "...", "email": "..." } }
]
}
]
}Task オブジェクト
| フィールド | 型 | 説明 |
|---|---|---|
| id | number | タスクID |
| event_id | number | イベントID |
| assignee_id | number | null | 単一担当者(後方互換) |
| title | string | タイトル |
| link | string | リンク(1本) |
| links | string[] | リンク複数 |
| start_at | string | null | 開始日時 (RFC3339) |
| deadline | string | 締切 (RFC3339) |
| recurrence | object | null | 繰り返しルール(下記) |
| status | string | TaskStatus |
| is_ai_generated | boolean | AI 生成フラグ |
| created_at / updated_at | string | 日時 |
| assignee | User | null | 単一担当者(後方互換) |
| assignees | TaskAssignee[] | 担当者一覧(user を Preload) |
RecurrenceRule
{
"type": "weekly",
"weekdays": [1, 3, 5],
"start_time": "09:00",
"end_time": "17:00"
}type:"weekly"|"daily"|"monthly"weekdays: 0=日..6=土
| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/events/:id/tasks |
不要 |
- パスパラメータ:
id— イベントID
リクエスト Body
{
"title": "会場予約",
"link": "https://...",
"links": ["https://..."],
"start_at": "2025-02-01T09:00:00Z",
"deadline": "2025-02-10T23:59:59Z",
"recurrence": null,
"status": "todo",
"is_ai_generated": false,
"assignee_ids": [1, 2]
}| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| title | string | - | タイトル |
| link | string | - | リンク1本 |
| links | string[] | - | リンク複数 |
| start_at | string | - | 開始日時 (RFC3339) |
| deadline | string | ○ | 締切 (RFC3339) |
| recurrence | RecurrenceRule | - | 繰り返し |
| status | string | - | 省略時 todo |
| is_ai_generated | boolean | - | デフォルト false |
| assignee_ids | number[] | - | 担当者ユーザーIDの配列 |
レスポンス (201)
作成された task(assignee / assignees 含む)。
| メソッド | パス | 認証 |
|---|---|---|
| PUT | /api/tasks/:id |
不要 |
- パスパラメータ:
id— タスクID
リクエスト Body
更新したい項目のみ送る(未指定は変更しない)。
{
"title": "会場予約(更新)",
"link": "https://...",
"links": [],
"start_at": null,
"deadline": "2025-02-15T23:59:59Z",
"recurrence": null,
"status": "in_progress",
"assignee_ids": [1]
}- 各フィールドは任意。
assignee_idsを送るとその内容で担当者を置き換え。
レスポンス (200)
更新後の task。
| メソッド | パス | 認証 |
|---|---|---|
| DELETE | /api/tasks/:id |
不要 |
レスポンス (200)
{
"message": "Task deleted successfully"
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/tasks/generate |
不要 |
リクエスト Body
{
"eventTitle": "技術勉強会 2025"
}レスポンス (200)
{
"tasks": [
{ "title": "会場の手配", "deadline": "2週間前" },
{ "title": "資料準備", "deadline": "1週間前" }
]
}tasksの各要素は TaskSuggestion(title / deadline のみ)。deadline は日本語表現のことがある(「残り2日」「本日締め切り」等)。実際にタスクとして登録する場合は、フロント/Android で/api/events/:id/tasksに POST すること。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events/:id/budgets |
不要 |
レスポンス (200)
{
"budgets": [
{
"id": 1,
"event_id": 1,
"category": "会場費",
"type": "expense",
"planned_amount": 50000,
"actual_amount": 0,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}- type:
"income"|"expense"
| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/events/:id/budgets |
不要 |
リクエスト Body
{
"category": "会場費",
"type": "expense",
"planned_amount": 50000,
"actual_amount": 0
}| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| category | string | ○ | カテゴリ名 |
| type | string | ○ | "income" | "expense" |
| planned_amount | number | - | 予定金額(デフォルト 0) |
| actual_amount | number | - | 実績金額(デフォルト 0) |
レスポンス (201)
作成された budget。
| メソッド | パス | 認証 |
|---|---|---|
| PUT | /api/budgets/:id |
不要 |
リクエスト Body
更新したい項目のみ。
{
"category": "会場費(更新)",
"type": "expense",
"planned_amount": 60000,
"actual_amount": 55000
}レスポンス (200)
更新後の budget。
| メソッド | パス | 認証 |
|---|---|---|
| DELETE | /api/budgets/:id |
不要 |
レスポンス (200)
{
"ok": true
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/users |
不要 |
リクエスト Body
{
"name": "山田太郎",
"email": "yamada@example.com"
}- 同一 email が既にいると 400(「このメールアドレスは既に登録されています」)。
レスポンス (201)
作成された user。デフォルト組織にも追加される。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/users/search?q=検索文字列 |
要 Bearer |
- クエリ:
q— 名前またはメールの部分一致(最大30件)。
レスポンス (200)
{
"users": [
{ "id": 1, "name": "山田太郎", "email": "yamada@example.com", ... }
]
}| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/users/:id |
不要 |
レスポンス (200)
user 単体。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/users/:id/events |
不要 |
- EventStaff として参加しているイベントのみ返却。
レスポンス (200)
{
"events": [ { "id": 1, "title": "...", ... } ]
}いずれも Authorization: Bearer <JWT> 必須。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events/:id/invitable-users |
要 Bearer |
- 同一組織内で、未参加・未招待のユーザー。呼び出しユーザーがイベント Admin である必要あり。
レスポンス (200)
users 配列。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events/:id/invitations |
要 Bearer |
レスポンス (200)
{
"invitations": [
{
"id": 1,
"event_id": 1,
"inviter_id": 1,
"user_id": 2,
"role": "Staff",
"status": "pending",
"created_at": "...",
"updated_at": "...",
"user": { ... },
"inviter": { ... }
}
]
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/events/:id/invitations |
要 Bearer |
リクエスト Body
user_id または email のどちらか必須。
{
"user_id": 2,
"email": "",
"role": "Staff"
}- role: 例
"Admin","Staff","Sponsor"など。
レスポンス (201)
作成された invitation(user / inviter Preload)。
| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/invitations/:id/accept |
要 Bearer |
- 招待の宛先ユーザー本人のみ実行可。
レスポンス (200)
{
"invitation": { ... },
"event_staff": { "id": 1, "event_id": 1, "user_id": 2, "role": "Staff", ... }
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/invitations/:id/decline |
要 Bearer |
レスポンス (200)
更新後の invitation。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/invitations/mine |
要 Bearer |
レスポンス (200)
invitations 配列(status: pending、Event / Inviter Preload)。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/notifications |
要 Bearer |
レスポンス (200)
{
"notifications": [
{
"id": 1,
"user_id": 1,
"type": "event_invite",
"title": "イベントへの招待",
"body": "山田 さんから「技術勉強会」への招待が届きました。",
"related_id": 1,
"related_type": "event_invitation",
"read_at": null,
"created_at": "2025-01-01T00:00:00Z"
}
]
}- 直近100件、
created_at降順。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/notifications/unread-count |
要 Bearer |
レスポンス (200)
{
"count": 3
}| メソッド | パス | 認証 |
|---|---|---|
| PATCH | /api/notifications/:id/read |
要 Bearer |
- Body は不要(空でよい)。自分の通知のみ既読にできる。
レスポンス (200)
更新後の notification(read_at がセットされたもの)。
いずれも Authorization: Bearer <JWT> 必須。イベントに紐づくチャンネルで、Slack 風のチャット機能を提供。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/events/:id/channels |
要 Bearer |
- チャンネルが0件の場合は「#全体」が自動作成され、スタッフがメンバーになる。
レスポンス (200)
{
"channels": [
{
"id": 1,
"event_id": 1,
"name": "#全体",
"description": "このチャンネルは...",
"is_private": false,
"created_at": "...",
"updated_at": "..."
}
]
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/events/:id/channels |
要 Bearer |
リクエスト Body
{
"name": "general",
"description": "雑談用",
"is_private": false
}nameに#が無い場合はサーバー側で先頭に付与。
レスポンス (201)
作成された channel。作成者は自動でメンバーになる。
| メソッド | パス | 認証 |
|---|---|---|
| PATCH | /api/channels/:id |
要 Bearer |
リクエスト Body
更新したい項目のみ。
{
"name": "#general",
"description": "説明文",
"is_private": false
}レスポンス (200)
更新後の channel。
| メソッド | パス | 認証 |
|---|---|---|
| DELETE | /api/channels/:id |
要 Bearer |
- 「#全体」は削除不可(400 でエラー)。
レスポンス (200)
{ "ok": true }。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/channels/:id/members |
要 Bearer |
レスポンス (200)
members 配列(ChannelMember、User Preload)。
| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/channels/:id/members |
要 Bearer |
リクエスト Body
{
"user_id": 2
}- 対象ユーザーはイベントスタッフである必要あり。
レスポンス (201)
追加された member。
| メソッド | パス | 認証 |
|---|---|---|
| DELETE | /api/channels/:id/members/:userId |
要 Bearer |
- パス:
userIdは削除するユーザーID(数値)。
レスポンス (200)
{ "ok": true }。
| メソッド | パス | 認証 |
|---|---|---|
| GET | /api/channels/:id/messages |
要 Bearer |
- 親メッセージのみ(スレッドの子は含まない)。最大100件、
created_at昇順。
レスポンス (200)
{
"messages": [
{
"id": 1,
"channel_id": 1,
"user_id": 1,
"content": "こんにちは",
"parent_message_id": null,
"created_at": "...",
"updated_at": "...",
"is_deleted": false,
"user": { "id": 1, "name": "...", "email": "..." },
"reactions": [
{ "id": 1, "message_id": 1, "user_id": 2, "emoji": "👍", "user": { ... } }
]
}
]
}| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/channels/:id/messages |
要 Bearer |
- イベントスタッフのみ投稿可。
リクエスト Body
{
"content": "メッセージ本文"
}レスポンス (201)
作成された message(user Preload)。同一チャンネルには WebSocket でも配信される。
| メソッド | パス | 認証 |
|---|---|---|
| PATCH | /api/messages/:id |
要 Bearer |
リクエスト Body
{
"content": "編集後の本文"
}レスポンス (200)
更新後の message。WebSocket で message_updated が配信される。
| メソッド | パス | 認証 |
|---|---|---|
| DELETE | /api/messages/:id |
要 Bearer |
レスポンス (200)
{ "ok": true }。WebSocket で message_deleted が配信される。
| メソッド | パス | 認証 |
|---|---|---|
| POST | /api/messages/:id/reactions |
要 Bearer |
リクエスト Body
{
"emoji": "👍"
}emoji省略時は"👍"。既に同じユーザーが同じ絵文字でつけていれば削除、なければ追加。
レスポンス (200)
追加時: { "action": "added", "reaction": { "id": 1, "message_id": 1, "user_id": 2, "emoji": "👍", ... } }
削除時: { "action": "removed", "emoji": "👍" }。WebSocket で reaction イベントが配信される。
チャットの新着メッセージ・編集・削除・リアクション、およびカレンダー(タスク/イベント)更新をリアルタイムで受け取るために WebSocket を利用する。
- URL:
GET /api/ws?token=<JWT> - プロトコル: 通常の HTTP から WebSocket へアップグレード。クエリ
tokenに JWT を渡す(Android ではログイン済みの Bearer トークンと同じ値)。 - トークン未指定・無効の場合は 401 JSON
{ "error": "token required" }または{ "error": "invalid token" }。
JSON 1行で送る。共通フォーマット:
{
"type": "join",
"channel_id": 1,
"event_id": 0,
"user_name": ""
}| type | 説明 | 必要なフィールド |
|---|---|---|
| join | チャンネルに参加(メッセージ配信を受け取る) | channel_id |
| leave | チャンネルから離脱 | channel_id |
| join_calendar | イベントのカレンダー更新を受け取る | event_id |
| leave_calendar | カレンダー購読解除 | event_id |
| typing | 入力中通知 | channel_id, user_name(任意) |
| typing_stop | 入力停止 | channel_id |
いずれも JSON テキストで1メッセージ。
共通エンベロープ(メッセージ配信)
{
"type": "message",
"message": { ... }
}type: "message"のとき、messageは新規投稿された Message オブジェクト(id, channel_id, user_id, content, user など)。
汎用イベント
{
"type": "message_updated",
"payload": { ... }
}{
"type": "message_deleted",
"payload": { "message_id": 1, "channel_id": 1 }
}{
"type": "reaction",
"payload": { "message_id": 1, "channel_id": 1, "user_id": 2, "emoji": "👍", "action": "remove" }
}- 追加時は
payloadに Reaction オブジェクトが入る場合あり。
タイピング
{
"type": "typing",
"payload": {
"user_id": 1,
"user_name": "山田",
"typing": true
}
}カレンダー更新
{
"type": "calendar_update",
"payload": {}
}- イベントのタスクやイベント情報が更新されたときに、そのイベントを
join_calendarしているクライアントに送られる。中身は空なので、アプリ側で必要なら該当イベントのデータを再取得する。
エラー
{
"type": "error",
"error": "channel_id required"
}- プレフィックス:
/api/admin - 認証: ヘッダー
X-Admin-KeyまたはAuthorization: Bearer <ADMIN_API_KEY> - 主な用途: 全イベント一覧、バッチ実行(論理削除チャンネル物理削除など)
Android アプリからは通常利用しないため、ここではエンドポイントの列挙のみとする。
GET /api/admin/events— 全イベント一覧POST /api/admin/batch/run— バッチ実行
-
ベースURL
ビルドフレーバーや BuildConfig で開発/本番のベースURLを切り替えるとよい。 -
認証
- Google ログインは WebView または Chrome Custom Tabs で
/api/auth/googleを開き、リダイレクト先のauth/callback?token=xxxから JWT を取得。 - 取得した JWT は Secure な保存(例: EncryptedSharedPreferences)に格納し、各 API リクエストの
Authorization: Bearer <token>に付与する。
- Google ログインは WebView または Chrome Custom Tabs で
-
日時
- 送受信とも RFC3339 文字列。Android では
Instant/ZonedDateTimeと相互変換する。
- 送受信とも RFC3339 文字列。Android では
-
エラー処理
- レスポンスが 4xx/5xx のときは body の
errorをパースしてユーザーに表示する。
- レスポンスが 4xx/5xx のときは body の
-
WebSocket
- OkHttp の
WebSocketや Jetpack の推奨ライブラリで/api/ws?token=<JWT>に接続。 - 受信 JSON の
typeに応じて UI 更新(新着メッセージ表示・カレンダー再取得など)を行う。
- OkHttp の
- 初版: イベント・タスク・予算・ユーザー・認証・招待・通知・チャンネル・メッセージ・WebSocket を記載。