Server API v1

Server API 요청/응답 레퍼런스

Partner backend가 SSAFY profile, directory, Mattermost notification을 안전하게 위임하기 위한 confidential API입니다.각 endpoint는 쿠팡식 Path, Request Parameters, Response Message, Error Spec 순서로 정리되어 있습니다.

App credential 모델

하나의 서비스는 App 하나로 관리하고 User Auth client와 Server API credential을 분리합니다.

  • User Auth client: Public PKCE.Hosted SDK popup과 redirect_uri에 사용합니다.
  • Server API credential: Confidential.client_secret은 backend 환경변수에만 저장합니다.
  • 두 credential은 같은 App 아래에 있지만 client_id, secret, scope, redirect 설정은 분리됩니다.

Server API scopes

Endpoint 접근 권한과 profile field 반환 권한은 분리됩니다.

ssafy.profile.read?
profile snapshot 조회
ssafy.profile.sync?
profile sync 요청
ssafy.directory.lookup?
directory lookup
ssafy.notify.mattermost.send?
Mattermost DM 발송
ssafy.notify.mattermost.status?
notification 상태 조회

Field filtering

  • profile endpoint 접근에는 Server API scope가 필요합니다.
  • 반환 profile field는 같은 App의 approved Public PKCE User Auth client에 승인된 claim scope 기준입니다.
  • 이름은 ssafy.name, 이미지는 ssafy.profile_image, Mattermost provider user id는 ssafy.mattermost_id가 User Auth client에 승인되어야 응답에 포함됩니다.
  • /v1/ssafy-members/{sub}/profile의 sub는 User Auth client 기준 pairwise subject입니다.
  • 승인되지 않은 field는 null이 아니라 field 자체가 생략됩니다.

Credential status

App, User Auth client, Server API credential 상태가 서로 다른 영역을 차단합니다.

approved / approved / approved?
Server API token 발급 가능, profile field 반환
approved / pending User Auth / approved Server API?
profile subject와 claim field 기준 없음
pending 또는 disabled App?
Server API token 발급과 resource request 차단
pending 또는 disabled Server API credential?
Server API token 발급과 resource request 차단

Token quota and backfill

토큰 재사용, client별 분당 한도, batch profile 백필 정책을 확인합니다.

Token TTL?
기본 600초
Token quota?
client별 30회/분
Backfill allowlist?
SERVER_API_BACKFILL_CLIENT_IDS
Batch profile?
최대 500개 ID
Lifecycle batch?
MM User ID only
Provider request ID?
x-request-id + request_id

Profile response examples

정상 응답과 sparse 응답을 비교해 claim scope 또는 App 연결 문제를 빠르게 진단합니다.

profile-response.normal.jsonapproved User Auth claim scope 기준 정상 응답
{
  "ok": true,
  "data": {
    "sub": "sub_pairwise_subject_placeholder",
    "ssafy_verified": true,
    "ssafy_mattermost_user_id": "mattermost_user_id",
    "username": "mattermost_username",
    "name": "홍길동",
    "picture": "https://verify.myknow.xyz/api/mattermost/avatar/mattermost_user_id",
    "ssafy_cohort": "15",
    "ssafy_campus": "서울",
    "ssafy_region": "서울",
    "ssafy_track": "java-major",
    "ssafy_track_name": "자바 전공",
    "ssafy_role": "member",
    "ssafy_role_name": "교육생",
    "ssafy_is_staff": false,
    "updated_at": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}
profile-response.sparse.json가입 profile 구성에 부족한 sparse 응답
{
  "ok": true,
  "data": {
    "sub": "sub_pairwise_subject_placeholder",
    "updated_at": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}
User Auth claim scope?
필요 claim scope 승인 여부
Same App grouping?
Server API credential과 User Auth client의 App 일치
Approved status?
App, User Auth client, Server API credential 상태
User Auth subject?
{sub} 발급 기준
Server token scope?
server scope escalation 없음

PROFILE_NOT_FOUND 진단

단건 오류와 batch missing[]의 detail_code를 통해 탈퇴·비활성·권한·ID 매핑 문제를 구분합니다.

MATTERMOST_ID_NOT_MAPPED?
ID 매핑 없음
MATTERMOST_USERNAME_NOT_MAPPED?
username 매핑 없음
IDENTITY_USER_MISMATCH?
identity 연결 불일치
PROFILE_RECORD_NOT_FOUND?
profile snapshot 없음
USER_INACTIVE?
비활성 회원
USER_AUTH_CLIENT_NOT_APPROVED?
권한/subject 기준 없음
SUBJECT_NOT_MAPPED?
sub 매핑 없음

MM User ID lifecycle status

  • lifecycle 판정은 POST /v1/mattermost-users/lifecycle/batch에 stable Mattermost user id 목록을 보내며 username fallback을 수행하지 않습니다.
  • USER_INACTIVE가 확정된 student만 graduated, staff만 departed로 반환합니다.ID 매핑 누락·프로필 누락·원인 미확정은 unresolved입니다.
  • MATTERMOST_ID_NOT_MAPPED는 member_type이 null인 provider 진단입니다.HTTP 2xx 응답에서만 partner가 자체 Supabase의 exact mm_user_id와 is_staff를 확인해 student=graduated, staff=departed로 후속 판정할 수 있습니다.
  • partner는 기존 캠퍼스·기수·프로필을 유지하고 MM 인증만 비활성화하며, 교육생은 이메일 로그인 전환 대상으로 설정합니다.429·5xx·timeout·권한 오류 및 그 밖의 unresolved는 상태를 바꾸지 않습니다.
  • 모든 최종 반영은 provider request_id와 함께 partner 서버의 감사 로그를 거쳐 수행합니다.

Avatar image contract

profile.picture는 이미지 URL 계약입니다.profile JSON에 base64를 기본 포함하지 않습니다.

picture format?
URL string
URL resolution?
absolute HTTPS 우선
Access?
public-cacheable image URL
Cache?
Cache-Control + ETag
Content-Type?
image/*

Mattermost notification templates

Production 발송은 approved template/purpose 조합과 제한된 variables만 허용합니다.

template state?
pending | approved | disabled
approval key?
template.id + purpose
generic template?
approved template + limited variables
Announcement example?
partner_admin_notification
Account recovery example?
partner_account_recovery

Template variables

  • Variable은 템플릿별 파트너 선언 allowlist입니다.
  • Developer Portal의 chip 입력으로 allowed_variables를 선언합니다.
  • Body chip 버튼은 현재 커서 위치에 {{variable_name}} placeholder를 삽입합니다.
  • Server API 호출 payload의 template.variables는 승인된 key의 값만 전달합니다.
Variable source?
template-scoped partner declaration
Body placeholder?
{{variable_name}}
Runtime payload?
values only
Validation?
INVALID_REQUEST
Sanitization?
1000 chars per value
Profile claims?
not auto-injected

NOTIFICATION_TEMPLATE_NOT_APPROVED

  • Developer Portal에서 해당 Server API credential의 template 요청 상태를 확인합니다.
  • template.id와 purpose가 approved 조합과 exact match인지 확인합니다.
  • template.variables key가 allowed_variables에 포함되는지 확인합니다.
  • Admin Console에서 pending 또는 disabled 상태가 아닌지 확인합니다.
Developer Portal?
template 요청 상태
Admin Console?
승인 또는 비활성 이력
Payload exact match?
template.id, purpose, variables

Public response values

저장소 내부 값이 달라도 public API/JWT/userinfo 응답은 아래 canonical vocabulary만 반환합니다.

ssafy_campus / ssafy_region?
서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null
ssafy_cohort?
0 | 14 | 15 | 16 | null
ssafy_role?
member | owner | admin | auditor
notification status?
queued | sent | retrying | failed | skipped

Endpoint index

Server API token

/v1/server/token

approved confidential Server API credential로 구성 가능한 TTL의 access token을 발급합니다.

POST

Path

/v1/server/token

Authentication

client_id와 client_secret을 form body로 전송합니다.

Headers

NameTypeRequiredNullableValuesDescription
content-typestringyesnoapplication/x-www-form-urlencodedform body endpoint에서 사용합니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Body

NameTypeRequiredNullableValuesDescription
grant_typestringyesnoclient_credentialsServer API token 발급 grant입니다.
client_idstringyesno-Developer Portal에서 발급된 confidential Server API client id입니다.
client_secretstringyesno-backend 환경변수에만 저장하는 secret입니다.브라우저로 전달하지 않습니다.
scopespace-separated stringyesnossafy.profile.read | ssafy.profile.sync | ssafy.directory.lookup | ssafy.notify.mattermost.send | ssafy.notify.mattermost.status승인된 Server API scope만 요청할 수 있습니다.
server-token.shtoken 발급 요청
curl -X POST https://verify.myknow.xyz/v1/server/token \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode grant_type=client_credentials \
  --data-urlencode client_id=client_example_confidential \
  --data-urlencode client_secret=$SSAFY_VERIFY_CLIENT_SECRET \
  --data-urlencode 'scope=ssafy.profile.read ssafy.directory.lookup'

Response Message

NameTypeRequiredNullableValuesDescription
access_tokenJWT stringyesno-Server API 호출에 사용하는 bearer token입니다.aud는 ssafy-verify-server-api입니다.
token_typestringyesnoBearer고정값입니다.
expires_innumberyesno기본 600, 60-3600초 단위 token TTL입니다.운영 환경의 SERVER_API_TOKEN_TTL_SECONDS로 조정하며 refresh token은 없습니다.
scopespace-separated stringyesno-실제로 발급된 Server API scope입니다.
request_idstringyesno-token 발급을 추적하는 provider request ID입니다.
server-token-response.jsontoken 발급 성공 응답
{
  "access_token": "server_api_jwt_placeholder",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "ssafy.profile.read ssafy.directory.lookup",
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
INVALID_REQUEST400form body 누락 또는 grant_type 불일치content-type과 필수 form field를 확인합니다.
SERVER_TOKEN_INVALID401public client, 알 수 없는 client, client_secret 불일치Server API credential과 secret 환경변수를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403요청 scope가 Server API scope가 아니거나 승인되지 않음Developer Portal에서 승인된 Server API scope만 요청합니다.
CLIENT_DISABLED403client가 disabled 또는 approved가 아님client 상태를 확인하고 운영자에게 request_id를 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.

Security Notes

  • client_secret은 backend 환경변수에만 저장합니다.
  • access_token은 기본 10분 뒤 만료되며 만료 전 같은 bearer token을 재사용할 수 있습니다.refresh token은 발급하지 않습니다.
  • client별 기본 token quota는 30회/분입니다.백필 allowlist client는 별도 quota를 사용하며, 429에는 Retry-After 헤더가 포함됩니다.
  • browser/OIDC token과 Server API token을 혼용하지 않습니다.

Profile by pairwise subject

/v1/ssafy-members/{sub}/profile

client별 pairwise subject 기준으로 정규화된 SSAFY profile snapshot을 조회합니다.

GET

Path

/v1/ssafy-members/{sub}/profile

Authentication

Bearer Server API token.Required scope: ssafy.profile.read

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
subpath stringyesno-Verify API 또는 Server API profile 조회에서 받은 client별 pairwise subject입니다.

Response Message

NameTypeRequiredNullableValuesDescription
okbooleanyesnotrue요청 성공 여부입니다.
data.substringyesno-client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다.
data.updated_atISO 8601 stringyesno-SSAFY profile snapshot의 최종 업데이트 시각입니다.
data.ssafy_verifiedbooleanscope-gatednotrue | false`ssafy.verify` scope가 승인된 경우 포함됩니다.
data.ssafy_mattermost_user_idstringscope-gatedno-`ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다.
data.usernamestringscope-gatedno-Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다.
data.namestringscope-gatedno-`ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다.
data.pictureURL stringscope-gatedyes-`ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다.
data.ssafy_cohortstringscope-gatedyes0 | 14 | 15 | 16 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다.
data.ssafy_campusstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다.
data.ssafy_regionstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다.
data.ssafy_trackstringscope-gatedyescanonical slug | null`ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다.
data.ssafy_track_namestringscope-gatedyesfree-form display label | null`ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다.
data.ssafy_rolestringscope-gatednomember | owner | admin | auditor`ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다.
data.ssafy_role_namestringscope-gatedyesfree-form display label표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다.
data.ssafy_is_staffbooleanscope-gatednotrue | false운영진/스태프성 역할 여부입니다.
request_idstringyesno-성공 응답에도 포함되는 request id입니다.

CODE / Mean

CODEMean
ssafy_campus서울 | 대전 | 광주 | 구미 | 부울경 | 창업 또는 null만 반환합니다.
ssafy_cohort0 | 14 | 15 | 16 또는 null만 반환합니다.
ssafy_track`ssafy.track` 승인 시 canonical slug 또는 null을 반환합니다.
ssafy_rolemember | owner | admin | auditor
profile-response.json프로필 조회 성공 응답
{
  "ok": true,
  "data": {
    "sub": "sub_pairwise_subject_placeholder",
    "ssafy_verified": true,
    "ssafy_mattermost_user_id": "mattermost_user_id",
    "username": "mattermost_username",
    "name": "홍길동",
    "picture": "https://verify.myknow.xyz/api/mattermost/avatar/mattermost_user_id",
    "ssafy_cohort": "15",
    "ssafy_campus": "서울",
    "ssafy_region": "서울",
    "ssafy_track": "java-major",
    "ssafy_track_name": "자바 전공",
    "ssafy_role": "member",
    "ssafy_role_name": "교육생",
    "ssafy_is_staff": false,
    "updated_at": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
PROFILE_NOT_FOUND404sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다.
INVALID_REQUEST400username 또는 cohort 형식 오류username은 3-64자 영문/숫자/점/밑줄/하이픈, cohort는 0 | 14 | 15 | 16로 보냅니다.

Security Notes

  • 응답은 같은 App의 approved User Auth client claim scope 승인 상태에 따라 field가 생략됩니다.
  • `sub`는 User Auth client 기준 pairwise subject입니다.Server API credential client_id 기준 subject로 조회하지 않습니다.
  • raw Mattermost user object는 반환하지 않습니다.
  • unknown campus/cohort 저장값은 public 응답에서 null로 반환합니다.

Profile by Mattermost user id

/v1/mattermost-users/{id}/profile

stable Mattermost provider user id 기준으로 SSAFY profile snapshot을 조회합니다.

GET

Path

/v1/mattermost-users/{id}/profile

Authentication

Bearer Server API token.Required scope: ssafy.profile.read ssafy.directory.lookup

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
idpath stringyesno-Mattermost raw user object의 id입니다.username이 아닙니다.

Response Message

NameTypeRequiredNullableValuesDescription
okbooleanyesnotrue요청 성공 여부입니다.
data.substringyesno-client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다.
data.updated_atISO 8601 stringyesno-SSAFY profile snapshot의 최종 업데이트 시각입니다.
data.ssafy_verifiedbooleanscope-gatednotrue | false`ssafy.verify` scope가 승인된 경우 포함됩니다.
data.ssafy_mattermost_user_idstringscope-gatedno-`ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다.
data.usernamestringscope-gatedno-Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다.
data.namestringscope-gatedno-`ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다.
data.pictureURL stringscope-gatedyes-`ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다.
data.ssafy_cohortstringscope-gatedyes0 | 14 | 15 | 16 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다.
data.ssafy_campusstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다.
data.ssafy_regionstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다.
data.ssafy_trackstringscope-gatedyescanonical slug | null`ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다.
data.ssafy_track_namestringscope-gatedyesfree-form display label | null`ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다.
data.ssafy_rolestringscope-gatednomember | owner | admin | auditor`ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다.
data.ssafy_role_namestringscope-gatedyesfree-form display label표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다.
data.ssafy_is_staffbooleanscope-gatednotrue | false운영진/스태프성 역할 여부입니다.
request_idstringyesno-성공 응답에도 포함되는 request id입니다.

CODE / Mean

CODEMean
mattermost user idstable provider id.계정 매핑 키로 사용합니다.
username변경 가능한 보조 표시값입니다.
ssafy_campus서울 | 대전 | 광주 | 구미 | 부울경 | 창업 또는 null만 반환합니다.
mattermost-profile-response.jsonMattermost id 기준 프로필 조회 성공 응답
{
  "ok": true,
  "data": {
    "sub": "sub_pairwise_subject_placeholder",
    "ssafy_verified": true,
    "ssafy_mattermost_user_id": "mattermost_user_id",
    "username": "mattermost_username",
    "name": "홍길동",
    "picture": "https://verify.myknow.xyz/api/mattermost/avatar/mattermost_user_id",
    "ssafy_cohort": "15",
    "ssafy_campus": "서울",
    "ssafy_region": "서울",
    "ssafy_track": "java-major",
    "ssafy_track_name": "자바 전공",
    "ssafy_role": "member",
    "ssafy_role_name": "교육생",
    "ssafy_is_staff": false,
    "updated_at": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
PROFILE_NOT_FOUND404sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다.
INVALID_REQUEST400username 또는 cohort 형식 오류username은 3-64자 영문/숫자/점/밑줄/하이픈, cohort는 0 | 14 | 15 | 16로 보냅니다.

Security Notes

  • 반환 profile field는 같은 App의 approved User Auth client claim scope 기준입니다.
  • Mattermost id는 `ssafy.mattermost_id` scope가 승인된 경우에만 응답에 포함됩니다.
  • username은 매핑 기준으로 저장하지 않습니다.
  • raw Mattermost response와 credential은 노출하지 않습니다.

Batch profiles by Mattermost user id

/v1/mattermost-users/profiles/batch

최대 500개의 Mattermost user id를 한 번에 조회해 백필 단건 호출을 줄입니다.

POST

Path

/v1/mattermost-users/profiles/batch

Authentication

Bearer Server API token.Required scope: ssafy.profile.read ssafy.directory.lookup

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
content-typestringyesnoapplication/jsonJSON request body를 전송할 때 사용합니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Body

NameTypeRequiredNullableValuesDescription
mattermost_user_idsarray<string>yesno1-500 unique ids, [A-Za-z0-9_-]+Mattermost raw user object의 stable id 목록입니다.username 목록이 아닙니다.
profile-batch-request.jsonbatch profile 요청
{
  "mattermost_user_ids": [
    "mm_user_001",
    "mm_user_002"
  ]
}

Response Message

NameTypeRequiredNullableValuesDescription
data.requested_countnumberyesno-요청한 Mattermost user id 개수입니다.
data.found_countnumberyesno-정상 profile이 반환된 개수입니다.
data.missing_countnumberyesno-PROFILE_NOT_FOUND로 분류된 개수입니다.
data.profilesarray<object>yesno-정상 profile snapshot 목록입니다.field filtering은 단건 profile endpoint와 같습니다.
data.missing[]array<object>yesno-누락된 ID와 PROFILE_NOT_FOUND, 원인별 detail_code 목록입니다.
request_idstringyesno-성공 응답 request id입니다.

CODE / Mean

CODEMean
found_count / missing_countpartial result를 나타냅니다.일부 ID가 누락되어도 전체 HTTP 요청은 성공할 수 있습니다.
detail_codeMATTERMOST_ID_NOT_MAPPED, MATTERMOST_USERNAME_NOT_MAPPED, IDENTITY_USER_MISMATCH, PROFILE_RECORD_NOT_FOUND, USER_INACTIVE, USER_AUTH_CLIENT_NOT_APPROVED, SUBJECT_NOT_MAPPED 중 하나입니다.
profile-batch-response.jsonbatch profile 응답
{
  "ok": true,
  "data": {
    "requested_count": 2,
    "found_count": 1,
    "missing_count": 1,
    "profiles": [
      {
        "sub": "sub_pairwise_subject_placeholder",
        "ssafy_mattermost_user_id": "mm_user_001",
        "username": "member_one",
        "updated_at": "2026-06-20T00:00:00.000Z"
      }
    ],
    "missing": [
      {
        "mattermost_user_id": "mm_user_002",
        "error_code": "PROFILE_NOT_FOUND",
        "detail_code": "PROFILE_RECORD_NOT_FOUND"
      }
    ]
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
INVALID_REQUEST400body가 JSON이 아니거나 ID 목록이 비어 있거나 중복/형식 오류1-500개의 고유한 Mattermost user id를 JSON으로 전송합니다.
PROFILE_NOT_FOUND404같은 App의 approved User Auth client가 없음missing[] detail_code 또는 App/User Auth client 상태를 확인합니다.

Security Notes

  • 배치 요청은 identity, user, profile을 서버 내부 bulk query로 읽고 raw Mattermost 응답을 호출하거나 반환하지 않습니다.
  • username은 mutable helper이고 배치 입력에는 stable Mattermost user id만 사용합니다.
  • 일반 client는 10회/분, 백필 allowlist client는 60회/분 quota를 사용합니다.

Lifecycle status by Mattermost user id

/v1/mattermost-users/lifecycle/batch

username fallback 없이 Mattermost user id 목록의 lifecycle 판정 결과를 조회합니다.

POST

Path

/v1/mattermost-users/lifecycle/batch

Authentication

Bearer Server API token.Required scope: ssafy.profile.read ssafy.directory.lookup

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
content-typestringyesnoapplication/jsonJSON request body를 전송할 때 사용합니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Body

NameTypeRequiredNullableValuesDescription
mattermost_user_idsarray<string>yesno1-500 unique ids, [A-Za-z0-9_-]+stable Mattermost user id 목록입니다.username을 입력하지 않습니다.
profile-lifecycle-batch-request.jsonlifecycle batch 요청
{
  "mattermost_user_ids": [
    "mm_user_001",
    "mm_user_002"
  ]
}

Response Message

NameTypeRequiredNullableValuesDescription
data.requested_countnumberyesno-요청한 Mattermost user id 개수입니다.
data.results[]array<object>yesno-ID별 lifecycle 판정 결과입니다.username fallback이나 상태 변경을 수행하지 않습니다.
data.results[].mm_user_idstringyesno-요청으로 받은 stable Mattermost user id입니다.
data.results[].usernamestringyesyes-Verify에 저장된 기존 username입니다.lookup key가 아니며 매핑이 없으면 null입니다.
data.results[].member_typestringyesyesstudent | staff | null기존 profile에서 판정한 회원 유형입니다.profile이 없으면 null입니다.
data.results[].lifecycle_statusstringyesnoactive | graduated | departed | unresolvedpartner가 최종 상태를 반영하기 전에 확인해야 하는 lifecycle 판정입니다.
data.results[].detail_codestringyesnoACTIVE | USER_INACTIVE | MATTERMOST_ID_NOT_MAPPED | IDENTITY_USER_MISMATCH | PROFILE_RECORD_NOT_FOUND | USER_AUTH_CLIENT_NOT_APPROVEDlifecycle 판정의 안정적인 진단 코드입니다.
data.results[].effective_atISO 8601 stringyesno-Verify가 해당 결과를 평가한 시각입니다.
data.results[].request_idstringyesno-해당 lifecycle 평가를 추적할 provider request id입니다.
request_idstringyesno-성공 응답 request id입니다.

CODE / Mean

CODEMean
graduatedstudent profile에서 USER_INACTIVE가 확정된 경우입니다.
departedstaff profile에서 USER_INACTIVE가 확정된 경우입니다.
MATTERMOST_ID_NOT_MAPPEDVerify profile이 없는 ID 진단입니다.member_type은 null이며, HTTP 2xx일 때 partner가 자체 exact mm_user_id + is_staff 조회로만 후속 판정할 수 있습니다.
unresolvedVerify에서 확정할 수 없는 상태입니다.MATTERMOST_ID_NOT_MAPPED의 partner-side 후속 판정을 제외한 모든 unresolved와 non-2xx·429·5xx·timeout·권한 오류는 상태를 변경하지 않습니다.
profile-lifecycle-batch-response.jsonlifecycle batch 응답
{
  "ok": true,
  "data": {
    "requested_count": 2,
    "results": [
      {
        "mm_user_id": "mm_user_001",
        "username": "student_one",
        "member_type": "student",
        "lifecycle_status": "graduated",
        "detail_code": "USER_INACTIVE",
        "effective_at": "2026-07-15T00:00:00.000Z",
        "request_id": "req_lifecycle_placeholder"
      },
      {
        "mm_user_id": "mm_user_002",
        "username": null,
        "member_type": null,
        "lifecycle_status": "unresolved",
        "detail_code": "MATTERMOST_ID_NOT_MAPPED",
        "effective_at": "2026-07-15T00:00:00.000Z",
        "request_id": "req_lifecycle_placeholder"
      }
    ]
  },
  "request_id": "req_lifecycle_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.

Security Notes

  • 이 endpoint는 내부 profile/user/identity를 읽기만 하며 partner Supabase나 회원 lifecycle 상태를 변경하지 않습니다.
  • username lookup은 수행하지 않습니다.기존 username은 응답 표시값으로만 반환합니다.
  • MATTERMOST_ID_NOT_MAPPED는 member_type null의 진단 결과이며, partner가 자체 exact mm_user_id + is_staff 조회를 통과한 경우에만 partner-side graduated/departed로 반영합니다.
  • 그 밖의 unresolved와 모든 non-2xx 응답은 상태 변경하지 않고 request_id와 함께 감사 로그에 남깁니다.

Directory lookup by username

/v1/mattermost-users?username={username}&cohort={cohort}

Mattermost username과 cohort 조합으로 프로필을 조회하는 보조 lookup endpoint입니다.

GET

Path

/v1/mattermost-users?username={username}&cohort={cohort}

Authentication

Bearer Server API token.Required scope: ssafy.directory.lookup

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
usernamequery stringyesno3-64 chars, [a-zA-Z0-9._-]+Mattermost username입니다.변경 가능하므로 영구 매핑 키로 쓰지 않습니다.
cohortquery stringyesno0 | 14 | 15 | 16Mattermost 기수 context입니다.

Response Message

NameTypeRequiredNullableValuesDescription
okbooleanyesnotrue요청 성공 여부입니다.
data.substringyesno-client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다.
data.updated_atISO 8601 stringyesno-SSAFY profile snapshot의 최종 업데이트 시각입니다.
data.ssafy_verifiedbooleanscope-gatednotrue | false`ssafy.verify` scope가 승인된 경우 포함됩니다.
data.ssafy_mattermost_user_idstringscope-gatedno-`ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다.
data.usernamestringscope-gatedno-Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다.
data.namestringscope-gatedno-`ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다.
data.pictureURL stringscope-gatedyes-`ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다.
data.ssafy_cohortstringscope-gatedyes0 | 14 | 15 | 16 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다.
data.ssafy_campusstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다.
data.ssafy_regionstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다.
data.ssafy_trackstringscope-gatedyescanonical slug | null`ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다.
data.ssafy_track_namestringscope-gatedyesfree-form display label | null`ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다.
data.ssafy_rolestringscope-gatednomember | owner | admin | auditor`ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다.
data.ssafy_role_namestringscope-gatedyesfree-form display label표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다.
data.ssafy_is_staffbooleanscope-gatednotrue | false운영진/스태프성 역할 여부입니다.
request_idstringyesno-성공 응답에도 포함되는 request id입니다.

CODE / Mean

CODEMean
username3-64자 영문, 숫자, 점, 밑줄, 하이픈만 허용합니다.
cohort0 | 14 | 15 | 16만 지원합니다.
directory-lookup-response.jsonusername lookup 성공 응답
{
  "ok": true,
  "data": {
    "sub": "sub_pairwise_subject_placeholder",
    "ssafy_verified": true,
    "ssafy_mattermost_user_id": "mattermost_user_id",
    "username": "mattermost_username",
    "name": "홍길동",
    "picture": "https://verify.myknow.xyz/api/mattermost/avatar/mattermost_user_id",
    "ssafy_cohort": "15",
    "ssafy_campus": "서울",
    "ssafy_region": "서울",
    "ssafy_track": "java-major",
    "ssafy_track_name": "자바 전공",
    "ssafy_role": "member",
    "ssafy_role_name": "교육생",
    "ssafy_is_staff": false,
    "updated_at": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
PROFILE_NOT_FOUND404sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다.
INVALID_REQUEST400username 또는 cohort 형식 오류username은 3-64자 영문/숫자/점/밑줄/하이픈, cohort는 0 | 14 | 15 | 16로 보냅니다.

Security Notes

  • 반환 profile field는 같은 App의 approved User Auth client claim scope 기준입니다.
  • 이 endpoint는 helper입니다.stable mapping은 sub 또는 Mattermost user id를 사용합니다.
  • username/cohort/IP 기준 rate limit이 적용됩니다.
  • SQL-like 입력은 validation 단계에서 거절됩니다.

Profile sync

/v1/mattermost-users/{id}/sync

Mattermost user id 기준으로 profile sync event를 만들고 현재 profile snapshot을 반환합니다.

POST

Path

/v1/mattermost-users/{id}/sync

Authentication

Bearer Server API token.Required scope: ssafy.profile.sync ssafy.directory.lookup

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
idpath stringyesno-Mattermost provider user id입니다.

Response Message

NameTypeRequiredNullableValuesDescription
okbooleanyesnotrue요청 성공 여부입니다.
data.substringyesno-client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다.
data.updated_atISO 8601 stringyesno-SSAFY profile snapshot의 최종 업데이트 시각입니다.
data.ssafy_verifiedbooleanscope-gatednotrue | false`ssafy.verify` scope가 승인된 경우 포함됩니다.
data.ssafy_mattermost_user_idstringscope-gatedno-`ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다.
data.usernamestringscope-gatedno-Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다.
data.namestringscope-gatedno-`ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다.
data.pictureURL stringscope-gatedyes-`ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다.
data.ssafy_cohortstringscope-gatedyes0 | 14 | 15 | 16 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다.
data.ssafy_campusstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다.
data.ssafy_regionstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null`ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다.
data.ssafy_trackstringscope-gatedyescanonical slug | null`ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다.
data.ssafy_track_namestringscope-gatedyesfree-form display label | null`ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다.
data.ssafy_rolestringscope-gatednomember | owner | admin | auditor`ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다.
data.ssafy_role_namestringscope-gatedyesfree-form display label표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다.
data.ssafy_is_staffbooleanscope-gatednotrue | false운영진/스태프성 역할 여부입니다.
request_idstringyesno-성공 응답에도 포함되는 request id입니다.
profile-sync-response.jsonsync 성공 응답
{
  "ok": true,
  "data": {
    "sub": "sub_pairwise_subject_placeholder",
    "ssafy_verified": true,
    "ssafy_mattermost_user_id": "mattermost_user_id",
    "username": "mattermost_username",
    "name": "홍길동",
    "picture": "https://verify.myknow.xyz/api/mattermost/avatar/mattermost_user_id",
    "ssafy_cohort": "15",
    "ssafy_campus": "서울",
    "ssafy_region": "서울",
    "ssafy_track": "java-major",
    "ssafy_track_name": "자바 전공",
    "ssafy_role": "member",
    "ssafy_role_name": "교육생",
    "ssafy_is_staff": false,
    "updated_at": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
PROFILE_NOT_FOUND404sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다.
INVALID_REQUEST400username 또는 cohort 형식 오류username은 3-64자 영문/숫자/점/밑줄/하이픈, cohort는 0 | 14 | 15 | 16로 보냅니다.

Security Notes

  • sync는 raw Mattermost object를 반환하지 않습니다.
  • profile event payload에는 source와 Mattermost user id 수준의 최소 정보만 기록합니다.
  • 반환 profile field는 같은 App의 approved User Auth client claim scope와 public 정규화 규칙을 따릅니다.

Profile events

/v1/profile-events?cursor={cursor}&limit={limit}

profile event를 cursor 기반으로 조회합니다.

GET

Path

/v1/profile-events?cursor={cursor}&limit={limit}

Authentication

Bearer Server API token.Required scope: ssafy.profile.read

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
cursorquery stringnoyes-이전 응답의 next_cursor입니다.없으면 최신 event부터 조회합니다.
limitquery numbernono1-100, default 50조회 개수입니다.서버가 1-100 범위로 clamp합니다.

Response Message

NameTypeRequiredNullableValuesDescription
data.events[].eventIdstringyesno-profile event id입니다.
data.events[].eventTypestringyesnouser.profile.updated | user.affiliation.updated | user.role.updated | user.mattermost.disabled | user.mattermost.deletedevent type입니다.
data.events[].payloadobjectyesno-raw provider response가 아닌 최소 event payload입니다.
data.next_cursorstringyesyes-다음 페이지 조회에 사용할 cursor입니다.
request_idstringyesno-성공 응답 request id입니다.
profile-events-response.jsonprofile event 조회 응답
{
  "ok": true,
  "data": {
    "events": [
      {
        "eventId": "event_uuid",
        "userId": "user_uuid",
        "eventType": "user.profile.updated",
        "payload": {
          "source": "server_api_sync"
        },
        "createdAt": "2026-06-20T00:00:00.000Z"
      }
    ],
    "next_cursor": "2026-06-20T00:00:00.000Z"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.

Security Notes

  • payload에는 token, credential, raw Mattermost response를 넣지 않습니다.
  • cursor는 event created_at 기준입니다.
  • profile field 값이 필요한 경우 profile endpoint를 다시 조회합니다.

Mattermost notification

/v1/notifications/mattermost

Admin이 승인한 template/purpose 조합으로 Mattermost DM 단건 발송을 요청합니다.

POST

Path

/v1/notifications/mattermost

Authentication

Bearer Server API token.Required scope: ssafy.notify.mattermost.send

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
content-typestringyesnoapplication/jsonJSON request body를 전송할 때 사용합니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Body

NameTypeRequiredNullableValuesDescription
idempotency_keystringyesno8-200 charsclient별 중복 발송 방지 키입니다.같은 payload 재요청은 기존 결과를 반환합니다.
campaign_idstringyesno1-120 charscampaign 조회 기준입니다.
purposestringyesno[a-z0-9_.-]+Admin 승인 template purpose입니다.
target.ssafy_mattermost_user_idstringone-ofno-Mattermost provider user id입니다.target.sub와 동시에 보낼 수 없습니다.
target.substringone-ofno-client별 pairwise subject입니다.target.ssafy_mattermost_user_id와 동시에 보낼 수 없습니다.
template.idstringyesno[a-z0-9_.-]+Admin이 승인한 template id입니다.
template.variablesobject<string,string>nono-승인된 allowedVariables key의 값만 허용됩니다.호출 시 새 variable을 선언할 수 없고 값은 최대 1000자입니다.
notification-request.json단건 발송 요청
{
  "idempotency_key": "partner-campaign-member",
  "campaign_id": "campaign_uuid",
  "purpose": "event_reward",
  "target": {
    "ssafy_mattermost_user_id": "mattermost_user_id"
  },
  "template": {
    "id": "partner_event_reward",
    "variables": {
      "title": "이벤트 당첨 안내",
      "body": "당첨자 정보를 입력해 주세요.",
      "url": "https://partner.example.com/events/..."
    }
  }
}

Response Message

NameTypeRequiredNullableValuesDescription
data.notification_idstringyesno-notification delivery id입니다.
data.idempotency_keystringyesno-요청의 idempotency key입니다.
data.statusstringyesnoqueued | sent | retrying | failed | skipped즉시 발송 또는 retry queue 반영 후 상태입니다.
data.request_idstringyesno-발송 처리 request id입니다.
request_idstringyesno-envelope request id입니다.

CODE / Mean

CODEMean
queueddelivery row가 생성되었고 아직 발송되지 않았습니다.
sentMattermost DM 발송이 성공했습니다.
retryingretryable 실패 후 worker 재시도 대상입니다.
failed재시도할 수 없거나 retry schedule을 모두 소진했습니다.
skippedclient disabled 등 정책상 발송을 건너뛰었습니다.
notification-response.json단건 발송 응답
{
  "ok": true,
  "data": {
    "notification_id": "notify_uuid",
    "idempotency_key": "partner-campaign-member",
    "status": "sent",
    "request_id": "req_placeholder"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
INVALID_REQUEST400body schema 오류, target one-of 위반, 승인되지 않은 template variablerequest body field와 allowedVariables를 확인합니다.
NOTIFICATION_TEMPLATE_NOT_APPROVED403template.id와 purpose 조합이 승인되지 않음Developer Portal에서 template 요청 상태를 확인하고, Admin Console에서 approved 상태인지 확인합니다.
NOTIFICATION_TARGET_NOT_FOUND404sub 또는 Mattermost user id에 해당하는 대상자를 찾을 수 없음대상 식별자와 client별 subject 매핑을 확인합니다.
IDEMPOTENCY_CONFLICT409같은 idempotency_key로 다른 payload를 전송새 idempotency_key를 쓰거나 원래 payload로 재요청합니다.

Security Notes

  • raw free-form message text는 받지 않습니다.
  • 운영 공지처럼 본문이 매번 달라지는 경우에도 approved generic template과 제한된 allowedVariables를 사용합니다.
  • template variable은 profile claim이 아니며 호출 payload의 template.variables 값으로만 치환됩니다.
  • message body 원문과 raw Mattermost response는 delivery metadata에 저장하지 않습니다.
  • template variable은 control character 제거와 길이 제한을 거칩니다.

Mattermost notification batch

/v1/notifications/mattermost/batch

최대 25명에게 승인 template 기반 Mattermost DM을 batch로 요청합니다.

POST

Path

/v1/notifications/mattermost/batch

Authentication

Bearer Server API token.Required scope: ssafy.notify.mattermost.send

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
content-typestringyesnoapplication/jsonJSON request body를 전송할 때 사용합니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Body

NameTypeRequiredNullableValuesDescription
campaign_idstringyesno1-120 charscampaign 조회 기준입니다.
purposestringyesno[a-z0-9_.-]+Admin 승인 template purpose입니다.
templateobjectyesno-단건 발송과 같은 template.id, template.variables 구조입니다.
targets[]arrayyesno1-25 items각 target은 고유 idempotency_key와 target one-of 객체를 포함합니다.
notification-batch-request.jsonbatch 발송 요청
{
  "campaign_id": "campaign_uuid",
  "purpose": "event_reward",
  "template": {
    "id": "partner_event_reward",
    "variables": {
      "title": "이벤트 당첨 안내"
    }
  },
  "targets": [
    {
      "idempotency_key": "partner-campaign-member-1",
      "target": {
        "ssafy_mattermost_user_id": "mattermost_user_id_1"
      }
    },
    {
      "idempotency_key": "partner-campaign-member-2",
      "target": {
        "sub": "pairwise_subject_2"
      }
    }
  ]
}

Response Message

NameTypeRequiredNullableValuesDescription
data.campaign_idstringyesno-요청 campaign id입니다.
data.summary.totalnumberyesno-batch target 수입니다.
data.summary.queued/sent/retrying/failed/skippednumberyesno-status별 결과 수입니다.
data.results[].idempotency_keystringyesno-target별 idempotency key입니다.
data.results[].notification_idstringsuccess onlyno-target별 notification id입니다.
data.results[].statusstringyesnoqueued | sent | retrying | failed | skippedtarget별 처리 상태입니다.partial success를 허용합니다.
data.results[].errorobjectfailed onlyno-target 단위 실패의 code, message, request_id입니다.

CODE / Mean

CODEMean
partial success일부 target 실패가 있어도 HTTP 200과 target별 error를 반환합니다.
max targets한 batch 요청은 최대 25명입니다.
notification-batch-response.jsonbatch partial success 응답
{
  "ok": true,
  "data": {
    "campaign_id": "campaign_uuid",
    "summary": {
      "total": 2,
      "queued": 0,
      "sent": 1,
      "retrying": 1,
      "failed": 0,
      "skipped": 0
    },
    "results": [
      {
        "idempotency_key": "partner-campaign-member-1",
        "notification_id": "notify_1",
        "status": "sent"
      },
      {
        "idempotency_key": "partner-campaign-member-2",
        "notification_id": "notify_2",
        "status": "retrying"
      }
    ]
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
INVALID_REQUEST400body schema 오류, batch target 수 초과, idempotency_key 중복request body와 target별 idempotency_key를 확인합니다.
NOTIFICATION_TEMPLATE_NOT_APPROVED403template.id와 purpose 조합이 승인되지 않음Developer Portal에서 template 요청 상태를 확인하고, Admin Console에서 approved 상태인지 확인합니다.
NOTIFICATION_TARGET_NOT_FOUND404대상자를 찾을 수 없음sub 또는 Mattermost user id 매핑을 확인합니다.
IDEMPOTENCY_CONFLICT409같은 idempotency_key로 다른 payload를 전송새 idempotency_key를 사용합니다.

Security Notes

  • batch 내부 idempotency_key 중복은 INVALID_REQUEST입니다.
  • 각 target은 서로 독립적으로 처리되어 partial success가 발생할 수 있습니다.
  • 한 번에 25명을 초과해 보내지 않습니다.

Notification status

/v1/notifications/{notification_id}

notification delivery 단건 상태를 조회합니다.

GET

Path

/v1/notifications/{notification_id}

Authentication

Bearer Server API token.Required scope: ssafy.notify.mattermost.status

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
notification_idpath stringyesno-단건 또는 batch 발송 응답에서 받은 notification id입니다.

Response Message

NameTypeRequiredNullableValuesDescription
data.notificationIdstringyesno-notification delivery id입니다.
data.campaignIdstringyesno-campaign id입니다.
data.idempotencyKeystringyesno-client별 idempotency key입니다.
data.statusstringyesnoqueued | sent | retrying | failed | skipped현재 delivery status입니다.
data.attemptsnumberyesno-발송 시도 횟수입니다.
data.nextRetryAtISO 8601 stringyesyes-다음 retry 예정 시각입니다.없으면 null입니다.
data.lastErrorCodestringyesyes-마지막 실패 error code입니다.

CODE / Mean

CODEMean
queueddelivery row가 생성되었고 아직 발송되지 않았습니다.
sentMattermost DM 발송 성공입니다.
retryingretry schedule에 따라 worker가 다시 시도합니다.
failed더 이상 retry하지 않는 실패 상태입니다.
skippedclient 비활성화 등 정책상 발송하지 않은 상태입니다.
notification-status-response.jsonnotification 상태 조회 응답
{
  "ok": true,
  "data": {
    "notificationId": "notify_uuid",
    "clientId": "client_example_confidential",
    "campaignId": "campaign_uuid",
    "idempotencyKey": "partner-campaign-member",
    "payloadHash": "payload_hash_placeholder",
    "targetUserId": "user_uuid",
    "targetMattermostUserId": "mattermost_user_id",
    "templateId": "partner_event_reward",
    "purpose": "event_reward",
    "variables": {
      "title": "이벤트 당첨 안내"
    },
    "status": "sent",
    "attempts": 1,
    "nextRetryAt": null,
    "mattermostPostId": "post_id_placeholder",
    "mattermostPostChecksum": "checksum_placeholder",
    "lastErrorCode": null,
    "requestId": "req_placeholder",
    "createdAt": "2026-06-20T00:00:00.000Z",
    "updatedAt": "2026-06-20T00:00:03.000Z"
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
NOTIFICATION_TARGET_NOT_FOUND404해당 notification_id를 client 범위에서 찾을 수 없음notification_id와 client credential이 같은 App의 값인지 확인합니다.

Security Notes

  • client 자기 delivery만 조회할 수 있습니다.
  • message body 원문과 Mattermost raw response는 반환하지 않습니다.
  • post id와 checksum은 내부 추적용 필드로만 다룹니다.

Campaign notification status

/v1/notifications?campaign_id={campaign_id}

campaign_id 기준 notification delivery 목록을 최근 100건까지 조회합니다.

GET

Path

/v1/notifications?campaign_id={campaign_id}

Authentication

Bearer Server API token.Required scope: ssafy.notify.mattermost.status

Headers

NameTypeRequiredNullableValuesDescription
AuthorizationstringyesnoBearer {server_api_access_token}/v1/server/token에서 받은 Server API access token입니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
campaign_idquery stringyesno-발송 요청에 사용한 campaign id입니다.

Response Message

NameTypeRequiredNullableValuesDescription
data.notifications[]array<NotificationDelivery>yesno-최근 100건까지의 delivery 목록입니다.
data.notifications[].notificationIdstringyesno-notification delivery id입니다.
data.notifications[].campaignIdstringyesno-campaign id입니다.
data.notifications[].idempotencyKeystringyesno-client별 idempotency key입니다.
data.notifications[].statusstringyesnoqueued | sent | retrying | failed | skipped현재 delivery status입니다.
data.notifications[].attemptsnumberyesno-발송 시도 횟수입니다.
data.notifications[].nextRetryAtISO 8601 stringyesyes-다음 retry 예정 시각입니다.없으면 null입니다.
data.notifications[].lastErrorCodestringyesyes-마지막 실패 error code입니다.

CODE / Mean

CODEMean
queueddelivery row가 생성되었고 아직 발송되지 않았습니다.
sentMattermost DM 발송 성공입니다.
retryingretry schedule에 따라 worker가 다시 시도합니다.
failed더 이상 retry하지 않는 실패 상태입니다.
skippedclient 비활성화 등 정책상 발송하지 않은 상태입니다.
notification-campaign-response.jsoncampaign 상태 조회 응답
{
  "ok": true,
  "data": {
    "notifications": [
      {
        "notificationId": "notify_uuid",
        "clientId": "client_example_confidential",
        "campaignId": "campaign_uuid",
        "idempotencyKey": "partner-campaign-member",
        "targetMattermostUserId": "mattermost_user_id",
        "templateId": "partner_event_reward",
        "purpose": "event_reward",
        "variables": {
          "title": "이벤트 당첨 안내"
        },
        "status": "sent",
        "attempts": 1,
        "nextRetryAt": null,
        "mattermostPostId": "post_id_placeholder",
        "mattermostPostChecksum": "checksum_placeholder",
        "lastErrorCode": null,
        "requestId": "req_placeholder",
        "createdAt": "2026-06-20T00:00:00.000Z",
        "updatedAt": "2026-06-20T00:00:03.000Z"
      }
    ]
  },
  "request_id": "req_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SERVER_TOKEN_INVALID401Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token/v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다.
SERVER_SCOPE_NOT_ALLOWED403token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다.
CLIENT_DISABLED403Server API credential 또는 owning App이 approved 상태가 아님App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다.
RATE_LIMITED429client 또는 IP 기준 단기 호출 한도 초과Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다.
DATABASE_UNAVAILABLE503일시적인 저장소 접근 실패Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다.
INVALID_REQUEST400campaign_id query 누락campaign_id를 query string으로 전달합니다.

Security Notes

  • client 자기 campaign delivery만 조회합니다.
  • 최근 100건까지만 반환합니다.
  • delivery metadata는 status, attempts, post id/checksum, error code 수준으로 제한됩니다.