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을 발급합니다.
Path
/v1/server/tokenAuthentication
client_id와 client_secret을 form body로 전송합니다.
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
content-type | string | yes | no | application/x-www-form-urlencoded | form body endpoint에서 사용합니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Body
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
grant_type | string | yes | no | client_credentials | Server API token 발급 grant입니다. |
client_id | string | yes | no | - | Developer Portal에서 발급된 confidential Server API client id입니다. |
client_secret | string | yes | no | - | backend 환경변수에만 저장하는 secret입니다.브라우저로 전달하지 않습니다. |
scope | space-separated string | yes | no | ssafy.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
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
access_token | JWT string | yes | no | - | Server API 호출에 사용하는 bearer token입니다.aud는 ssafy-verify-server-api입니다. |
token_type | string | yes | no | Bearer | 고정값입니다. |
expires_in | number | yes | no | 기본 600, 60-3600 | 초 단위 token TTL입니다.운영 환경의 SERVER_API_TOKEN_TTL_SECONDS로 조정하며 refresh token은 없습니다. |
scope | space-separated string | yes | no | - | 실제로 발급된 Server API scope입니다. |
request_id | string | yes | no | - | 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 Code | HTTP | Cause | Action |
|---|---|---|---|
INVALID_REQUEST | 400 | form body 누락 또는 grant_type 불일치 | content-type과 필수 form field를 확인합니다. |
SERVER_TOKEN_INVALID | 401 | public client, 알 수 없는 client, client_secret 불일치 | Server API credential과 secret 환경변수를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | 요청 scope가 Server API scope가 아니거나 승인되지 않음 | Developer Portal에서 승인된 Server API scope만 요청합니다. |
CLIENT_DISABLED | 403 | client가 disabled 또는 approved가 아님 | client 상태를 확인하고 운영자에게 request_id를 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | 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을 조회합니다.
Path
/v1/ssafy-members/{sub}/profileAuthentication
Bearer Server API token.Required scope: ssafy.profile.read
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
sub | path string | yes | no | - | Verify API 또는 Server API profile 조회에서 받은 client별 pairwise subject입니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
ok | boolean | yes | no | true | 요청 성공 여부입니다. |
data.sub | string | yes | no | - | client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다. |
data.updated_at | ISO 8601 string | yes | no | - | SSAFY profile snapshot의 최종 업데이트 시각입니다. |
data.ssafy_verified | boolean | scope-gated | no | true | false | `ssafy.verify` scope가 승인된 경우 포함됩니다. |
data.ssafy_mattermost_user_id | string | scope-gated | no | - | `ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다. |
data.username | string | scope-gated | no | - | Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다. |
data.name | string | scope-gated | no | - | `ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다. |
data.picture | URL string | scope-gated | yes | - | `ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다. |
data.ssafy_cohort | string | scope-gated | yes | 0 | 14 | 15 | 16 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다. |
data.ssafy_campus | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다. |
data.ssafy_region | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다. |
data.ssafy_track | string | scope-gated | yes | canonical slug | null | `ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다. |
data.ssafy_track_name | string | scope-gated | yes | free-form display label | null | `ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다. |
data.ssafy_role | string | scope-gated | no | member | owner | admin | auditor | `ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다. |
data.ssafy_role_name | string | scope-gated | yes | free-form display label | 표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다. |
data.ssafy_is_staff | boolean | scope-gated | no | true | false | 운영진/스태프성 역할 여부입니다. |
request_id | string | yes | no | - | 성공 응답에도 포함되는 request id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
ssafy_campus | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 또는 null만 반환합니다. |
ssafy_cohort | 0 | 14 | 15 | 16 또는 null만 반환합니다. |
ssafy_track | `ssafy.track` 승인 시 canonical slug 또는 null을 반환합니다. |
ssafy_role | member | 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
PROFILE_NOT_FOUND | 404 | sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음 | error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다. |
INVALID_REQUEST | 400 | username 또는 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을 조회합니다.
Path
/v1/mattermost-users/{id}/profileAuthentication
Bearer Server API token.Required scope: ssafy.profile.read ssafy.directory.lookup
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
id | path string | yes | no | - | Mattermost raw user object의 id입니다.username이 아닙니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
ok | boolean | yes | no | true | 요청 성공 여부입니다. |
data.sub | string | yes | no | - | client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다. |
data.updated_at | ISO 8601 string | yes | no | - | SSAFY profile snapshot의 최종 업데이트 시각입니다. |
data.ssafy_verified | boolean | scope-gated | no | true | false | `ssafy.verify` scope가 승인된 경우 포함됩니다. |
data.ssafy_mattermost_user_id | string | scope-gated | no | - | `ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다. |
data.username | string | scope-gated | no | - | Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다. |
data.name | string | scope-gated | no | - | `ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다. |
data.picture | URL string | scope-gated | yes | - | `ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다. |
data.ssafy_cohort | string | scope-gated | yes | 0 | 14 | 15 | 16 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다. |
data.ssafy_campus | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다. |
data.ssafy_region | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다. |
data.ssafy_track | string | scope-gated | yes | canonical slug | null | `ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다. |
data.ssafy_track_name | string | scope-gated | yes | free-form display label | null | `ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다. |
data.ssafy_role | string | scope-gated | no | member | owner | admin | auditor | `ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다. |
data.ssafy_role_name | string | scope-gated | yes | free-form display label | 표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다. |
data.ssafy_is_staff | boolean | scope-gated | no | true | false | 운영진/스태프성 역할 여부입니다. |
request_id | string | yes | no | - | 성공 응답에도 포함되는 request id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
mattermost user id | stable 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
PROFILE_NOT_FOUND | 404 | sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음 | error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다. |
INVALID_REQUEST | 400 | username 또는 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를 한 번에 조회해 백필 단건 호출을 줄입니다.
Path
/v1/mattermost-users/profiles/batchAuthentication
Bearer Server API token.Required scope: ssafy.profile.read ssafy.directory.lookup
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
content-type | string | yes | no | application/json | JSON request body를 전송할 때 사용합니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Body
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
mattermost_user_ids | array<string> | yes | no | 1-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
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.requested_count | number | yes | no | - | 요청한 Mattermost user id 개수입니다. |
data.found_count | number | yes | no | - | 정상 profile이 반환된 개수입니다. |
data.missing_count | number | yes | no | - | PROFILE_NOT_FOUND로 분류된 개수입니다. |
data.profiles | array<object> | yes | no | - | 정상 profile snapshot 목록입니다.field filtering은 단건 profile endpoint와 같습니다. |
data.missing[] | array<object> | yes | no | - | 누락된 ID와 PROFILE_NOT_FOUND, 원인별 detail_code 목록입니다. |
request_id | string | yes | no | - | 성공 응답 request id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
found_count / missing_count | partial result를 나타냅니다.일부 ID가 누락되어도 전체 HTTP 요청은 성공할 수 있습니다. |
detail_code | MATTERMOST_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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
INVALID_REQUEST | 400 | body가 JSON이 아니거나 ID 목록이 비어 있거나 중복/형식 오류 | 1-500개의 고유한 Mattermost user id를 JSON으로 전송합니다. |
PROFILE_NOT_FOUND | 404 | 같은 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 판정 결과를 조회합니다.
Path
/v1/mattermost-users/lifecycle/batchAuthentication
Bearer Server API token.Required scope: ssafy.profile.read ssafy.directory.lookup
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
content-type | string | yes | no | application/json | JSON request body를 전송할 때 사용합니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Body
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
mattermost_user_ids | array<string> | yes | no | 1-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
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.requested_count | number | yes | no | - | 요청한 Mattermost user id 개수입니다. |
data.results[] | array<object> | yes | no | - | ID별 lifecycle 판정 결과입니다.username fallback이나 상태 변경을 수행하지 않습니다. |
data.results[].mm_user_id | string | yes | no | - | 요청으로 받은 stable Mattermost user id입니다. |
data.results[].username | string | yes | yes | - | Verify에 저장된 기존 username입니다.lookup key가 아니며 매핑이 없으면 null입니다. |
data.results[].member_type | string | yes | yes | student | staff | null | 기존 profile에서 판정한 회원 유형입니다.profile이 없으면 null입니다. |
data.results[].lifecycle_status | string | yes | no | active | graduated | departed | unresolved | partner가 최종 상태를 반영하기 전에 확인해야 하는 lifecycle 판정입니다. |
data.results[].detail_code | string | yes | no | ACTIVE | USER_INACTIVE | MATTERMOST_ID_NOT_MAPPED | IDENTITY_USER_MISMATCH | PROFILE_RECORD_NOT_FOUND | USER_AUTH_CLIENT_NOT_APPROVED | lifecycle 판정의 안정적인 진단 코드입니다. |
data.results[].effective_at | ISO 8601 string | yes | no | - | Verify가 해당 결과를 평가한 시각입니다. |
data.results[].request_id | string | yes | no | - | 해당 lifecycle 평가를 추적할 provider request id입니다. |
request_id | string | yes | no | - | 성공 응답 request id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
graduated | student profile에서 USER_INACTIVE가 확정된 경우입니다. |
departed | staff profile에서 USER_INACTIVE가 확정된 경우입니다. |
MATTERMOST_ID_NOT_MAPPED | Verify profile이 없는 ID 진단입니다.member_type은 null이며, HTTP 2xx일 때 partner가 자체 exact mm_user_id + is_staff 조회로만 후속 판정할 수 있습니다. |
unresolved | Verify에서 확정할 수 없는 상태입니다.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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | 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입니다.
Path
/v1/mattermost-users?username={username}&cohort={cohort}Authentication
Bearer Server API token.Required scope: ssafy.directory.lookup
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
username | query string | yes | no | 3-64 chars, [a-zA-Z0-9._-]+ | Mattermost username입니다.변경 가능하므로 영구 매핑 키로 쓰지 않습니다. |
cohort | query string | yes | no | 0 | 14 | 15 | 16 | Mattermost 기수 context입니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
ok | boolean | yes | no | true | 요청 성공 여부입니다. |
data.sub | string | yes | no | - | client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다. |
data.updated_at | ISO 8601 string | yes | no | - | SSAFY profile snapshot의 최종 업데이트 시각입니다. |
data.ssafy_verified | boolean | scope-gated | no | true | false | `ssafy.verify` scope가 승인된 경우 포함됩니다. |
data.ssafy_mattermost_user_id | string | scope-gated | no | - | `ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다. |
data.username | string | scope-gated | no | - | Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다. |
data.name | string | scope-gated | no | - | `ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다. |
data.picture | URL string | scope-gated | yes | - | `ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다. |
data.ssafy_cohort | string | scope-gated | yes | 0 | 14 | 15 | 16 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다. |
data.ssafy_campus | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다. |
data.ssafy_region | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다. |
data.ssafy_track | string | scope-gated | yes | canonical slug | null | `ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다. |
data.ssafy_track_name | string | scope-gated | yes | free-form display label | null | `ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다. |
data.ssafy_role | string | scope-gated | no | member | owner | admin | auditor | `ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다. |
data.ssafy_role_name | string | scope-gated | yes | free-form display label | 표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다. |
data.ssafy_is_staff | boolean | scope-gated | no | true | false | 운영진/스태프성 역할 여부입니다. |
request_id | string | yes | no | - | 성공 응답에도 포함되는 request id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
username | 3-64자 영문, 숫자, 점, 밑줄, 하이픈만 허용합니다. |
cohort | 0 | 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
PROFILE_NOT_FOUND | 404 | sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음 | error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다. |
INVALID_REQUEST | 400 | username 또는 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을 반환합니다.
Path
/v1/mattermost-users/{id}/syncAuthentication
Bearer Server API token.Required scope: ssafy.profile.sync ssafy.directory.lookup
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
id | path string | yes | no | - | Mattermost provider user id입니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
ok | boolean | yes | no | true | 요청 성공 여부입니다. |
data.sub | string | yes | no | - | client별 pairwise subject입니다.앱 간 추적에 쓰지 않습니다. |
data.updated_at | ISO 8601 string | yes | no | - | SSAFY profile snapshot의 최종 업데이트 시각입니다. |
data.ssafy_verified | boolean | scope-gated | no | true | false | `ssafy.verify` scope가 승인된 경우 포함됩니다. |
data.ssafy_mattermost_user_id | string | scope-gated | no | - | `ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다. |
data.username | string | scope-gated | no | - | Mattermost username입니다.변경 가능한 보조 표시값이며 매핑 키로 쓰지 않습니다. |
data.name | string | scope-gated | no | - | `ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다. |
data.picture | URL string | scope-gated | yes | - | `ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다. |
data.ssafy_cohort | string | scope-gated | yes | 0 | 14 | 15 | 16 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다. |
data.ssafy_campus | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다. |
data.ssafy_region | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | `ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다. |
data.ssafy_track | string | scope-gated | yes | canonical slug | null | `ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다. |
data.ssafy_track_name | string | scope-gated | yes | free-form display label | null | `ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다. |
data.ssafy_role | string | scope-gated | no | member | owner | admin | auditor | `ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다. |
data.ssafy_role_name | string | scope-gated | yes | free-form display label | 표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다. |
data.ssafy_is_staff | boolean | scope-gated | no | true | false | 운영진/스태프성 역할 여부입니다. |
request_id | string | yes | no | - | 성공 응답에도 포함되는 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
PROFILE_NOT_FOUND | 404 | sub, Mattermost user id, username/cohort 조합에 해당하는 활성 프로필 또는 approved User Auth client를 찾을 수 없음 | error.detail_code로 매핑/비활성/프로필/권한 원인을 구분하고 User Auth client claim scope, App 연결, 상태를 확인합니다. |
INVALID_REQUEST | 400 | username 또는 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 기반으로 조회합니다.
Path
/v1/profile-events?cursor={cursor}&limit={limit}Authentication
Bearer Server API token.Required scope: ssafy.profile.read
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
cursor | query string | no | yes | - | 이전 응답의 next_cursor입니다.없으면 최신 event부터 조회합니다. |
limit | query number | no | no | 1-100, default 50 | 조회 개수입니다.서버가 1-100 범위로 clamp합니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.events[].eventId | string | yes | no | - | profile event id입니다. |
data.events[].eventType | string | yes | no | user.profile.updated | user.affiliation.updated | user.role.updated | user.mattermost.disabled | user.mattermost.deleted | event type입니다. |
data.events[].payload | object | yes | no | - | raw provider response가 아닌 최소 event payload입니다. |
data.next_cursor | string | yes | yes | - | 다음 페이지 조회에 사용할 cursor입니다. |
request_id | string | yes | no | - | 성공 응답 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | 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 단건 발송을 요청합니다.
Path
/v1/notifications/mattermostAuthentication
Bearer Server API token.Required scope: ssafy.notify.mattermost.send
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
content-type | string | yes | no | application/json | JSON request body를 전송할 때 사용합니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Body
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
idempotency_key | string | yes | no | 8-200 chars | client별 중복 발송 방지 키입니다.같은 payload 재요청은 기존 결과를 반환합니다. |
campaign_id | string | yes | no | 1-120 chars | campaign 조회 기준입니다. |
purpose | string | yes | no | [a-z0-9_.-]+ | Admin 승인 template purpose입니다. |
target.ssafy_mattermost_user_id | string | one-of | no | - | Mattermost provider user id입니다.target.sub와 동시에 보낼 수 없습니다. |
target.sub | string | one-of | no | - | client별 pairwise subject입니다.target.ssafy_mattermost_user_id와 동시에 보낼 수 없습니다. |
template.id | string | yes | no | [a-z0-9_.-]+ | Admin이 승인한 template id입니다. |
template.variables | object<string,string> | no | no | - | 승인된 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
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.notification_id | string | yes | no | - | notification delivery id입니다. |
data.idempotency_key | string | yes | no | - | 요청의 idempotency key입니다. |
data.status | string | yes | no | queued | sent | retrying | failed | skipped | 즉시 발송 또는 retry queue 반영 후 상태입니다. |
data.request_id | string | yes | no | - | 발송 처리 request id입니다. |
request_id | string | yes | no | - | envelope request id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
queued | delivery row가 생성되었고 아직 발송되지 않았습니다. |
sent | Mattermost DM 발송이 성공했습니다. |
retrying | retryable 실패 후 worker 재시도 대상입니다. |
failed | 재시도할 수 없거나 retry schedule을 모두 소진했습니다. |
skipped | client 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
INVALID_REQUEST | 400 | body schema 오류, target one-of 위반, 승인되지 않은 template variable | request body field와 allowedVariables를 확인합니다. |
NOTIFICATION_TEMPLATE_NOT_APPROVED | 403 | template.id와 purpose 조합이 승인되지 않음 | Developer Portal에서 template 요청 상태를 확인하고, Admin Console에서 approved 상태인지 확인합니다. |
NOTIFICATION_TARGET_NOT_FOUND | 404 | sub 또는 Mattermost user id에 해당하는 대상자를 찾을 수 없음 | 대상 식별자와 client별 subject 매핑을 확인합니다. |
IDEMPOTENCY_CONFLICT | 409 | 같은 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로 요청합니다.
Path
/v1/notifications/mattermost/batchAuthentication
Bearer Server API token.Required scope: ssafy.notify.mattermost.send
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
content-type | string | yes | no | application/json | JSON request body를 전송할 때 사용합니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Body
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
campaign_id | string | yes | no | 1-120 chars | campaign 조회 기준입니다. |
purpose | string | yes | no | [a-z0-9_.-]+ | Admin 승인 template purpose입니다. |
template | object | yes | no | - | 단건 발송과 같은 template.id, template.variables 구조입니다. |
targets[] | array | yes | no | 1-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
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.campaign_id | string | yes | no | - | 요청 campaign id입니다. |
data.summary.total | number | yes | no | - | batch target 수입니다. |
data.summary.queued/sent/retrying/failed/skipped | number | yes | no | - | status별 결과 수입니다. |
data.results[].idempotency_key | string | yes | no | - | target별 idempotency key입니다. |
data.results[].notification_id | string | success only | no | - | target별 notification id입니다. |
data.results[].status | string | yes | no | queued | sent | retrying | failed | skipped | target별 처리 상태입니다.partial success를 허용합니다. |
data.results[].error | object | failed only | no | - | target 단위 실패의 code, message, request_id입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
INVALID_REQUEST | 400 | body schema 오류, batch target 수 초과, idempotency_key 중복 | request body와 target별 idempotency_key를 확인합니다. |
NOTIFICATION_TEMPLATE_NOT_APPROVED | 403 | template.id와 purpose 조합이 승인되지 않음 | Developer Portal에서 template 요청 상태를 확인하고, Admin Console에서 approved 상태인지 확인합니다. |
NOTIFICATION_TARGET_NOT_FOUND | 404 | 대상자를 찾을 수 없음 | sub 또는 Mattermost user id 매핑을 확인합니다. |
IDEMPOTENCY_CONFLICT | 409 | 같은 idempotency_key로 다른 payload를 전송 | 새 idempotency_key를 사용합니다. |
Security Notes
- batch 내부 idempotency_key 중복은 INVALID_REQUEST입니다.
- 각 target은 서로 독립적으로 처리되어 partial success가 발생할 수 있습니다.
- 한 번에 25명을 초과해 보내지 않습니다.
Notification status
/v1/notifications/{notification_id}
notification delivery 단건 상태를 조회합니다.
Path
/v1/notifications/{notification_id}Authentication
Bearer Server API token.Required scope: ssafy.notify.mattermost.status
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
notification_id | path string | yes | no | - | 단건 또는 batch 발송 응답에서 받은 notification id입니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.notificationId | string | yes | no | - | notification delivery id입니다. |
data.campaignId | string | yes | no | - | campaign id입니다. |
data.idempotencyKey | string | yes | no | - | client별 idempotency key입니다. |
data.status | string | yes | no | queued | sent | retrying | failed | skipped | 현재 delivery status입니다. |
data.attempts | number | yes | no | - | 발송 시도 횟수입니다. |
data.nextRetryAt | ISO 8601 string | yes | yes | - | 다음 retry 예정 시각입니다.없으면 null입니다. |
data.lastErrorCode | string | yes | yes | - | 마지막 실패 error code입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
queued | delivery row가 생성되었고 아직 발송되지 않았습니다. |
sent | Mattermost DM 발송 성공입니다. |
retrying | retry schedule에 따라 worker가 다시 시도합니다. |
failed | 더 이상 retry하지 않는 실패 상태입니다. |
skipped | client 비활성화 등 정책상 발송하지 않은 상태입니다. |
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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
NOTIFICATION_TARGET_NOT_FOUND | 404 | 해당 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건까지 조회합니다.
Path
/v1/notifications?campaign_id={campaign_id}Authentication
Bearer Server API token.Required scope: ssafy.notify.mattermost.status
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
Authorization | string | yes | no | Bearer {server_api_access_token} | /v1/server/token에서 받은 Server API access token입니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Parameters
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
campaign_id | query string | yes | no | - | 발송 요청에 사용한 campaign id입니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
data.notifications[] | array<NotificationDelivery> | yes | no | - | 최근 100건까지의 delivery 목록입니다. |
data.notifications[].notificationId | string | yes | no | - | notification delivery id입니다. |
data.notifications[].campaignId | string | yes | no | - | campaign id입니다. |
data.notifications[].idempotencyKey | string | yes | no | - | client별 idempotency key입니다. |
data.notifications[].status | string | yes | no | queued | sent | retrying | failed | skipped | 현재 delivery status입니다. |
data.notifications[].attempts | number | yes | no | - | 발송 시도 횟수입니다. |
data.notifications[].nextRetryAt | ISO 8601 string | yes | yes | - | 다음 retry 예정 시각입니다.없으면 null입니다. |
data.notifications[].lastErrorCode | string | yes | yes | - | 마지막 실패 error code입니다. |
CODE / Mean
| CODE | Mean |
|---|---|
queued | delivery row가 생성되었고 아직 발송되지 않았습니다. |
sent | Mattermost DM 발송 성공입니다. |
retrying | retry schedule에 따라 worker가 다시 시도합니다. |
failed | 더 이상 retry하지 않는 실패 상태입니다. |
skipped | client 비활성화 등 정책상 발송하지 않은 상태입니다. |
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 Code | HTTP | Cause | Action |
|---|---|---|---|
SERVER_TOKEN_INVALID | 401 | Bearer token 누락, 만료, aud/token_use 불일치, 잘못된 client_credentials token | /v1/server/token으로 새 token을 발급하고 Authorization header를 확인합니다. |
SERVER_SCOPE_NOT_ALLOWED | 403 | token scope 또는 client 승인 scope가 endpoint 요구 scope를 만족하지 않음 | Developer Portal의 Server API credential 승인 scope와 요청 scope를 맞춥니다. |
CLIENT_DISABLED | 403 | Server API credential 또는 owning App이 approved 상태가 아님 | App, User Auth client, Server API credential 상태와 request_id를 운영자에게 전달합니다. |
RATE_LIMITED | 429 | client 또는 IP 기준 단기 호출 한도 초과 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id를 운영 문의에 전달합니다. |
DATABASE_UNAVAILABLE | 503 | 일시적인 저장소 접근 실패 | Retry-After 초만큼 기다린 뒤 재시도하고, x-request-id와 request_id로 운영 로그를 확인합니다. |
INVALID_REQUEST | 400 | campaign_id query 누락 | campaign_id를 query string으로 전달합니다. |
Security Notes
- client 자기 campaign delivery만 조회합니다.
- 최근 100건까지만 반환합니다.
- delivery metadata는 status, attempts, post id/checksum, error code 수준으로 제한됩니다.