본문으로 건너뛰기

HR 명단 API

인사 시스템이 직원 명단을 보내면 회사 스페이스의 구성원을 맞춰요. 회사마다 열어 드려요.

AI 에게 넘기기ChatGPT 에서 열기 (새 창)Claude 에서 열기 (새 창)원문 열기

회사 HR 시스템이 정해진 때마다 직원 명단을 보내면 스페이스의 구성원을 맞춰요. 정지와 복귀는 늘 스페이스 관리자가 확인해요. API 로 넣은 직원에게는 초대 메일이 가지 않아요.

준비#

  • 스페이스가 열리면 주인이 설정 탭 「명단 API」에서 토큰을 만들어요.
  • 범위는 읽기와 쓰기예요. 읽기는 상태 보기와 dry_run, 쓰기는 apply 까지예요. 쓰기는 허용 IP 를 하나 이상 적어야 해요(최대 10개 · 예: 203.0.113.10/32).
  • 토큰은 만든 창에서 한 번만 보여요. HR 시스템의 비밀 저장소에 넣어 주세요.
  • 스페이스마다 살아 있는 토큰은 2개까지예요(바꿀 때 겹쳐 쓰는 몫).
  • 서버의 배치 작업에서 불러요. 브라우저에서는 부를 수 없어요.

주소#

메서드주소범위하는 일
POSThttps://oneport.kr/api/spaces/roster/v1/runsdry_run 은 읽기 · apply 는 쓰기명단 보내기
GEThttps://oneport.kr/api/spaces/roster/v1/runs/{id}읽기회차 하나 보기
GEThttps://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 · applydry_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_cap24시간 자동 맞추기 수가 끝에 닿았어요
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코드뜻 · 할 일
400bad_body본문 꼴이 틀렸어요(detail.field · detail.reason)
400bad_row꼴이 틀린 줄이 있어요(detail.row_problems)
400bad_idempotency_keyapply 에 Idempotency-Key 가 없거나 꼴이 틀렸어요
401token_missing토큰이 없어요
401token_invalid토큰을 알아보지 못했어요
401token_expired토큰 기한이 지났어요(주인이 새로 만들어요)
401token_revoked끊긴 토큰이에요
401token_issuer_changed토큰을 만든 분이 더 이상 주인이 아니에요(지금 주인이 새로 만들어요)
403origin_not_allowed브라우저에서 불렀어요
403ip_not_allowed허용 IP 밖이에요
403scope_missing읽기 토큰으로 apply 를 보냈어요
403space_closed닫힌 스페이스예요
403not_org회사·기관 스페이스가 아니에요
404run_not_found그 회차가 없어요
404not_found찾지 못했어요(아직 열리지 않은 스페이스도 이렇게 답해요)
409run_active이 스페이스에서 맞추는 중이에요(Retry-After)
409idempotency_in_progress같은 키의 요청을 아직 판정하고 있어요(Retry-After)
409idempotency_mismatch같은 키로 다른 명단이 왔어요(새 명단이면 새 키)
409older_than_applied이미 맞춘 명단보다 오래된 명단이에요
413body_too_large본문이 4MB 를 넘어요
413too_many_rows줄 수가 상한을 넘었어요(detail.max)
415unsupported_media_typeContent-Type 이 JSON 이 아니에요
422duplicate_email같은 메일이 두 줄 넘게 있어요(detail.row_nos)
422duplicate_employee_id같은 사번이 두 줄 넘게 있어요(detail.row_nos)
422field_not_declaredfields 에 없는 칸이 든 줄이 있어요
422roster_blocked이 명단으로는 맞추지 않았어요(detail.block)
429rate_limited요청이 잦아요(Retry-After)
503not_ready준비 중이에요(Retry-After)
503unread스페이스 정보를 끝까지 읽지 못했어요(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 으로 끝나요.
  • 명단이 끊기면요? 스페이스에 정한 주기(매일 · 매주 · 매달)를 넘기면 관리자 화면에 알림이 서요.