급여대장과 급여명세서 엑셀 만들기
직원별로 계산한 급여 금액을 받아 급여대장과 급여명세서 엑셀 파일을 만드는 작업 두 개의 준비, 요청, 파일 구조, 결과, 오류 안내입니다.
이 작업이 하는 일
직원별로 이미 계산한 급여 금액을 받아 엑셀 파일(xlsx)을 만드는 작업 두 개입니다.급여대장(register_xlsx)은 직원 전체를 한 장의 표로, 급여명세서(payslip_xlsx)는 직원마다 시트를 하나씩 담은 파일 하나로 만듭니다.
결과는 짧은 요약(result.data)과 파일 목록(result.artifacts)이고, 파일마다 내려받는 서명된 주소(download_url)가 붙습니다.이 작업은 급여나 공제액을 계산하지 않고 기관에 접속하지도 않습니다. 그래서 기관 계정 연동이나 로그인이 필요 없습니다.
| 작업 | 2개(아래 표) |
|---|---|
| REST | POST /v1/jobs (provider payroll_artifact) |
| MCP | POST https://api.xdata.kr/mcp (tools/call) |
| 인증 | Authorization: Basic |
| 환경 헤더 | X-Env-Scope (REST, 필수), X-MCP-Env-Scope (MCP, 선택) |
| 결과 | result.artifacts[].download_url |
| 주소 유효 시간 | 15분 |
이 작업은 금액을 계산하지 않고 보낸 값을 그대로 파일에 적습니다.공제계와 실수령액이 공제 항목의 합과 맞는지도 검사하지 않으니, 계산은 보내기 전에 끝내 주세요. 소득세와 지방소득세도 이미 구한 금액을 넣습니다.
작업 두 개
| 작업 | REST action | MCP 도구 | 만드는 파일 | 결과 스키마 |
|---|---|---|---|---|
| 급여대장 엑셀 생성 | register_xlsx | payroll_artifact__register_xlsx | 급여대장_20260825.xlsx: 직원마다 한 행이 있는 표 한 장 | payroll_artifact.register_xlsx.v1 |
| 급여명세서 엑셀 생성 | payslip_xlsx | payroll_artifact__payslip_xlsx | 급여명세서_20260825.xlsx: 직원마다 시트가 하나씩 있는 파일 하나 | payroll_artifact.payslip_xlsx.v1 |
부르기 전에 준비할 것
- 계산을 마친 금액을 준비합니다
직원별 월 급여, 국민연금, 건강보험, 장기요양보험, 고용보험, 소득세, 지방소득세, 공제계, 실수령액을 원 단위 정수로 준비합니다.필요한 칸은 아래 「직원 한 명의 칸」 표에 있습니다. 급여 보험료 조회 도구가 돌려주는 금액은 사업장 단위 합계라 직원별 입력으로 그대로 쓸 수 없습니다.
- API 키를 준비합니다
REST 로 부르려면 REST API 빠른 시작의 순서로 API 키를 만듭니다.MCP 로 부르려면 MCP 연동 개요의 순서대로 MCP 서버를 연결합니다. 기관 계정을 연동하거나 로그인할 필요는 없습니다.문의는 서비스문의로 하면 됩니다.
요청
같은 요청을 두 방법으로 보낼 수 있습니다.REST 는 POST /v1/jobs 에 provider, action, params 를 담아 작업을 제출하고, MCP 는 tools/call 에 도구 이름과 arguments 를 담습니다.params 와 arguments 의 칸은 같습니다. 계정 연동이 필요 없는 작업이라 account_link_id 를 보낼 필요가 없습니다. 예시의 이름, 사업자등록번호, 주소는 지어낸 값입니다.금액은 설명을 위해 맞춘 값이라 지금의 보험료율이나 간이세액표 금액과 다를 수 있으니 실제 계산에 쓰지 마세요.
REST 로 요청하기
curl --request POST \
--url 'https://api.xdata.kr/v1/jobs' \
--header 'Authorization: Basic {base64(client_id:client_secret)}' \
--header 'X-Env-Scope: real_test' \
--header 'Idempotency-Key: demo-register_xlsx-001' \
--header 'Content-Type: application/json' \
--data '{
"provider": "payroll_artifact",
"action": "register_xlsx",
"params": {
"attribution_ym": "202608",
"workplace_name": "예시상사",
"business_registration_no": "000-00-00000",
"workplace_address": "서울특별시 예시구 예시로 1",
"employees": [
{
"employee_key": "E001",
"employee_name": "홍길동",
"birth_date": "1990-01-15",
"dependent_count": 1,
"children_count_8_to_20": 0,
"monthly_pay_won": 3000000,
"base_salary_won": 3000000,
"national_pension_won": 135000,
"health_insurance_won": 106350,
"long_term_care_won": 13770,
"employment_insurance_won": 27000,
"income_tax_won": 74350,
"local_income_tax_won": 7430,
"total_deduction_won": 363900,
"net_pay_won": 2636100
},
{
"employee_key": "E002",
"employee_name": "김가나",
"birth_date": "1985-06-30",
"dependent_count": 3,
"children_count_8_to_20": 1,
"monthly_pay_won": 3500000,
"base_salary_won": 3500000,
"national_pension_won": 157500,
"health_insurance_won": 124075,
"long_term_care_won": 16065,
"employment_insurance_won": 31500,
"income_tax_won": 38960,
"local_income_tax_won": 3890,
"total_deduction_won": 371990,
"net_pay_won": 3128010
}
]
}
}'curl --request POST \
--url 'https://api.xdata.kr/v1/jobs' \
--header 'Authorization: Basic {base64(client_id:client_secret)}' \
--header 'X-Env-Scope: real_test' \
--header 'Idempotency-Key: demo-payslip_xlsx-001' \
--header 'Content-Type: application/json' \
--data '{
"provider": "payroll_artifact",
"action": "payslip_xlsx",
"params": {
"attribution_ym": "202608",
"workplace_name": "예시상사",
"business_registration_no": "000-00-00000",
"workplace_address": "서울특별시 예시구 예시로 1",
"employees": [
{
"employee_key": "E001",
"employee_name": "홍길동",
"birth_date": "1990-01-15",
"dependent_count": 1,
"children_count_8_to_20": 0,
"monthly_pay_won": 3000000,
"base_salary_won": 3000000,
"national_pension_won": 135000,
"health_insurance_won": 106350,
"long_term_care_won": 13770,
"employment_insurance_won": 27000,
"income_tax_won": 74350,
"local_income_tax_won": 7430,
"total_deduction_won": 363900,
"net_pay_won": 2636100
},
{
"employee_key": "E002",
"employee_name": "김가나",
"birth_date": "1985-06-30",
"dependent_count": 3,
"children_count_8_to_20": 1,
"monthly_pay_won": 3500000,
"base_salary_won": 3500000,
"national_pension_won": 157500,
"health_insurance_won": 124075,
"long_term_care_won": 16065,
"employment_insurance_won": 31500,
"income_tax_won": 38960,
"local_income_tax_won": 3890,
"total_deduction_won": 371990,
"net_pay_won": 3128010
}
],
"payment_date": "2026-08-25"
}
}'MCP 로 요청하기(급여대장, 급여명세서는 name 과 arguments 만 바꿉니다)
# 급여대장 엑셀 만들기
# echo -n "<client_id>:<client_secret>" | base64
export MCP_URL="https://api.xdata.kr/mcp"
export MCP_AUTH_B64="<base64_결과_붙여넣기>"
curl -sS -X POST "$MCP_URL" \
-H "Authorization: Basic $MCP_AUTH_B64" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "payroll_artifact__register_xlsx",
"arguments": {
"attribution_ym": "202608",
"workplace_name": "예시상사",
"business_registration_no": "000-00-00000",
"workplace_address": "서울특별시 예시구 예시로 1",
"employees": [
{
"employee_key": "E001",
"employee_name": "홍길동",
"birth_date": "1990-01-15",
"dependent_count": 1,
"children_count_8_to_20": 0,
"monthly_pay_won": 3000000,
"base_salary_won": 3000000,
"national_pension_won": 135000,
"health_insurance_won": 106350,
"long_term_care_won": 13770,
"employment_insurance_won": 27000,
"income_tax_won": 74350,
"local_income_tax_won": 7430,
"total_deduction_won": 363900,
"net_pay_won": 2636100
},
{
"employee_key": "E002",
"employee_name": "김가나",
"birth_date": "1985-06-30",
"dependent_count": 3,
"children_count_8_to_20": 1,
"monthly_pay_won": 3500000,
"base_salary_won": 3500000,
"national_pension_won": 157500,
"health_insurance_won": 124075,
"long_term_care_won": 16065,
"employment_insurance_won": 31500,
"income_tax_won": 38960,
"local_income_tax_won": 3890,
"total_deduction_won": 371990,
"net_pay_won": 3128010
}
]
}
}
}'급여대장 엑셀 생성 요청 칸
| 필드 | 타입 | 필수 | 설명 | 예시 |
|---|---|---|---|---|
| attribution_ym | string | 필수 | 귀속연월입니다. YYYYMM 여섯 자리 숫자 문자열입니다(예: 202608). 파일 위쪽의 귀속연월 칸에 적힙니다. | 202608 |
| employees | array | 필수 | 직원 목록입니다. 한 명 이상이어야 하고 한 명이 급여대장의 한 행, 급여명세서의 한 시트가 됩니다. 직원 한 명의 칸은 아래 표에 있습니다. | [{ ... }, { ... }] |
| workplace_name | string | 선택 | 사업장명입니다. 파일 위쪽의 사업장명 칸에 적힙니다. company_name 도 같은 뜻으로 읽고, 둘 다 있으면 workplace_name 이 우선합니다. | 예시상사 |
| company_name | string | 선택 | workplace_name 의 다른 이름입니다. workplace_name 이 없을 때만 씁니다. | — |
| business_registration_no | string | 선택 | 사업자등록번호입니다. 입력한 글이 그대로 적힙니다. | 000-00-00000 |
| workplace_address | string | 선택 | 사업장 주소입니다. 파일 위쪽의 사업장 주소 칸에 적힙니다. | 서울특별시 예시구 예시로 1 |
급여명세서 엑셀 생성 요청 칸
| 필드 | 타입 | 필수 | 설명 | 예시 |
|---|---|---|---|---|
| attribution_ym | string | 필수 | 귀속연월입니다. YYYYMM 여섯 자리 숫자 문자열입니다(예: 202608). 파일 위쪽의 귀속연월 칸에 적힙니다. | 202608 |
| payment_date | string | 필수 | 급여 지급일입니다. YYYY-MM-DD 형식이고 달력에 있는 날짜여야 합니다(예: 2026-08-25). 급여명세서에만 있습니다. | 2026-08-25 |
| employees | array | 필수 | 직원 목록입니다. 한 명 이상이어야 하고 한 명이 급여대장의 한 행, 급여명세서의 한 시트가 됩니다. 직원 한 명의 칸은 아래 표에 있습니다. | [{ ... }, { ... }] |
| workplace_name | string | 필수 | 사업장명입니다. 파일 위쪽의 사업장명 칸에 적힙니다. company_name 도 같은 뜻으로 읽고, 둘 다 있으면 workplace_name 이 우선합니다. | 예시상사 |
| company_name | string | 선택 | workplace_name 의 다른 이름입니다. workplace_name 이 없을 때만 씁니다. | — |
| business_registration_no | string | 필수 | 사업자등록번호입니다. 입력한 글이 그대로 적힙니다. | 000-00-00000 |
| workplace_address | string | 필수 | 사업장 주소입니다. 파일 위쪽의 사업장 주소 칸에 적힙니다. | 서울특별시 예시구 예시로 1 |
직원 한 명의 칸
두 작업의 employees 항목은 같은 칸을 씁니다.표의 필수는 입력 스키마가 선언한 값입니다.이름이 다른 두 칸이 같은 뜻인 경우(monthly_pay_won 과 monthly_salary_won, base_salary_won 과 basic_salary_won)는 설명에 우선순위를 적었습니다.우선하는 칸이 정수로 읽히지 않으면 다른 칸을 씁니다.
직원 한 명의 칸(employees 의 한 항목)
| 필드 | 타입 | 필수 | 설명 | 예시 |
|---|---|---|---|---|
| employee_key | string | 필수 | 요청 안에서 직원을 가르는 값입니다. 필수이지만 파일에는 적히지 않습니다. | E001 |
| employee_name | string | 필수 | 직원 이름입니다. 급여대장의 이름 열, 급여명세서의 성명과 시트 이름에 적힙니다. | 홍길동 |
| birth_date | string | 필수 | 생년월일입니다. YYYY-MM-DD 또는 YYYYMMDD 문자열이고 입력한 글이 그대로 적힙니다. | 1990-01-15 |
| dependent_count | integer | 필수 | 부양가족 수입니다. 본인을 포함해 1 이상의 정수입니다. | 1 |
| children_count_8_to_20 | integer | 선택 | 8세 이상 20세 이하 자녀 수입니다. 0 이상의 정수입니다. 월 급여(monthly_pay_won 또는 monthly_salary_won)를 보내면 생략할 수 있고 생략하면 0 으로 적힙니다. 급여명세서에 월 급여 없이 gross_pay_won 만 보낼 때는 생략할 수 없습니다(payroll_artifact.missing_children_count_8_to_20 으로 거절됩니다). 값은 급여대장에만 나옵니다. | 0 |
| monthly_pay_won | integer | 필수 | 월 급여(지급합계)입니다. 급여대장의 월지급액과 급여명세서의 지급합계에 적힙니다. monthly_salary_won 과 함께 보내면 이 값이 우선합니다. | 3000000 |
| monthly_salary_won | integer | 선택 | monthly_pay_won 의 다른 이름입니다. monthly_pay_won 이 없을 때만 씁니다. | — |
| base_salary_won | integer | 선택 | 기본급입니다. 급여대장의 기본급 열과 급여명세서의 기본급에 적힙니다. 급여대장의 기본급 열에는 받은 값이 그대로 적힙니다. | 3000000 |
| basic_salary_won | integer | 선택 | 급여명세서의 기본급으로 읽습니다. base_salary_won 과 함께 보내면 이 값이 우선합니다. 급여대장의 기본급 열은 이 칸을 읽지 않고 base_salary_won 만 읽습니다. | — |
| national_pension_won | integer | 필수 | 국민연금 공제액입니다(원 단위 정수). | 135000 |
| health_insurance_won | integer | 필수 | 건강보험 공제액입니다(원 단위 정수). | 106350 |
| long_term_care_won | integer | 필수 | 장기요양보험 공제액입니다(원 단위 정수). | 13770 |
| employment_insurance_won | integer | 필수 | 고용보험 공제액입니다(원 단위 정수). | 27000 |
| income_tax_won | integer | 필수 | 소득세 공제액입니다(원 단위 정수). 이 작업은 세액을 계산하지 않고 보낸 값을 적습니다. | 74350 |
| local_income_tax_won | integer | 필수 | 지방소득세 공제액입니다(원 단위 정수). 이 작업은 세액을 계산하지 않고 보낸 값을 적습니다. | 7430 |
| total_deduction_won | integer | 필수 | 공제계입니다(원 단위 정수). 보낸 값이 그대로 적히고 공제 항목의 합과 맞는지는 검사하지 않습니다. | 363900 |
| net_pay_won | integer | 필수 | 실수령액입니다(원 단위 정수). 보낸 값이 그대로 적히고 월 급여에서 공제계를 뺀 값과 맞는지는 검사하지 않습니다. | 2636100 |
| gross_pay_won | integer | 선택 | 급여명세서의 지급합계입니다. monthly_pay_won 과 monthly_salary_won 이 모두 없을 때만 쓰고, 이때는 children_count_8_to_20 도 보내야 합니다. 급여대장은 이 칸을 월 급여로 읽지 않습니다. | — |
급여명세서는 공제 항목 여섯 칸(국민연금, 건강보험, 장기요양보험, 고용보험, 소득세, 지방소득세)을 검사하지 않습니다.빠지면 그 칸이 비고 숫자가 아닌 글을 보내면 그 글이 그대로 적히니, 빠뜨리지 말고 정수로 보내세요. 급여대장은 월 급여, 공제 항목 여섯 칸, 공제계, 실수령액을 정수로 읽을 수 없으면 거절합니다.
급여대장 파일
직원 한 명이 한 행이 되고 직원 행은 5행부터 채워집니다. 파일 위쪽에는 아래 표의 네 칸이 적힙니다.
파일 위쪽 칸
| 이름 | 값이 적히는 칸 | 입력 칸 | 예시 값 |
|---|---|---|---|
| 귀속연월 | B1 | attribution_ym | 202608 |
| 사업장명 | G1 | workplace_name(없으면 company_name) | 예시상사 |
| 사업자등록번호 | M1 | business_registration_no | 000-00-00000 |
| 사업장 주소 | B2 | workplace_address | 서울특별시 예시구 예시로 1 |
열은 아래와 같습니다. 입력 칸이 선언돼 있지 않은 수당 열은 보내지 않으면 비어 있고, 이 안내는 그 열에 값을 보내는 방법을 다루지 않습니다.
열
| 열 | 머리 글 | 입력 칸 | 비고 |
|---|---|---|---|
| A | 번호 | - | 1 부터 세는 번호가 자동으로 적힙니다 |
| B | 이 름 | employee_name | |
| C | 생년월일 | birth_date | |
| D | 부양가족수 | dependent_count | |
| E | 8세 이상 20세 이하 자녀수 | children_count_8_to_20 | |
| F | 기본급 | base_salary_won | |
| G | 식대 | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| H | 시간외 수당 | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| I | 성과수당 | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| J | 직책수당 | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| K | 연차수당 | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| L | 월지급액 | monthly_pay_won(없으면 monthly_salary_won) | |
| M | 국민연금 | national_pension_won | |
| N | 건강보험 | health_insurance_won | |
| O | 고용보험 | employment_insurance_won | |
| P | 장기노인 요양보험 | long_term_care_won | |
| Q | 소득세 | income_tax_won | |
| R | 지방 소득세 | local_income_tax_won | |
| S | 공제계 | total_deduction_won | |
| T | 차인지급액 | net_pay_won |
아래는 위 요청 예시의 첫 직원 행입니다. 머리 글이 키이고 비어 있는 칸은 null 입니다.
{
"번호": 1,
"이 름": "홍길동",
"생년월일": "1990-01-15",
"부양가족수": 1,
"8세 이상 20세 이하 자녀수": 0,
"기본급": 3000000,
"식대": null,
"시간외 수당": null,
"성과수당": null,
"직책수당": null,
"연차수당": null,
"월지급액": 3000000,
"국민연금": 135000,
"건강보험": 106350,
"고용보험": 27000,
"장기노인 요양보험": 13770,
"소득세": 74350,
"지방 소득세": 7430,
"공제계": 363900,
"차인지급액": 2636100
}직원 행 다음 줄(예시에서는 7행)에 「총 합 계」 행이 붙습니다.F열부터 T열까지는 SUM 수식입니다(예: =SUM(M5:M6)). 수식의 계산 결과는 파일에 저장되지 않습니다. 엑셀이나 스프레드시트 앱에서 열면 계산되지만, 파일의 값만 읽는 프로그램은 합계 칸을 빈 칸으로 읽습니다.
급여명세서 파일
직원마다 시트가 하나씩 만들어지고 시트 이름은 직원 이름입니다. 아래는 위 요청 예시의 첫 직원 시트입니다.
시트 이름 규칙
| 상황 | 예시 | 결과 |
|---|---|---|
| 이름이 같은 직원 | 홍길동, 홍길동 | 홍길동, 홍길동_2 (뒤의 시트에 번호가 붙습니다) |
| 시트 이름에 쓸 수 없는 글자 | 이름에 \ / * ? : [ ] 가 들어 있을 때 | 그 글자를 지우고 남은 글로 시트 이름을 만듭니다 |
| 긴 이름 | 42글자 | 28글자까지만 시트 이름으로 씁니다 |
위쪽 칸
| 이름 | 값이 적히는 칸 | 입력 칸 | 예시 값 |
|---|---|---|---|
| 귀속연월 | A4 | attribution_ym | 202608 |
| 급여지급일 | C4 | payment_date | 2026-08-25 |
| 사업장명 | E4 | workplace_name(없으면 company_name) | 예시상사 |
근로자 정보 칸
| 이름 | 값이 적히는 칸 | 입력 칸 | 예시 값 | 비고 |
|---|---|---|---|---|
| 성명 | B6 | employee_name | 홍길동 | |
| 사원번호 | D6 | - | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| 부서 | F6 | - | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| 직급 | H6 | - | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| 생년월일 | B7 | birth_date | 1990-01-15 | |
| 입사일 | D7 | - | - | 선언된 입력 칸이 없어 보내지 않으면 비어 있습니다 |
| 사업자등록번호 | B8 | business_registration_no | 000-00-00000 | |
| 사업장 주소 | F8 | workplace_address | 서울특별시 예시구 예시로 1 |
지급 항목
| 항목 | 입력 칸 | 예시 금액(원) | 계산방법 칸의 글 |
|---|---|---|---|
| 기본급 | base_salary_won(또는 basic_salary_won) | 3,000,000 | 월 고정급 |
| 직책수당 | - | - | - |
| 식대 | - | - | - |
| 연장근로수당 | - | - | - |
| 야간근로수당 | - | - | - |
| 휴일근로수당 | - | - | - |
| 상여금 | - | - | - |
| 성과금 | - | - | - |
| 기타수당 | - | - | - |
공제 항목
| 항목 | 입력 칸 | 예시 금액(원) | 산정기준/비고 칸의 글 |
|---|---|---|---|
| 국민연금 | national_pension_won | 135,000 | 기관 조회 결과 반영 |
| 건강보험 | health_insurance_won | 106,350 | 기관 조회 결과 반영 |
| 장기요양보험 | long_term_care_won | 13,770 | 기관 조회 결과 반영 |
| 고용보험 | employment_insurance_won | 27,000 | 기관 조회 결과 반영 |
| 소득세 | income_tax_won | 74,350 | 입력된 세액 반영 |
| 지방소득세 | local_income_tax_won | 7,430 | 입력된 세액 반영 |
| 기타공제 | - | - | - |
합계
| 항목 | 값이 적히는 칸 | 입력 칸 | 예시 금액(원) |
|---|---|---|---|
| 지급합계 | B20 | monthly_pay_won(없으면 monthly_salary_won, 그것도 없으면 gross_pay_won) | 3,000,000 |
| 공제합계 | F20 | total_deduction_won | 363,900 |
| 차인지급액(실지급액) | G22 | net_pay_won | 2,636,100 |
지급 항목의 계산방법 칸과 공제 항목의 산정기준/비고 칸에 적히는 글은 항목마다 정해진 문장이고 금액이 정수로 읽히며 0 이 아닐 때 적힙니다.요청에는 금액의 출처를 알리는 칸이 없으므로, 이 글은 금액을 어디서 구했는지와 관계가 없습니다.
결과 받기
REST 로 제출하면 곧바로 job_id 가 오고(HTTP 202), 파일은 작업이 끝난 뒤 결과 조회로 받습니다.상태 확인과 결과 조회의 필드는 작업 상태 조회에 있습니다.이 작업의 result.data 에는 귀속연월(attribution_ym)과 파일에 적은 직원 수(employee_count)가 오고, 파일은 result.artifacts 에 옵니다.파일 만들기는 보통 몇 초 안에 끝납니다(2026-10-20 운영에서 직원 한 명으로 한 번씩 잰 제출부터 완료까지는 급여대장이 약 4초, 급여명세서가 약 2초였습니다). 끝났는지는 상태 조회로 확인하세요. 제출 응답은 아래와 같습니다.
{
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMNPQ",
"status": "QUEUED",
"env_scope": "real_test",
"provider": "payroll_artifact",
"action": "register_xlsx",
"submitted_at": "2026-05-27T12:34:56+09:00"
}성공하면 결과 조회가 아래와 같이 옵니다. 급여명세서도 같은 모양이고 schema 와 파일 이름, 크기만 다릅니다.내려받는 파일은 엑셀(xlsx) 형식입니다. 파일 이름(name)은 「업무 이름_만든 날짜(한국 시간).xlsx」 입니다(예: 급여대장_20260825.xlsx, 급여명세서_20260825.xlsx).내려받은 파일을 저장할 때 이 이름을 그대로 쓰면 됩니다. sha256 은 파일의 SHA-256 해시(16진수 64자)이고 예시에서는 줄여 적었습니다.
{
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMNPQ",
"status": "SUCCEEDED",
"result": {
"schema": "payroll_artifact.register_xlsx.v1",
"data": {
"attribution_ym": "202608",
"employee_count": 2
},
"artifacts": [
{
"artifact_id": "art_01JC8YH8R0E1ABCDEFGHJKMNPR",
"name": "급여대장_20260825.xlsx",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size_bytes": 6847,
"sha256": "...",
"expires_at": "2026-09-01T12:00:03+09:00",
"expired": false,
"download_url": "https://storage.googleapis.com/example-bucket/real_test/job_results/job_01JC8YH8R0E1ABCDEFGHJKMNPQ/art_01JC8YH8R0E1ABCDEFGHJKMNPR.xlsx?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Expires=900&X-Goog-Signature=..."
}
]
},
"finished_at": "2026-08-25T12:00:03+09:00"
}download_url 은 15분 동안만 유효합니다.주소를 저장해 두지 말고 필요할 때 결과 조회를 다시 호출해 새 주소를 받으세요. 주소 자체가 서명돼 있어 내려받을 때 인증 헤더를 붙이지 않습니다. 파일은 만든 때부터 7일 동안 보관합니다(expires_at).그 뒤에는 expired 가 true 이고 download_url 이 null 이니 그 전에 내려받아 두세요.
MCP 로 부르면 도구가 파일이 만들어질 때까지 기다렸다가 결과를 한 번에 돌려줍니다.result.content[0].text 에 REST 결과와 같은 schema, data, artifacts 와 그 작업의 job_id 를 담은 JSON 문자열이 옵니다.MCP 2025-06-18 이후 판으로 연결한 클라이언트는 파일마다 resource_link 블록도 함께 받습니다(주소는 download_url 과 같습니다).그 블록의 자리는 호출마다 다릅니다 - 사용 토큰을 세는 환경에서는 남은 사용 토큰을 알리는 블록이 먼저 끼므로(MCP 연동 개요의 「응답에 실리는 _meta」) 순번이 아니라 type 으로 골라 읽으세요.주소가 만료되면 xdata__job__result 도구에 그 job_id 를 넣어 새 주소를 받습니다 - 작업을 다시 돌리지 않고 사용 토큰도 쓰지 않습니다.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"job_id\": \"job_01JC8YH8R0E1ABCDEFGHJKMNPQ\", \"schema\": \"payroll_artifact.register_xlsx.v1\", \"data\": {\"attribution_ym\": \"202608\", \"employee_count\": 2}, \"artifacts\": [{\"artifact_id\": \"art_01JC8YH8R0E1ABCDEFGHJKMNPR\", \"name\": \"급여대장_20260825.xlsx\", \"content_type\": \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\", \"size_bytes\": 6847, \"sha256\": \"...\", \"expires_at\": \"2026-09-01T12:00:03+09:00\", \"expired\": false, \"download_url\": \"https://storage.googleapis.com/example-bucket/real_test/job_results/job_01JC8YH8R0E1ABCDEFGHJKMNPQ/art_01JC8YH8R0E1ABCDEFGHJKMNPR.xlsx?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Expires=900&X-Goog-Signature=...\"}]}"
},
{
"type": "resource_link",
"uri": "https://storage.googleapis.com/example-bucket/real_test/job_results/job_01JC8YH8R0E1ABCDEFGHJKMNPQ/art_01JC8YH8R0E1ABCDEFGHJKMNPR.xlsx?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Expires=900&X-Goog-Signature=...",
"name": "급여대장_20260825.xlsx",
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 6847,
"description": "엑셀(xlsx) · 이 작업의 결과로 만든 문서. 내려받기 링크는 15분 동안 열리고, 파일은 2026-09-01 12:00까지 보관됩니다."
}
],
"isError": false
}
}입력값이 잘못됐을 때
값이 잘못되면 작업은 FAILED 로 끝나고 어느 값이 문제인지는 error.provider_error_ref 로 가립니다.error.message 는 영어 진단 문장이라 사람이 읽는 용도이고, 분기에는 provider_error_ref 를 쓰세요.문장 속 칸 이름은 서버 안에서 쓰는 이름이라 보낸 칸 이름과 다를 수 있고(월 급여가 없으면 급여대장은 monthly_salary_won, 급여명세서는 gross_pay_won 이고, 사업장명이 없으면 company_name 입니다), 사업장 칸이 없을 때도 문장이 employees[1] 로 시작할 수 있습니다.문장 안의 employees[N] 은 1 부터 세는 순번입니다(첫 직원이 employees[1]). 이 오류들은 retryable 이 false 입니다.값을 고친 뒤 새 Idempotency-Key 로 다시 제출하세요(재시도). 아래 표의 조건은 한 번에 하나씩 바꿔 실제 실행기에 넣어 잰 결과입니다.
급여대장 엑셀 생성의 입력 오류
| 조건 | error.code | error.provider_error_ref |
|---|---|---|
| 귀속연월(attribution_ym)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_attribution_ym |
| 귀속연월이 YYYYMM 여섯 자리가 아닐 때(예: 2026-08) | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_attribution_ym |
| 직원 목록(employees)이 비어 있을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_employees |
| 직원 항목이 객체가 아닐 때 | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_employee |
| 직원의 employee_key 가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_employee_key |
| 직원의 employee_name 이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_employee_name |
| 직원의 birth_date 가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_birth_date |
| 공제계(total_deduction_won)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 실수령액(net_pay_won)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 부양가족 수(dependent_count)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_dependent_count |
| 부양가족 수가 1 보다 작을 때(본인을 포함해 1 이상) | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_dependent_count |
| 자녀 수(children_count_8_to_20)가 0 보다 작을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_children_count_8_to_20 |
| 생년월일(birth_date)에 글이나 숫자가 아닌 값(목록이나 객체)을 넣었을 때 | INTERNAL_ERROR | payroll_artifact.internal |
| 월 급여(monthly_pay_won)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 금액 칸(예: national_pension_won)이 정수로 읽히지 않을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 공제 금액 칸(예: health_insurance_won)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
급여명세서 엑셀 생성의 입력 오류
| 조건 | error.code | error.provider_error_ref |
|---|---|---|
| 귀속연월(attribution_ym)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_attribution_ym |
| 귀속연월이 YYYYMM 여섯 자리가 아닐 때(예: 2026-08) | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_attribution_ym |
| 직원 목록(employees)이 비어 있을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_employees |
| 직원 항목이 객체가 아닐 때 | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_employee |
| 직원의 employee_key 가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_employee_key |
| 직원의 employee_name 이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_employee_name |
| 직원의 birth_date 가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_birth_date |
| 공제계(total_deduction_won)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 실수령액(net_pay_won)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 부양가족 수(dependent_count)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_dependent_count |
| 부양가족 수가 1 보다 작을 때(본인을 포함해 1 이상) | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_dependent_count |
| 자녀 수(children_count_8_to_20)가 0 보다 작을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_children_count_8_to_20 |
| 생년월일(birth_date)에 글이나 숫자가 아닌 값(목록이나 객체)을 넣었을 때 | INTERNAL_ERROR | payroll_artifact.internal |
| 급여 지급일(payment_date)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_payment_date |
| 급여 지급일이 YYYY-MM-DD 가 아닐 때(예: 2026/08/25) | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_payment_date |
| 급여 지급일이 달력에 없는 날짜일 때(예: 2026-02-30) | VALIDATION_INVALID_FIELD | payroll_artifact.invalid_payment_date |
| 사업장명(workplace_name 또는 company_name)이 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_company_name |
| 사업자등록번호(business_registration_no)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_business_registration_no |
| 사업장 주소(workplace_address)가 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_workplace_address |
| 월 급여(monthly_pay_won)와 지급합계(gross_pay_won)가 모두 없을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
| 실수령액(net_pay_won)이 정수로 읽히지 않을 때 | VALIDATION_INVALID_FIELD | payroll_artifact.missing_or_invalid_amount |
아래는 REST 로 상태를 조회했을 때 받는 실패 응답입니다. 급여명세서도 같은 모양이고 action 과 provider_error_ref 는 조건에 따라 다릅니다.
{
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMNPQ",
"status": "FAILED",
"env_scope": "real_test",
"provider": "payroll_artifact",
"action": "register_xlsx",
"submitted_at": "2026-08-25T12:00:00+09:00",
"started_at": "2026-08-25T12:00:01+09:00",
"finished_at": "2026-08-25T12:00:03+09:00",
"retry_count": 0,
"progress": 100,
"progress_message": null,
"retry_after_seconds": null,
"error": {
"code": "VALIDATION_INVALID_FIELD",
"message": "attribution_ym is required (YYYYMM)",
"retryable": false,
"provider_error_ref": "payroll_artifact.missing_attribution_ym",
"upstream_status": null
}
}MCP 로 부르면 같은 실패가 도구 결과(result.isError)로 옵니다.첫 번째 content 블록에 오류 문장이, 두 번째 블록에 job_id 와 error_code, safe_to_retry 를 담은 JSON 이 문자열로 들어 있습니다.MCP 응답에는 provider_error_ref 가 없으니 REST 로 상태를 조회하거나 메시지의 값 이름으로 원인을 찾으세요.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "작업이 실패했습니다(오류 VALIDATION_INVALID_FIELD). attribution_ym is required (YYYYMM)"
},
{
"type": "text",
"text": "(아래 JSON 을 문자열로 담은 값)"
}
],
"isError": true
}
}두 번째 블록의 text 를 JSON 으로 풀면 아래와 같습니다.
{
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMNPQ",
"error_code": "VALIDATION_INVALID_FIELD",
"safe_to_retry": false
}우리 쪽에서 파일을 만들지 못했을 때
입력 표의 INTERNAL_ERROR 두 행은 값의 형식이 어긋난 경우라 값을 고치면 풀립니다.그 밖에 파일을 만들거나 저장하는 중 우리 쪽에서 실패하면 아래처럼 끝납니다. 제출할 때 이미 파일 저장소가 준비되지 않았다면 작업을 만들지 않고 제출 자체를 거절합니다.REST 에서는 HTTP 503 SERVICE_UNAVAILABLE 이고 MCP 에서는 JSON-RPC 오류 -32603 에 일반 내부 오류 안내 문장이 옵니다.어느 쪽이든 작업(job_id)이 생기지 않고 사용 토큰도 쓰지 않으니 잠시 뒤 같은 요청을 다시 보내면 됩니다.값을 확인해도 같은 오류가 계속되면 서비스문의로 알려 주세요.
| 상황 | error.code | error.provider_error_ref | retryable |
|---|---|---|---|
| 파일을 만드는 중 예상하지 못한 오류가 난 경우 | INTERNAL_ERROR | payroll_artifact.internal | false |
| 제출한 뒤 파일 저장소가 준비되지 않아 작업을 시작하지 않은 경우 | INTERNAL_ERROR | job_artifacts.storage_not_ready | false |
| 만든 파일을 저장하지 못한 경우 | INTERNAL_ERROR | job_artifacts.store_failed | false |
알아 둘 점
파일에 적히는 글이 =, +, -, @ 로 시작하면 엑셀이 수식으로 실행하지 않도록 글 칸으로 적습니다.글은 보낸 그대로이고, 엑셀에서 작은따옴표를 쳐 넣은 칸과 같게 보입니다. 정수로 읽히는 금액은 수식이 아니라 숫자로 적힙니다.다만 급여대장의 기본급 열(base_salary_won)은 받은 값을 그대로 적으니 글이 아니라 숫자(정수)로 보내세요.
코드로 급여대장 받기(REST)
import { writeFile } from 'node:fs/promises';
const api = process.env.XBOSS_API_URL ?? 'https://api.xdata.kr';
const auth = 'Basic ' + Buffer.from(`${process.env.XBOSS_CLIENT_ID}:${process.env.XBOSS_CLIENT_SECRET}`).toString('base64');
const headers = { Authorization: auth, 'X-Env-Scope': 'real_test' };
const params = {
"attribution_ym": "202608",
"workplace_name": "예시상사",
"business_registration_no": "000-00-00000",
"workplace_address": "서울특별시 예시구 예시로 1",
"employees": [
{
"employee_key": "E001",
"employee_name": "홍길동",
"birth_date": "1990-01-15",
"dependent_count": 1,
"children_count_8_to_20": 0,
"monthly_pay_won": 3000000,
"base_salary_won": 3000000,
"national_pension_won": 135000,
"health_insurance_won": 106350,
"long_term_care_won": 13770,
"employment_insurance_won": 27000,
"income_tax_won": 74350,
"local_income_tax_won": 7430,
"total_deduction_won": 363900,
"net_pay_won": 2636100
},
{
"employee_key": "E002",
"employee_name": "김가나",
"birth_date": "1985-06-30",
"dependent_count": 3,
"children_count_8_to_20": 1,
"monthly_pay_won": 3500000,
"base_salary_won": 3500000,
"national_pension_won": 157500,
"health_insurance_won": 124075,
"long_term_care_won": 16065,
"employment_insurance_won": 31500,
"income_tax_won": 38960,
"local_income_tax_won": 3890,
"total_deduction_won": 371990,
"net_pay_won": 3128010
}
]
};
// 1. 작업을 제출합니다. 같은 요청을 다시 보낼 때는 같은 Idempotency-Key 를 씁니다.
const submitted = await fetch(`${api}/v1/jobs`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': 'payroll-register-202608-001' },
body: JSON.stringify({ provider: 'payroll_artifact', action: 'register_xlsx', params }),
});
if (!submitted.ok) throw new Error(`제출 실패 ${submitted.status}`);
const { job_id: jobId } = await submitted.json();
// 2. 끝날 때까지 상태를 확인합니다.
let job;
do {
await new Promise((resolve) => setTimeout(resolve, (job?.retry_after_seconds ?? 1) * 1000));
job = await (await fetch(`${api}/v1/jobs/${jobId}`, { headers })).json();
} while (job.status === 'QUEUED' || job.status === 'RUNNING');
if (job.status !== 'SUCCEEDED') {
// 입력값 문제는 provider_error_ref 로 가립니다(예: payroll_artifact.missing_birth_date). 값을 고친 뒤 새 Idempotency-Key 로 다시 제출합니다.
throw new Error(`${job.error.code} ${job.error.provider_error_ref}: ${job.error.message}`);
}
// 3. 결과의 파일 목록에서 주소를 받아 내려받습니다. 주소 자체가 서명돼 있어 인증 헤더를 붙이지 않습니다.
const result = await (await fetch(`${api}/v1/jobs/${jobId}/result`, { headers })).json();
const [artifact] = result.result.artifacts;
const file = await fetch(artifact.download_url);
await writeFile(artifact.name, Buffer.from(await file.arrayBuffer()));import base64
import os
import time
import requests
api = os.environ.get("XBOSS_API_URL", "https://api.xdata.kr")
auth = base64.b64encode(
f"{os.environ['XBOSS_CLIENT_ID']}:{os.environ['XBOSS_CLIENT_SECRET']}".encode()
).decode()
headers = {"Authorization": f"Basic {auth}", "X-Env-Scope": "real_test"}
params = {
"attribution_ym": "202608",
"workplace_name": "예시상사",
"business_registration_no": "000-00-00000",
"workplace_address": "서울특별시 예시구 예시로 1",
"employees": [
{
"employee_key": "E001",
"employee_name": "홍길동",
"birth_date": "1990-01-15",
"dependent_count": 1,
"children_count_8_to_20": 0,
"monthly_pay_won": 3000000,
"base_salary_won": 3000000,
"national_pension_won": 135000,
"health_insurance_won": 106350,
"long_term_care_won": 13770,
"employment_insurance_won": 27000,
"income_tax_won": 74350,
"local_income_tax_won": 7430,
"total_deduction_won": 363900,
"net_pay_won": 2636100
},
{
"employee_key": "E002",
"employee_name": "김가나",
"birth_date": "1985-06-30",
"dependent_count": 3,
"children_count_8_to_20": 1,
"monthly_pay_won": 3500000,
"base_salary_won": 3500000,
"national_pension_won": 157500,
"health_insurance_won": 124075,
"long_term_care_won": 16065,
"employment_insurance_won": 31500,
"income_tax_won": 38960,
"local_income_tax_won": 3890,
"total_deduction_won": 371990,
"net_pay_won": 3128010
}
]
}
# 1. 작업을 제출합니다. 같은 요청을 다시 보낼 때는 같은 Idempotency-Key 를 씁니다.
submitted = requests.post(
f"{api}/v1/jobs",
headers={**headers, "Idempotency-Key": "payroll-register-202608-001"},
json={"provider": "payroll_artifact", "action": "register_xlsx", "params": params},
timeout=30,
)
submitted.raise_for_status()
job_id = submitted.json()["job_id"]
# 2. 끝날 때까지 상태를 확인합니다.
while True:
job = requests.get(f"{api}/v1/jobs/{job_id}", headers=headers, timeout=30).json()
if job["status"] not in ("QUEUED", "RUNNING"):
break
time.sleep(job.get("retry_after_seconds") or 1)
if job["status"] != "SUCCEEDED":
# 입력값 문제는 provider_error_ref 로 가립니다(예: payroll_artifact.missing_birth_date). 값을 고친 뒤 새 Idempotency-Key 로 다시 제출합니다.
error = job["error"]
raise RuntimeError(f"{error['code']} {error['provider_error_ref']}: {error['message']}")
# 3. 결과의 파일 목록에서 주소를 받아 내려받습니다. 주소 자체가 서명돼 있어 인증 헤더를 붙이지 않습니다.
result = requests.get(f"{api}/v1/jobs/{job_id}/result", headers=headers, timeout=30).json()
artifact = result["result"]["artifacts"][0]
file = requests.get(artifact["download_url"], timeout=60)
file.raise_for_status()
with open(artifact["name"], "wb") as handle:
handle.write(file.content)