{
  "openapi": "3.1.0",
  "info": {
    "title": "RootMap Geo API",
    "version": "2026-08-05-staging.3",
    "summary": "位置情報ゲーム向けの周辺オブジェクト検索API",
    "description": "RootMap Geo APIの公開staging版です。project credentialはゲーム開発者のバックエンドだけで保持し、ゲームクライアントには短期player tokenだけを渡してください。stagingは検証用途であり、本番SLAの対象外です。",
    "contact": {
      "name": "RootMap"
    }
  },
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "servers": [
    {
      "url": "https://api.rootmapgeo.com",
      "description": "公開staging。検証用データだけを使用してください。"
    },
    {
      "url": "http://localhost:8787",
      "description": "ローカル開発環境"
    }
  ],
  "tags": [
    {
      "name": "System",
      "description": "APIの稼働状態"
    },
    {
      "name": "Authentication",
      "description": "ゲームバックエンドによるplayer token発行"
    },
    {
      "name": "Objects",
      "description": "ゲームオブジェクトの検索"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "APIの稼働状態を確認する",
        "tags": ["System"],
        "security": [],
        "responses": {
          "200": {
            "description": "APIが応答可能",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "status": "ok",
                  "version": "2026-08-05-staging.3"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/player-sessions": {
      "post": {
        "operationId": "createPlayerSession",
        "summary": "短期player tokenを発行する",
        "description": "ゲーム開発者のバックエンドだけが呼び出します。project credentialを15分間有効なobjects:read tokenへ交換します。",
        "tags": ["Authentication"],
        "security": [
          {
            "projectCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlayerSessionRequest"
              },
              "example": {
                "playerId": "player_01HXYZ"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "player tokenを発行した",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSessionResponse"
                },
                "example": {
                  "tokenType": "Bearer",
                  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                  "expiresIn": 900,
                  "expiresAt": "2026-08-05T12:15:00.000Z",
                  "scope": "objects:read"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/objects/nearby": {
      "get": {
        "operationId": "getNearbyObjects",
        "summary": "指定座標の周辺オブジェクトを取得する",
        "description": "player tokenのprojectに属するactiveなオブジェクトだけを、距離が近い順で返します。H3で候補を絞り、最終結果は実距離で判定します。",
        "tags": ["Objects"],
        "security": [
          {
            "playerToken": []
          }
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "検索中心の緯度",
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "example": 35.6595
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "description": "検索中心の経度",
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "example": 139.7005
          },
          {
            "name": "radius",
            "in": "query",
            "required": false,
            "description": "検索半径（メートル）",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 500
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "返すオブジェクトの最大件数",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "周辺オブジェクトと利用量",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NearbyResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "treasure_shibuya_01",
                      "kind": "treasure",
                      "name": "青い宝箱",
                      "location": {
                        "latitude": 35.6597,
                        "longitude": 139.7008
                      },
                      "distanceMeters": 34.21,
                      "properties": {
                        "rarity": "rare"
                      }
                    }
                  ],
                  "meta": {
                    "radiusMeters": 500,
                    "h3Resolution": 10,
                    "h3CellCount": 271,
                    "candidateCount": 1,
                    "resultCount": 1,
                    "rowsRead": 272,
                    "sqlDurationMs": 1.2,
                    "limit": 20,
                    "usage": {
                      "periodStart": "2026-08-01",
                      "used": 1,
                      "limit": 100000,
                      "remaining": 99999,
                      "resetsAt": "2026-09-01T00:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "422": {
            "$ref": "#/components/responses/CandidateLimitExceeded"
          },
          "429": {
            "description": "短時間のレート上限または月間利用上限に到達",
            "headers": {
              "Retry-After": {
                "description": "rate_limitedの場合の推奨待機秒数。quota_exceededでは省略されます。",
                "schema": {
                  "type": "integer",
                  "const": 60
                }
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "rateLimited": {
                    "summary": "短時間の上限",
                    "value": {
                      "error": {
                        "code": "rate_limited",
                        "message": "Too many nearby requests; retry shortly",
                        "requestId": "04a5ad45-79b1-4a96-9eac-14c1f9f63193"
                      }
                    }
                  },
                  "quotaExceeded": {
                    "summary": "月間上限",
                    "value": {
                      "error": {
                        "code": "quota_exceeded",
                        "message": "The monthly API operation quota has been reached; it resets at 2026-09-01T00:00:00.000Z",
                        "requestId": "cebc36e1-7537-4d99-b3d5-5057f8902335"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "projectCredential": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "RootMap project credential",
        "description": "rmk.<credential-id>.<random-secret> 形式。ゲームクライアントへ配布せず、開発者のバックエンドだけで保管します。"
      },
      "playerToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "POST /v1/player-sessionsで発行する15分間のobjects:read tokenです。"
      }
    },
    "headers": {
      "RequestId": {
        "description": "問い合わせや障害調査に使うリクエスト識別子",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["status", "version"],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "version": {
            "type": "string"
          }
        }
      },
      "PlayerSessionRequest": {
        "type": "object",
        "additionalProperties": true,
        "required": ["playerId"],
        "properties": {
          "playerId": {
            "type": "string",
            "minLength": 3,
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$",
            "description": "個人情報を含めない仮名のplayer ID"
          }
        }
      },
      "PlayerSessionResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["tokenType", "accessToken", "expiresIn", "expiresAt", "scope"],
        "properties": {
          "tokenType": {
            "type": "string",
            "const": "Bearer"
          },
          "accessToken": {
            "type": "string"
          },
          "expiresIn": {
            "type": "integer",
            "const": 900
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "scope": {
            "type": "string",
            "const": "objects:read"
          }
        }
      },
      "Location": {
        "type": "object",
        "additionalProperties": false,
        "required": ["latitude", "longitude"],
        "properties": {
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        }
      },
      "GameObject": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "kind", "name", "location", "distanceMeters", "properties"],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "distanceMeters": {
            "type": "number",
            "minimum": 0
          },
          "properties": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "Usage": {
        "type": "object",
        "additionalProperties": false,
        "required": ["periodStart", "used", "limit", "remaining", "resetsAt"],
        "properties": {
          "periodStart": {
            "type": "string",
            "format": "date"
          },
          "used": {
            "type": "integer",
            "minimum": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1
          },
          "remaining": {
            "type": "integer",
            "minimum": 0
          },
          "resetsAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NearbyMeta": {
        "type": "object",
        "additionalProperties": false,
        "required": ["radiusMeters", "h3Resolution", "h3CellCount", "candidateCount", "resultCount", "rowsRead", "sqlDurationMs", "limit", "usage"],
        "properties": {
          "radiusMeters": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "h3Resolution": {
            "type": "integer",
            "enum": [10, 11]
          },
          "h3CellCount": {
            "type": "integer",
            "minimum": 1
          },
          "candidateCount": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5000
          },
          "resultCount": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "rowsRead": {
            "type": "integer",
            "minimum": 0
          },
          "sqlDurationMs": {
            "type": "number",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "NearbyResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["data", "meta"],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GameObject"
            },
            "maxItems": 100
          },
          "meta": {
            "$ref": "#/components/schemas/NearbyMeta"
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "additionalProperties": false,
        "required": ["code", "message"],
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "requestId": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error"],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "入力が不正",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "認証情報がない、無効、期限切れ、または失効済み",
        "headers": {
          "WWW-Authenticate": {
            "description": "Bearer認証が必要",
            "schema": {
              "type": "string",
              "const": "Bearer realm=\"RootMap\""
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Forbidden": {
        "description": "プロジェクト停止またはscope不足",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "HTTP methodが不正",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "リクエスト本文が4 KiBを超過",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "application/json以外の本文",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "CandidateLimitExceeded": {
        "description": "周辺候補が5,000件を超過",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InternalError": {
        "description": "予期しない内部エラー",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    }
  }
}
