HR 명단 API
인사 시스템이 직원 명단을 보내면 회사 스페이스의 구성원을 맞춰요. 회사마다 열어 드려요.
회사 HR 시스템이 정해진 때마다 직원 명단을 보내면 스페이스의 구성원을 맞춰요. 정지와 복귀는 늘 스페이스 관리자가 확인해요. API 로 넣은 직원에게는 초대 메일이 가지 않아요.
준비#
- 스페이스가 열리면 주인이 설정 탭 「명단 API」에서 토큰을 만들어요.
- 범위는 읽기와 쓰기예요. 읽기는 상태 보기와 dry_run, 쓰기는 apply 까지예요. 쓰기는 허용 IP 를 하나 이상 적어야 해요(최대 10개 · 예: 203.0.113.10/32).
- 토큰은 만든 창에서 한 번만 보여요. HR 시스템의 비밀 저장소에 넣어 주세요.
- 스페이스마다 살아 있는 토큰은 2개까지예요(바꿀 때 겹쳐 쓰는 몫).
- 서버의 배치 작업에서 불러요. 브라우저에서는 부를 수 없어요.
주소#
| 메서드 | 주소 | 범위 | 하는 일 |
|---|---|---|---|
| POST | https://oneport.kr/api/spaces/roster/v1/runs | dry_run 은 읽기 · apply 는 쓰기 | 명단 보내기 |
| GET | https://oneport.kr/api/spaces/roster/v1/runs/{id} | 읽기 | 회차 하나 보기 |
| GET | https://oneport.kr/api/spaces/roster/v1/status | 읽기 | 스페이스 명단 상태 보기 |
- Authorization: Bearer <토큰> — 늘 필요해요.
- Content-Type: application/json — 명단을 보낼 때.
- Idempotency-Key: <8–200자> — apply 에 꼭 필요해요. 파일 내용의 해시를 권해요. 같은 키 · 같은 본문은 한 번만 맞추고 처음 응답을 다시 줘요(replayed: true).
본문#
json
{
"mode": "dry_run",
"generated_at": "2026-11-04T09:00:00+09:00",
"fields": [
"email",
"employee_id",
"team",
"team_admin"
],
"rows": [
{
"email": "kim@corp.example",
"employee_id": "E1001",
"team": "영업팀",
"team_admin": false
}
]
}요청 본문
| 파라미터 | 받는 값 | 저희가 하는 일 |
|---|---|---|
| mode필수 | dry_run · apply | dry_run 은 결과만 보여 주고 아무것도 안 바꿔요. apply 는 맞춰요. |
| generated_at필수 | 시간대가 붙은 시각(+09:00 또는 Z) | HR 시스템이 명단을 만든 시각이에요. 이미 맞춘 명단보다 오래됐으면 409 older_than_applied 로 답해요. |
| fields필수 | email · employee_id · team · team_admin | 이 명단이 쓰는 칸이에요. email 은 꼭 있어야 해요. |
| rows필수 | 줄 목록 | 지금 재직 중인 전체 명단이에요. 명단에 없는 분은 정지 후보가 되어 관리자 확인을 기다려요. |
- 한 번에 받는 줄 수는 스페이스 자리 수에 따라 정해져요(넘으면 413 too_many_rows · detail.max). 본문은 4MB 까지예요.
- employee_id 를 보내면 메일이 바뀐 직원도 같은 분으로 이어 맞춰요. 엑셀이 앞자리 0 을 지우지 않게 글자로 보내 주세요.
- 처음에는 dry_run 으로 보내 run.plan 을 확인한 뒤 같은 명단을 apply 로 보내 주세요.
- 보낸 명단은 맞추는 동안과 관리자 확인을 기다리는 동안 7일까지 보관한 뒤 지워요. dry_run 명단은 보관하지 않아요.
응답#
json
{
"ok": true,
"replayed": false,
"run": {
"id": "…", "state": "review", "mode": "apply", "rows": 1, "generated_at": "…",
"plan": { "new": 1, "same": 0, "suspend": 0 },
"applied": { "added": 0 },
"waiting": { "new": 1 },
"hold_reasons": ["api_mode_review"], "waiting_reasons": [],
"blocked": null, "errors": [], "review_url": "https://oneport.kr/…",
"followups_left": 0, "created_at": "…", "finished_at": "…"
},
"warnings": []
}응답에는 이메일 · 사번이 실리지 않아요(수만 실려요).
| run.state | 뜻 |
|---|---|
| judging | 판정 중이에요 |
| dry_run | 결과만 보여 드렸어요(바뀐 것 없음) |
| applying | 맞추는 중이에요(큰 명단은 나눠 이어 가요 · followups_left) |
| applied | 맞췄어요 |
| review | 관리자 확인을 기다려요(review_url) |
| reviewed | 관리자가 확인해 맞췄어요 |
| dismissed | 관리자가 이 명단을 버렸어요 |
| superseded | 더 새 명단이 와서 이 회차는 접혔어요 |
| expired | 확인 없이 7일이 지나 접혔어요 |
| blocked | 이 명단으로는 맞추지 않았어요(blocked) |
| failed | 끝내지 못했어요(errors) |
관리자 확인으로 넘어가는 까닭#
| hold_reasons | 뜻 |
|---|---|
| api_mode_review | 스페이스가 「확인 뒤 맞추기」예요(처음 몇 번은 이렇게 둬요) |
| stale_roster | 복귀와 정지가 함께 많아요(옛 명단 같아요) |
| domain_mismatch | 명단의 메일 도메인이 지금 구성원과 달라요 |
| shrink | 지난번 명단보다 줄이 10% 넘게 줄었어요 |
| old_file | 만든 지 72시간이 넘은 명단이에요 |
| long_gap | 오래 끊겼다가 다시 온 명단이에요 |
| id_mismatch | 사번이 맞지 않는 줄이 많아요 |
| problems_many | 손으로 정리할 줄이 많아요 |
자동으로 안 하고 기다리는 줄의 까닭#
| waiting_reasons | 뜻 |
|---|---|
| adds_over_limit | 새로 넣을 분이 한 번에 자동으로 넣는 수를 넘었어요 |
| seat_short | 자리가 모자라요(관리자가 [자리 더 열어 달라고 하기]로 요청해요) |
| adds_foreign_domain | 스페이스가 모르는 도메인의 메일이에요 |
| new_team | 아직 없는 팀이에요 |
| daily_cap | 24시간 자동 맞추기 수가 끝에 닿았어요 |
| moves_over_limit | 팀 이동이 한 번에 자동으로 옮기는 수를 넘었어요 |
| privilege_up | 팀 담당으로 올리는 줄이에요 |
| last_team_admin | 팀의 마지막 담당을 내리는 줄이에요 |
| restore_needs_review | 복귀는 늘 사람이 확인해요 |
| suspend_needs_review | 정지는 늘 사람이 확인해요 |
| rekey_joined | 이미 들어온 분의 메일이 바뀌었어요 |
| rekey_admin | 관리자의 메일이 바뀌었어요 |
| rekey_domain | 모르는 도메인으로 메일이 바뀌었어요 |
| rekey_over_limit | 메일 바뀜이 한 번에 자동으로 잇는 수를 넘었어요 |
오류#
json
{ "ok": false, "error": { "code": "bad_row", "message": "…", "detail": { "row_nos": [3], "count": 1, "row_problems": [{ "row": 3, "field": "email", "reason": "invalid" }] } } }- detail.row_problems — 줄 번호(1부터) · 칸(row · email · employee_id · team · team_admin) · 까닭(not_object · missing · invalid · not_boolean). 값은 싣지 않아요.
- Retry-After 머리글이 오면 그만큼 기다린 뒤 같은 Idempotency-Key 로 다시 보내 주세요.
- 토큰을 알아본 응답에는 Oneport-Token-Expires-In(남은 날 수)이 붙고, 14일 안으로 남으면 Warning 머리글도 붙어요.
| HTTP | 코드 | 뜻 · 할 일 |
|---|---|---|
| 400 | bad_body | 본문 꼴이 틀렸어요(detail.field · detail.reason) |
| 400 | bad_row | 꼴이 틀린 줄이 있어요(detail.row_problems) |
| 400 | bad_idempotency_key | apply 에 Idempotency-Key 가 없거나 꼴이 틀렸어요 |
| 401 | token_missing | 토큰이 없어요 |
| 401 | token_invalid | 토큰을 알아보지 못했어요 |
| 401 | token_expired | 토큰 기한이 지났어요(주인이 새로 만들어요) |
| 401 | token_revoked | 끊긴 토큰이에요 |
| 401 | token_issuer_changed | 토큰을 만든 분이 더 이상 주인이 아니에요(지금 주인이 새로 만들어요) |
| 403 | origin_not_allowed | 브라우저에서 불렀어요 |
| 403 | ip_not_allowed | 허용 IP 밖이에요 |
| 403 | scope_missing | 읽기 토큰으로 apply 를 보냈어요 |
| 403 | space_closed | 닫힌 스페이스예요 |
| 403 | not_org | 회사·기관 스페이스가 아니에요 |
| 404 | run_not_found | 그 회차가 없어요 |
| 404 | not_found | 찾지 못했어요(아직 열리지 않은 스페이스도 이렇게 답해요) |
| 409 | run_active | 이 스페이스에서 맞추는 중이에요(Retry-After) |
| 409 | idempotency_in_progress | 같은 키의 요청을 아직 판정하고 있어요(Retry-After) |
| 409 | idempotency_mismatch | 같은 키로 다른 명단이 왔어요(새 명단이면 새 키) |
| 409 | older_than_applied | 이미 맞춘 명단보다 오래된 명단이에요 |
| 413 | body_too_large | 본문이 4MB 를 넘어요 |
| 413 | too_many_rows | 줄 수가 상한을 넘었어요(detail.max) |
| 415 | unsupported_media_type | Content-Type 이 JSON 이 아니에요 |
| 422 | duplicate_email | 같은 메일이 두 줄 넘게 있어요(detail.row_nos) |
| 422 | duplicate_employee_id | 같은 사번이 두 줄 넘게 있어요(detail.row_nos) |
| 422 | field_not_declared | fields 에 없는 칸이 든 줄이 있어요 |
| 422 | roster_blocked | 이 명단으로는 맞추지 않았어요(detail.block) |
| 429 | rate_limited | 요청이 잦아요(Retry-After) |
| 503 | not_ready | 준비 중이에요(Retry-After) |
| 503 | unread | 스페이스 정보를 끝까지 읽지 못했어요(Retry-After) |
예시#
bash
# 1) 결과만 보기
curl -X POST https://oneport.kr/api/spaces/roster/v1/runs \
-H "Authorization: Bearer $ONEPORT_ROSTER_TOKEN" -H "Content-Type: application/json" \
--data-binary @roster-dry-run.json
# 2) 맞추기 — 키는 파일 해시
curl -X POST https://oneport.kr/api/spaces/roster/v1/runs \
-H "Authorization: Bearer $ONEPORT_ROSTER_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(shasum -a 256 roster.json | cut -d' ' -f1)" \
--data-binary @roster.json
# 3) 상태
curl https://oneport.kr/api/spaces/roster/v1/status -H "Authorization: Bearer $ONEPORT_ROSTER_TOKEN"자주 묻는 것#
- SCIM 을 쓰나요? 아니요. HR 시스템이 위 JSON 명단을 보내 주세요.
- 정지가 저절로 되나요? 아니요. 명단에서 빠진 분은 정지 후보로 관리자 확인을 기다려요.
- 같은 명단을 두 번 보내면요? 같은 키면 처음 응답을 다시 드리고, 새 키여도 바뀐 것이 없으면 차이 0 으로 끝나요.
- 명단이 끊기면요? 스페이스에 정한 주기(매일 · 매주 · 매달)를 넘기면 관리자 화면에 알림이 서요.