Skip to content

Auth Guide

보안 정보

모든 API 요청은 OAuth를 사용하여 인증되어야 합니다. 이를 통해 콘텐츠 피드 및 보호된 리소스에 대한 안전하고 토큰 기반의 접근을 보장합니다.

OAuth 요청하기

* 참고

이 튜토리얼(및 관련 문서) 전반에 걸쳐 코드 예제에서 요소, 속성 및 값을 나타내기 위해 {placeholders}를 사용합니다. 따라서 요청 및 코드에서는 이러한 placeholders를 중괄호 {} 없이 바꿔야 합니다.

1. API 접근을 위한 인증 자격 증명

OAuth를 사용하여 당사 API에 액세스하려면 다음 자격 증명이 필요합니다. 이러한 자격 증명은 온보딩 과정의 일부로 안전하게 공유됩니다.

apikey
client_id
client_secret

2. OAuth API 호출

다음 URL로 API를 요청합니다.

* 참고

이 POST 요청으로 생성된 베어러 토큰은 30분간 유효합니다.


다음 단계를 참고하세요.

  1. POST 요청에 대한 Header 설정:

Content Type 설정

Content-Type: application/x-www-form-urlencoded

  1. POST 요청에 대한 Body 값 설정:

Body 값 설정

KEYVALUEEXAMPLE
client_id{ClientID}71428704-24e5-15a2-6dc9-efe759a80743
client_secret{ClientSecret}AC5tfkn291434f9kJzY8
grant_type{client_credentials}client_credentials
  1. OAuth API 호출:

Call the OAUTH API

요청의 실제 출력(예: access_token, expires_in, token_type)을 확인하는 곳에서 액세스 토큰을 변수에 저장하여 이후 GET 요청에서 Bearer Token 인증으로 사용합니다.

3. STATS API 호출

STATS API에 GET 요청을 보내고 인증 유형을 " Bearer Token "으로 설정하세요.
요청을 보내면 인증 헤더가 자동으로 생성됩니다.
예를 들어, 위의 OAuth 응답 예시에서 BearerToken의 값은 다음과 같습니다.

jsonl
{
    "access_token": "eyJhbGciOiJFUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHdUd1ZVZIaFJ1cHI3OGRmM3F1RklHQ2c0TWd0STFxSlYzMG5vNjB2VW5rIn0.eyJleHAiOjE3NTM3Nzk0MzEsImlhdCI6MTc1Mzc3NzYzMSwianRpIjoib25ydHJvOjZlMDVjZjY1LTZkYzItNDE0ZS04OThmLTFhNWEyOTc4NjE3ZSIsImlzcyI6Imh0dHBzOi8vYXBpLnN0YXRzcGVyZm9ybS5jb20vcmVhbG1zL2FwaWd3IiwiYXVkIjoiYXBpZ3ciLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmMzYzZmQ0Zi04NGNjLTRjNzYtOWJmZi0zNjNlOTk0OWJhNjUtY2xpZW50Iiwic2lkIjoiNjg1NTBjMDYtNDQxOC00ZTU4LWIzYzQtNzcyN2U0NDUyYjUzIiwic2NvcGUiOiJ1c2VyY29uZmlnIiwidXNlcmNvbmZpZyI6ImYzNjNmZDRmLTg0Y2MtNGM3Ni05YmZmLTM2M2U5OTQ5YmE2NS1vYXV0aCJ9.AZ96uHzVFs9W5tTn0AAsEgoVrP_L_b5kgh44-M5Zi0zYRMAVuQLT5ZAjt2UY-SryNm0A2aY_0mbDX0C6q0ArCTVMAJ7W1ZED1kVq3tiWypafMV4tvNVggeh3fIXcY1H5VLCnxajLImNZV79sgdIyFgnx68JA2TM9SNuVFCN9jh4b4be0",
    "expires_in": 1800,
    "refresh_expires_in": 0,
    "token_type": "Bearer",
    "not-before-policy": 0,
    "session_state": "68550c06-4418-4e58-b3c4-7727e4452b53",
    "scope": "userconfig"
}

요청 URL Format:

https
https://{requestDomain}/{feedResource}/{feedName}/{assetId}?accept={response type}

* 참고

· {requestDomain} 에 사용되는 STATS API 는 api.statsperform.com 입니다.
· {feedResource} 에는 soccer, basketball, football 등으로 적절히 바꿔주세요.
· {feedName} 에는 events 등으로 적절히 바꿔주세요.
· {assetId} 에는 호출하려는 피드에 맞게 적절히 바꿔주세요. 이는 {EventID}와 같은 스포츠 관련 개체의 UUID일 수 있습니다. 지원되는 호출에 대한 자세한 내용은 피드 문서를 참조하십시오.

요청 예시:

이 예시에서 요청은 야구 API의 이벤트 피드에서 특정 경기 일정을 가져오는 것이므로 다음과 같은 요소들을 포함합니다.

   · https://api.statsperform.com/statsapi/v1/ ({requestDomain}을 대체)
   · /baseball (야구API : {feedResource}를 대체)
   · /mlb (리그약어: {league}를 대체)
   · /events (피드 엔드포인트: {feedName}를 대체)
   · /2718451 (이벤트 ID: {assetId}를 대체)
   · ?accept=json (Json 포맷으로 받기 위한 매개변수)

위 내용으로 조합한 요청 URL 은 다음과 같습니다.

위 URL 에 요청한 응답은 아래와 같습니다.

jsonl
{
  "status": "OK",
  "recordCount": 1,
  "startTimestamp": "2025-09-02T10:58:46.6929001Z",
  "endTimestamp": "2025-09-02T10:58:46.7397767Z",
  "timeTaken": 0.0468766,
  "apiResults": [
    {
      "sportId": 2,
      "name": "Baseball",
      "league": {
        "leagueId": 7,
        "name": "Major League Baseball",
        "abbreviation": "MLB",
        "displayName": "Major League Baseball",
        "season": {
          "season": 2024,
          "name": "2024 MLB Season",
          "isActive": false,
          "eventType": [
            {
              "eventTypeId": 0,
              "name": "Spring Training",
              "events": [
                {
                  "eventId": 2619963,
                  "startDate": [
                    {
                      "year": 2024,
                      "month": 2,
                      "date": 22,
                      "hour": 13,
                      "minute": 10,
                      "full": "2024-02-22T13:10:00",
                      "dateType": "Local"
                    },
                    {
                      "year": 2024,
                      "month": 2,
                      "date": 22,
                      "hour": 20,
                      "minute": 10,
                      "full": "2024-02-22T20:10:00",
                      "dateType": "UTC"
                    }
                  ],
                  "isTba": false,
                  "isDataConfirmed": {
                    "score": true
                  },
                  "eventStatus": {
                    "eventStatusId": 4,
                    "balls": null,
                    "strikes": null,
                    "outs": null,
                    "inning": 9,
                    "inningDivision": null,
                    "isActive": false,
                    "firstPitchDateTime": null,
                    "dateType": null,
                    "name": "Final",
                    "currentBatter": null,
                    "runnersOnBase": []
                  },
                  "isDoubleheader": false,
                  "venue": {
                    "venueId": 2782,
                    "name": "Peoria Sports Complex",
                    "city": "Peoria",
                    "attendanceInfo": {
                      "reasonId": 0,
                      "reasonDesc": "Full Audience"
                    },
                    "state": {
                      "stateId": 3,
                      "name": "Arizona",
                      "abbreviation": "AZ"
                    },
                    "country": {
                      "countryId": 1,
                      "name": "United States",
                      "abbreviation": "USA"
                    },
                    "zipCode": "85382"
                  },
                  "tvStations": [
                    {
                      "tvStationId": 581,
                      "name": "ESPN",
                      "callLetters": "ESPN",
                      "networkType": {
                        "networkTypeId": 1,
                        "name": "National"
                      },
                      "country": {
                        "countryId": 1,
                        "name": "United States",
                        "abbreviation": "USA"
                      }
                    },
                    {
                      "tvStationId": 879,
                      "name": "Time Warner Cable SportsNet LA",
                      "callLetters": "SNLA",
                      "networkType": {
                        "networkTypeId": 2,
                        "name": "Regional"
                      },
                      "country": {
                        "countryId": 1,
                        "name": "United States",
                        "abbreviation": "USA"
                      }
                    },
                    {
                      "tvStationId": 864,
                      "name": "MLB Network",
                      "callLetters": "MLBN",
                      "networkType": {
                        "networkTypeId": 1,
                        "name": "National"
                      },
                      "country": {
                        "countryId": 1,
                        "name": "United States",
                        "abbreviation": "USA"
                      }
                    }
                  ],
                  "teams": [
                    {
                      "teamId": 249,
                      "location": "San Diego",
                      "nickname": "Padres",
                      "abbreviation": "SD",
                      "teamLocationType": {
                        "teamLocationTypeId": 1,
                        "name": "home"
                      },
                      "isSplitSquad": false,
                      "pitchers": [
                        {
                          "type": {
                            "typeId": 3,
                            "name": "lose"
                          },
                          "player": {
                            "playerId": 597766,
                            "firstName": "Joe",
                            "lastName": "Musgrove"
                          },
                          "wins": 0,
                          "losses": 1
                        },
                        {
                          "type": {
                            "typeId": 1,
                            "name": "starter"
                          },
                          "player": {
                            "playerId": 597766,
                            "firstName": "Joe",
                            "lastName": "Musgrove"
                          },
                          "wins": 0,
                          "losses": 1
                        }
                      ],
                      "conference": {
                        "conferenceId": 6,
                        "name": "National League",
                        "abbreviation": "NL"
                      },
                      "linescoreTotals": {
                        "runs": 1,
                        "hits": 7,
                        "errors": 0
                      },
                      "liveState": {
                        "currentPitcher": null,
                        "nextUpBatters": []
                      },
                      "record": {
                        "wins": 0,
                        "losses": 1,
                        "percentage": ".000"
                      },
                      "isWinner": false
                    },
                    {
                      "teamId": 243,
                      "location": "Los Angeles",
                      "nickname": "Dodgers",
                      "abbreviation": "LAD",
                      "teamLocationType": {
                        "teamLocationTypeId": 2,
                        "name": "away"
                      },
                      "isSplitSquad": false,
                      "pitchers": [
                        {
                          "type": {
                            "typeId": 2,
                            "name": "win"
                          },
                          "player": {
                            "playerId": 1218087,
                            "firstName": "Gavin",
                            "lastName": "Stone"
                          },
                          "wins": 1,
                          "losses": 0,
                          "earnedRunAverage": "0.00"
                        },
                        {
                          "type": {
                            "typeId": 1,
                            "name": "starter"
                          },
                          "player": {
                            "playerId": 1218087,
                            "firstName": "Gavin",
                            "lastName": "Stone"
                          },
                          "wins": 1,
                          "losses": 0,
                          "earnedRunAverage": "0.00"
                        }
                      ],
                      "conference": {
                        "conferenceId": 6,
                        "name": "National League",
                        "abbreviation": "NL"
                      },
                      "linescoreTotals": {
                        "runs": 14,
                        "hits": 12,
                        "errors": 0
                      },
                      "liveState": {
                        "currentPitcher": null,
                        "nextUpBatters": []
                      },
                      "record": {
                        "wins": 1,
                        "losses": 0,
                        "percentage": "1.000"
                      },
                      "isWinner": true
                    }
                  ],
                  "eventConference": {
                    "conferenceId": 1,
                    "name": "Cactus League",
                    "abbreviation": "Cactus League",
                    "isInterleague": false
                  },
                  "coverageLevel": {
                    "coverageLevelId": 93,
                    "details": "Spring Training - Scores only, one update per inning",
                    "name": "Level 3"
                  }
                }
              ]
            }
          ]
        }
      }
    }
  ]
}

OAuth 예시

Javascript

javascript
// ====================================
// 설정 및 라이브러리 임포트
// ====================================
// Node 18+에서는 fetch가 내장되어 있습니다.
// (Node 18 미만이면 node-fetch 설치가 필요합니다.)

// ====================================
// OAUTH 토큰 생성기
// Bearer Token을 얻기 위한 POST 함수
// ====================================
async function getOauthToken(hostname, apikey, clientId, clientSecret) {
  const requestUrl = `https://${hostname}/realms/apigw/protocol/openid-connect/token?apikey=${apikey}`;

  const headers = {
    "Content-Type": "application/x-www-form-urlencoded",
  };

  const data = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: clientId,
    client_secret: clientSecret,
  });

  try {
    const response = await fetch(requestUrl, {
      method: "POST",
      headers,
      body: data,
    });

    // HTTP 오류(4xx 또는 5xx)가 발생하면 예외처럼 처리
    if (!response.ok) {
      const text = await response.text();
      throw new Error(`HTTP ${response.status} - ${text}`);
    }

    const responseJson = await response.json();
    const accessToken = responseJson.access_token;

    if (accessToken) {
      console.log("OAuth 토큰을 성공적으로 가져왔습니다.");
      console.log(`Access Token: ${accessToken}`);
      console.log(`Expires In: ${responseJson.expires_in} 초`);
      return accessToken;
    } else {
      console.log("오류: 응답에 'access_token'이 없습니다.");
      console.log("전체 응답:", responseJson);
      return null;
    }
  } catch (e) {
    console.log(`요청 중 오류 발생: ${e.message}`);
    return null;
  }
}

// ====================================
// (JWT) Base64URL 디코딩 유틸
// ====================================
function base64UrlDecode(str) {
  str = str.replace(/-/g, "+").replace(/_/g, "/");
  const pad = str.length % 4;
  if (pad) str += "=".repeat(4 - pad);
  return Buffer.from(str, "base64").toString("utf8");
}

// ====================================
// 서명 검증 없이 JWT 페이로드만 디코딩
// ====================================
function decodeJwtPayload(accessToken) {
  const parts = accessToken.split(".");
  if (parts.length < 2) throw new Error("잘못된 JWT 형식입니다.");
  return JSON.parse(base64UrlDecode(parts[1]));
}

// ====================================
// 토큰 남은 유효 시간
// Bearer Token의 남은 시간을 조회하는 함수
// ====================================
function getTokenTimeRemaining(accessToken) {
  try {
    // 서명 검증 없이 토큰 페이로드를 디코딩합니다.
    // 만료 시간(exp)만 빠르게 확인하기 위함입니다.
    const decodedToken = decodeJwtPayload(accessToken);

    // 'exp' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
    const expirationTimestamp = decodedToken.exp;

    if (expirationTimestamp == null) {
      // 토큰에 'exp' 클레임이 없으면 유효성 판단이 어렵습니다.
      // 안전하게 invalid로 처리합니다.
      console.log("경고: 토큰에 'exp' 클레임이 없습니다.");
      return null;
    }

    // 현재 시간(UTC 기준으로 계산하려면 Date.now()로 충분)
    const currentTimeMs = Date.now();

    // 만료 시간을 ms로 변환
    const expirationTimeMs = expirationTimestamp * 1000;

    // 남은 시간 계산(ms)
    const timeRemainingMs = expirationTimeMs - currentTimeMs;

    // 남은 시간이 양수일 때만 반환
    return timeRemainingMs > 0 ? timeRemainingMs : null;
  } catch (e) {
    // 토큰이 잘못된 형식인 경우 처리
    console.log(`오류: 유효하지 않은 토큰 - ${e.message}`);
    return null;
  }
}

// ====================================
// 토큰 생성 시간
// Bearer Token의 생성 시간을 조회하는 함수
// ====================================
function getTokenCreationTime(accessToken) {
  try {
    // 서명 검증 없이 토큰 페이로드를 디코딩합니다.
    // 생성 시간(iat) 확인용입니다.
    const decodedToken = decodeJwtPayload(accessToken);

    // 'iat' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
    const creationTimestamp = decodedToken.iat;

    if (creationTimestamp == null) {
      // 토큰에 'iat' 클레임이 없으면 생성 시간 확인 불가
      console.log("경고: 토큰에 'iat' 클레임이 없습니다.");
      return null;
    }

    // 생성 시간을 Date 객체로 변환(UTC)
    const creationTime = new Date(creationTimestamp * 1000);
    return creationTime;
  } catch (e) {
    // 토큰이 잘못된 형식인 경우 처리
    console.log(`오류: 유효하지 않은 토큰 - ${e.message}`);
    return null;
  }
}

// ====================================
// 메인 실행부
// ====================================
(async () => {
  // --- 토큰 발급을 위한 설정 ---
  const hostname = "api.statsperform.com";
  const apikey = ""; // 실제 dev apikey로 교체
  const clientId = ""; // 실제 dev client_id로 교체
  const clientSecret = ""; // 실제 dev client_secret로 교체

  // --- OAuth 토큰 발급 ---
  console.log("--- OAuth 토큰 발급 시도 ---");
  const accessToken = await getOauthToken(hostname, apikey, clientId, clientSecret);

  if (!accessToken) {
    console.log("토큰 발급 실패");
    return;
  }

  console.log("--- 토큰 생성 시간 확인 ---");
  const creationTime = getTokenCreationTime(accessToken);
  console.log("토큰 생성 시간(UTC):", creationTime ? creationTime.toISOString() : null);

  console.log("--- 토큰 유효 시간 확인 ---");
  const timeLeftValidMs = getTokenTimeRemaining(accessToken);

  if (timeLeftValidMs != null) {
    console.log(`남은 유효 시간: ${Math.floor(timeLeftValidMs / 1000)} 초`);
  } else {
    console.log("토큰이 만료되었거나 유효하지 않습니다.");
  }
})();

Python

python
# ====================================
# 설정 및 라이브러리 임포트
# ====================================
import requests
import json
import time
import jwt
from datetime import datetime, timedelta
!pip install pyjwt

# ====================================
# OAUTH 토큰 생성기
# Bearer Token을 얻기 위한 POST 함수
# ====================================
def get_oauth_token(hostname, apikey, client_id, client_secret):
    request_url = f"https://{hostname}/realms/apigw/protocol/openid-connect/token?apikey={apikey}"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret
    }
    try:
        response = requests.post(request_url, headers=headers, data=data)
        response.raise_for_status()  # HTTP 오류(4xx 또는 5xx)가 발생하면 예외 발생
        response_json = response.json()
        access_token = response_json.get('access_token')
        if access_token:
            print("Successfully retrieved OAuth token!")
            print(f"Access Token: {access_token}")
            print(f"Expires In: {response_json.get('expires_in')} seconds")
            return access_token
        else:
            print("Error: 'access_token' not found in the response.")
            print(f"Full response: {response_json}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"An error occurred during the request: {e}")
        return None

# ====================================
# 토큰 남은 유효 시간
# Bearer Token의 남은 시간을 조회하는 함수
# ====================================
def get_token_time_remaining(access_token: str) -> timedelta | None:
    """
    JWT가 만료되기까지 남은 시간을 계산합니다.
    Args:
        access_token: JWT 문자열
    Returns:
        토큰이 유효하면 남은 시간을 timedelta 객체로 반환,
        만료되었거나 유효하지 않거나 만료 정보가 없으면 None 반환
    """
    try:
        # 서명 검증 없이 토큰 페이로드를 디코딩합니다.
        # 만료 시간(exp)만 빠르게 확인하기 위함입니다.
        decoded_token = jwt.decode(access_token, options={"verify_signature": False})

        # 'exp' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
        expiration_timestamp = decoded_token.get("exp")

        if expiration_timestamp is None:
            # 토큰에 'exp' 클레임이 없으면 유효성 판단이 어렵습니다.
            # 안전하게 invalid로 처리합니다.
            print("Warning: Token has no 'exp' claim.")
            return None

        # 현재 UTC 시간 가져오기
        current_time = datetime.utcnow()

        # 만료 타임스탬프를 datetime 객체로 변환
        expiration_time = datetime.utcfromtimestamp(expiration_timestamp)

        # 남은 시간 계산
        time_remaining = expiration_time - current_time

        # 남은 시간이 양수일 때만 반환
        if time_remaining.total_seconds() > 0:
            return time_remaining
        else:
            return None

    except jwt.InvalidTokenError as e:
        # 토큰이 잘못된 형식인 경우 처리
        print(f"Error: Invalid token - {e}")
        return None
    except Exception as e:
        # 기타 예외 처리
        print(f"An unexpected error occurred: {e}")
        return None

# ====================================
# 토큰 생성 시간
# Bearer Token의 생성 시간을 조회하는 함수
# ====================================
def get_token_creation_time(access_token: str) -> timedelta | None:
    """
    토큰이 생성된 시간을 계산합니다.
    Args:
        access_token: JWT 문자열
    Returns:
        유효한 토큰일 경우 생성 시간을 반환
    """
    try:
        # 서명 검증 없이 토큰 페이로드를 디코딩합니다.
        # 생성 시간(iat) 확인용입니다.
        decoded_token = jwt.decode(access_token, options={"verify_signature": False})

        # 'iat' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
        creation_timestamp = decoded_token.get("iat") 

        if creation_timestamp is None:
            # 토큰에 'iat' 클레임이 없으면 생성 시간 확인 불가
            print("Warning: Token has no 'iat' claim.")
            return None

        # 현재 UTC 시간
        current_time = datetime.utcnow()

        # 생성 타임스탬프를 datetime 객체로 변환
        creation_time = datetime.utcfromtimestamp(creation_timestamp) 

    except jwt.InvalidTokenError as e:
        # 토큰 형식 오류 처리
        print(f"Error: Invalid token - {e}")
        return None
    except Exception as e:
        # 기타 예외 처리
        print(f"An unexpected error occurred: {e}")
        return None

    return creation_time

# ====================================
# 메인 실행부
# ====================================
if __name__ == "__main__":
    # --- 토큰 발급을 위한 설정 ---
    hostname = "api.statsperform.com"
    apikey = ""  # 실제 dev apikey로 교체
    client_id = ""  # 실제 dev client_id로 교체
    client_secret = ""  # 실제 dev client_secret로 교체

    # --- OAuth 토큰 발급 ---
    print("--- Attempting to get OAuth token ---")
    access_token = get_oauth_token(
        hostname,
        apikey,
        client_id,
        client_secret,
    )

    print("--- Checking Oauth token creation time ---")
    creation_time = get_token_creation_time(access_token)
    print(f"Token Creation Time: {creation_time}") 

    print("--- Checking Oauth token validity ---")

    # 토큰 유효 시간 확인
    time_left_valid = get_token_time_remaining(access_token)

    if time_left_valid:
        print(f"Time remaining for valid token: {time_left_valid}")
    else:
        print("Valid token has expired or is invalid.")

C#

csharp
// ====================================
// 설정 및 라이브러리 임포트
// ====================================
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;
using System.IdentityModel.Tokens.Jwt;

// ====================================
// OAUTH 토큰 생성기
// Bearer Token을 얻기 위한 POST 함수
// ====================================
static async Task<string?> GetOAuthToken(string hostname, string apikey, string clientId, string clientSecret)
{
    var requestUrl = $"https://{hostname}/realms/apigw/protocol/openid-connect/token?apikey={apikey}";

    using var http = new HttpClient();
    var content = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["grant_type"] = "client_credentials",
        ["client_id"] = clientId,
        ["client_secret"] = clientSecret
    });

    try
    {
        var response = await http.PostAsync(requestUrl, content);
        response.EnsureSuccessStatusCode(); // HTTP 오류(4xx 또는 5xx)가 발생하면 예외 발생

        var json = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);

        if (doc.RootElement.TryGetProperty("access_token", out var tokenEl))
        {
            var accessToken = tokenEl.GetString();
            Console.WriteLine("OAuth 토큰을 성공적으로 가져왔습니다.");
            Console.WriteLine($"Access Token: {accessToken}");

            if (doc.RootElement.TryGetProperty("expires_in", out var expEl))
                Console.WriteLine($"Expires In: {expEl.GetInt32()} 초");

            return accessToken;
        }
        else
        {
            Console.WriteLine("오류: 응답에 'access_token'이 없습니다.");
            Console.WriteLine($"전체 응답: {json}");
            return null;
        }
    }
    catch (Exception e)
    {
        Console.WriteLine($"요청 중 오류 발생: {e.Message}");
        return null;
    }
}

// ====================================
// 토큰 남은 유효 시간
// Bearer Token의 남은 시간을 조회하는 함수
// ====================================
static TimeSpan? GetTokenTimeRemaining(string accessToken)
{
    try
    {
        // 서명 검증 없이 토큰 페이로드를 디코딩합니다.
        // 만료 시간(exp)만 빠르게 확인하기 위함입니다.
        var handler = new JwtSecurityTokenHandler();
        var jwt = handler.ReadJwtToken(accessToken);

        // 'exp' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
        if (!jwt.Payload.TryGetValue("exp", out var expObj))
        {
            Console.WriteLine("경고: 토큰에 'exp' 클레임이 없습니다.");
            return null;
        }

        var expirationTime = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(expObj)).UtcDateTime;
        var currentTime = DateTime.UtcNow;

        var timeRemaining = expirationTime - currentTime;

        // 남은 시간이 양수일 때만 반환
        return timeRemaining.TotalSeconds > 0 ? timeRemaining : null;
    }
    catch (Exception e)
    {
        Console.WriteLine($"토큰 처리 오류: {e.Message}");
        return null;
    }
}

// ====================================
// 토큰 생성 시간
// Bearer Token의 생성 시간을 조회하는 함수
// ====================================
static DateTime? GetTokenCreationTime(string accessToken)
{
    try
    {
        // 서명 검증 없이 토큰 페이로드를 디코딩합니다.
        // 생성 시간(iat) 확인용입니다.
        var handler = new JwtSecurityTokenHandler();
        var jwt = handler.ReadJwtToken(accessToken);

        // 'iat' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
        if (!jwt.Payload.TryGetValue("iat", out var iatObj))
        {
            Console.WriteLine("경고: 토큰에 'iat' 클레임이 없습니다.");
            return null;
        }

        return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(iatObj)).UtcDateTime;
    }
    catch (Exception e)
    {
        Console.WriteLine($"토큰 처리 오류: {e.Message}");
        return null;
    }
}

// ====================================
// 메인 실행부
// ====================================
static async Task Main()
{
    var hostname = "api.statsperform.com";
    var apikey = "";        // 실제 apikey로 교체
    var clientId = "";      // 실제 client_id로 교체
    var clientSecret = "";  // 실제 client_secret로 교체

    Console.WriteLine("--- OAuth 토큰 발급 시도 ---");
    var accessToken = await GetOAuthToken(hostname, apikey, clientId, clientSecret);

    if (string.IsNullOrWhiteSpace(accessToken))
    {
        Console.WriteLine("토큰 발급 실패");
        return;
    }

    Console.WriteLine("--- 토큰 생성 시간 확인 ---");
    var creationTime = GetTokenCreationTime(accessToken);
    Console.WriteLine($"토큰 생성 시간(UTC): {creationTime}");

    Console.WriteLine("--- 토큰 유효 시간 확인 ---");
    var remaining = GetTokenTimeRemaining(accessToken);

    if (remaining != null)
        Console.WriteLine($"남은 유효 시간: {remaining}");
    else
        Console.WriteLine("토큰이 만료되었거나 유효하지 않습니다.");
}

PHP

php
<?php
// ====================================
// 설정 및 라이브러리 임포트
// ====================================
// PHP 기본 함수(curl, json_decode 등)만 사용합니다.

// ====================================
// OAUTH 토큰 생성기
// Bearer Token을 얻기 위한 POST 함수
// ====================================
function get_oauth_token($hostname, $apikey, $client_id, $client_secret) {
    $request_url = "https://{$hostname}/realms/apigw/protocol/openid-connect/token?apikey={$apikey}";
    $headers = ["Content-Type: application/x-www-form-urlencoded"];
    $data = http_build_query([
        "grant_type" => "client_credentials",
        "client_id" => $client_id,
        "client_secret" => $client_secret
    ]);

    $ch = curl_init($request_url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $data,
        CURLOPT_RETURNTRANSFER => true
    ]);

    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        echo "요청 중 오류 발생: {$err}\n";
        return null;
    }

    // HTTP 오류(4xx 또는 5xx)가 발생하면 예외처럼 처리
    if ($http_code < 200 || $http_code >= 300) {
        echo "HTTP 오류 발생: {$http_code}\n";
        echo "응답: {$response}\n";
        return null;
    }

    $response_json = json_decode($response, true);
    $access_token = $response_json["access_token"] ?? null;

    if ($access_token) {
        echo "OAuth 토큰을 성공적으로 가져왔습니다.\n";
        echo "Access Token: {$access_token}\n";
        echo "Expires In: " . ($response_json["expires_in"] ?? "unknown") . " 초\n";
        return $access_token;
    } else {
        echo "오류: 응답에 'access_token'이 없습니다.\n";
        echo "전체 응답: {$response}\n";
        return null;
    }
}

// ====================================
// (JWT) Base64URL 디코딩 유틸
// ====================================
function base64url_decode($data) {
    $data = strtr($data, '-_', '+/');
    $pad = strlen($data) % 4;
    if ($pad) $data .= str_repeat('=', 4 - $pad);
    return base64_decode($data);
}

// ====================================
// 서명 검증 없이 JWT 페이로드만 디코딩
// ====================================
function decode_jwt_payload($access_token) {
    $parts = explode('.', $access_token);
    if (count($parts) < 2) {
        throw new Exception("잘못된 JWT 형식입니다.");
    }
    $payload_json = base64url_decode($parts[1]);
    $payload = json_decode($payload_json, true);
    if (!is_array($payload)) {
        throw new Exception("JWT 페이로드 디코딩 실패");
    }
    return $payload;
}

// ====================================
// 토큰 남은 유효 시간
// Bearer Token의 남은 시간을 조회하는 함수
// ====================================
function get_token_time_remaining($access_token) {
    try {
        // 서명 검증 없이 토큰 페이로드를 디코딩합니다.
        // 만료 시간(exp)만 빠르게 확인하기 위함입니다.
        $decoded_token = decode_jwt_payload($access_token);

        // 'exp' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
        $expiration_timestamp = $decoded_token["exp"] ?? null;

        if ($expiration_timestamp === null) {
            // 토큰에 'exp' 클레임이 없으면 유효성 판단이 어렵습니다.
            // 안전하게 invalid로 처리합니다.
            echo "경고: 토큰에 'exp' 클레임이 없습니다.\n";
            return null;
        }

        // 현재 시간(Unix seconds)
        $current_time = time();

        // 남은 시간 계산(초)
        $time_remaining = $expiration_timestamp - $current_time;

        // 남은 시간이 양수일 때만 반환
        return ($time_remaining > 0) ? $time_remaining : null;
    } catch (Exception $e) {
        // 토큰이 잘못된 형식인 경우 처리
        echo "오류: 유효하지 않은 토큰 - {$e->getMessage()}\n";
        return null;
    }
}

// ====================================
// 토큰 생성 시간
// Bearer Token의 생성 시간을 조회하는 함수
// ====================================
function get_token_creation_time($access_token) {
    try {
        // 서명 검증 없이 토큰 페이로드를 디코딩합니다.
        // 생성 시간(iat) 확인용입니다.
        $decoded_token = decode_jwt_payload($access_token);

        // 'iat' 클레임은 Unix 타임스탬프(에폭 이후 초 단위)입니다.
        $creation_timestamp = $decoded_token["iat"] ?? null;

        if ($creation_timestamp === null) {
            // 토큰에 'iat' 클레임이 없으면 생성 시간 확인 불가
            echo "경고: 토큰에 'iat' 클레임이 없습니다.\n";
            return null;
        }

        // 생성 시간을 DateTimeImmutable(UTC)로 변환
        $creation_time = (new DateTimeImmutable("@{$creation_timestamp}"))->setTimezone(new DateTimeZone("UTC"));
        return $creation_time;
    } catch (Exception $e) {
        // 토큰이 잘못된 형식인 경우 처리
        echo "오류: 유효하지 않은 토큰 - {$e->getMessage()}\n";
        return null;
    }
}

// ====================================
// 메인 실행부
// ====================================

// --- 토큰 발급을 위한 설정 ---
$hostname = "api.statsperform.com";
$apikey = "";        // 실제 dev apikey로 교체
$client_id = "";     // 실제 dev client_id로 교체
$client_secret = ""; // 실제 dev client_secret로 교체

// --- OAuth 토큰 발급 ---
echo "--- OAuth 토큰 발급 시도 ---\n";
$access_token = get_oauth_token($hostname, $apikey, $client_id, $client_secret);

if (!$access_token) {
    echo "토큰 발급 실패\n";
    exit(1);
}

echo "--- 토큰 생성 시간 확인 ---\n";
$creation_time = get_token_creation_time($access_token);
echo "토큰 생성 시간(UTC): " . ($creation_time ? $creation_time->format(DateTimeInterface::ATOM) : "null") . "\n";

echo "--- 토큰 유효 시간 확인 ---\n";
$time_left_valid = get_token_time_remaining($access_token);

if ($time_left_valid !== null) {
    echo "남은 유효 시간: {$time_left_valid} 초\n";
} else {
    echo "토큰이 만료되었거나 유효하지 않습니다.\n";
}

에러 코드

HTTP 상태 코드설명
200OK
304Not modified
401Developer inactive
세션이 종료되었을 수 있습니다. 예를 들어 서명이 더 이상 유효하지 않아 새 서명이 필요하거나, I/O 문서를 사용 중인 경우 로그인 후 다시 시도해야 합니다. 모든 경우에 새 서명을 생성하고 새 호출을 하기 전에 API 키가 올바른지 확인하십시오.
401Unauthorized/Not Authorized
권한이 부족합니다. 구독 범위 외의 리소스에 액세스하려고 시도했거나, API 키 또는 시크릿이 잘못되었거나, 고유 서명이 더 이상 유효하지 않아 새 서명이 필요할 수 있습니다. API 호출에는 반드시 고유 서명을 포함해야 합니다. 서명을 하드코딩하면 만료되므로 사용하지 마십시오. 인증 가이드에 설명된 대로 애플리케이션은 API 호출에 사용할 새 서명을 정기적으로 생성해야 합니다. 모든 경우에 새 서명을 생성하고 새 API 호출을 하기 전에 API 키가 올바른지 확인하십시오. 서명은 SHA256 해시로 생성해야 합니다.
401Waiting
계정이 설정되었지만 아직 활성화되지 않았습니다. 이 오류가 계속 발생하는 경우 계정 관리자에게 문의하여 계정을 활성화하세요.
403Forbidden
접근이 금지되었습니다.
403Client exceeded queries limit
클라이언트에게 허용된 쿼리수가 초과되었습니다.
403Timestamp is invalid
요청한 타임스탬프가 잘 못 되었습니다.
403Service requires SSL
서비스 요청시 SSL이 필요합니다.
403Invalid API key CID pair
잘못된 API 키 CID 쌍입니다.
403Invalid API plan
잘못된 API 계획입니다.
403CORS origin deniedCORS 위반 에러입니다.
403Developer Over QPS
각 API 계정에는 초당 호출 횟수 제한이 있습니다. 이 제한을 초과하면 이후 API 호출은 실패하고 오류가 반환됩니다. 다음 초에 다시 호출하면 정상적으로 처리됩니다. 이 문제를 방지하는 가장 쉬운 방법은 이전 호출이 완료된 후에 다음 호출이 실행되도록 설계하는 것입니다.
403Developer Over Limit
각 API 계정에는 일일/월간 API 호출 횟수 제한이 있습니다. 제한 횟수가 초과되면 해당 제한이 해제될 때까지 API 호출이 계속 실패하고 이 오류가 발생합니다. 담당 계정 관리자에게 문의하여 도움을 받으세요.
404Data not found / Not found
사용 가능한 데이터가 없습니다(데이터가 실제로 존재하지 않을 수 있습니다). 이러한 현상은 여러 가지 이유로 발생할 수 있으며, 몇 가지 예는 다음과 같습니다.
  • 경기 일정이 없는 날에는 하루 일정을 종료합니다.
  • 특정 경기(이벤트 ID 기준)와 관련된 데이터 요청이 이벤트 ID가 유효하지 않아 실패했습니다.
404 오류를 반환하는 API 호출 자체는 문제가 되지 않지만, 404 오류를 반환하는 호출도 API 호출 허용량에 포함됩니다.
405Mehtod Not Allowed
허락되지 않은 HTTP 메서드 입니다.
500Internal server error
501Not implemented
502Bad gateway
503Service unavailable
504Gateway timeout
596Invalid preflight CORS request
Origin 헤더가 누락되었거나 요청의 HTTP 메서드가 OPTIONS가 아닙니다.
596Service Not Found
다음과 같은 상황에서 이 오류 메시지가 표시됩니다.
  • Invalid URL: URL 또는 파라미터의 대소문자를 잘못 사용했을 가능성이 높습니다. (모든 URL과 파라미터는 대소문자를 구분합니다).
    또는 유효하지 않은 엔드포인트/경로이거나, Base URL, 엔드포인트, 파라미터의 조합이 올바르지 않은 경우입니다.
    예: /v1/decode/networktypes/은 오류가 발생합니다. 올바른 경로는 /v1/decode/networkTypes 입니다.
  • Invalid URL: 엔드포인트 끝에 반드시 포함되어야 하는 마지막 /를 생략한 경우입니다. (단, eventId 또는 teamId 값을 경로 파라미터로 전달하는 선택적 메서드의 경우는 예외입니다.)