개발자 문서
개발자 문서

급여대장과 급여명세서 엑셀 만들기

직원별로 계산한 급여 금액을 받아 급여대장과 급여명세서 엑셀 파일을 만드는 작업 두 개의 준비, 요청, 파일 구조, 결과, 오류 안내입니다.

이 작업이 하는 일

직원별로 이미 계산한 급여 금액을 받아 엑셀 파일(xlsx)을 만드는 작업 두 개입니다.

결과는 짧은 요약(result.data)과 파일 목록(result.artifacts)이고, 파일마다 내려받는 서명된 주소(download_url)가 붙습니다.

작업2개(아래 표)
RESTPOST /v1/jobs (provider payroll_artifact)
MCPPOST https://api.xdata.kr/mcp (tools/call)
인증Authorization: Basic
환경 헤더X-Env-Scope (REST, 필수), X-MCP-Env-Scope (MCP, 선택)
결과result.artifacts[].download_url
주소 유효 시간15분

이 작업은 금액을 계산하지 않고 보낸 값을 그대로 파일에 적습니다.

작업 두 개

작업REST actionMCP 도구만드는 파일결과 스키마
급여대장 엑셀 생성register_xlsxpayroll_artifact__register_xlsx급여대장_20260825.xlsx: 직원마다 한 행이 있는 표 한 장payroll_artifact.register_xlsx.v1
급여명세서 엑셀 생성payslip_xlsxpayroll_artifact__payslip_xlsx급여명세서_20260825.xlsx: 직원마다 시트가 하나씩 있는 파일 하나payroll_artifact.payslip_xlsx.v1

부르기 전에 준비할 것

  1. 계산을 마친 금액을 준비합니다

    직원별 월 급여, 국민연금, 건강보험, 장기요양보험, 고용보험, 소득세, 지방소득세, 공제계, 실수령액을 원 단위 정수로 준비합니다.

  2. API 키를 준비합니다

    REST 로 부르려면 REST API 빠른 시작의 순서로 API 키를 만듭니다.

요청

같은 요청을 두 방법으로 보낼 수 있습니다.

REST 로 요청하기

bash
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
      }
    ]
  }
}'

MCP 로 요청하기(급여대장, 급여명세서는 name 과 arguments 만 바꿉니다)

bash
# 급여대장 엑셀 만들기
# 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_ymstring필수귀속연월입니다. YYYYMM 여섯 자리 숫자 문자열입니다(예: 202608). 파일 위쪽의 귀속연월 칸에 적힙니다.202608
employeesarray필수직원 목록입니다. 한 명 이상이어야 하고 한 명이 급여대장의 한 행, 급여명세서의 한 시트가 됩니다. 직원 한 명의 칸은 아래 표에 있습니다.[{ ... }, { ... }]
workplace_namestring선택사업장명입니다. 파일 위쪽의 사업장명 칸에 적힙니다. company_name 도 같은 뜻으로 읽고, 둘 다 있으면 workplace_name 이 우선합니다.예시상사
company_namestring선택workplace_name 의 다른 이름입니다. workplace_name 이 없을 때만 씁니다.—
business_registration_nostring선택사업자등록번호입니다. 입력한 글이 그대로 적힙니다.000-00-00000
workplace_addressstring선택사업장 주소입니다. 파일 위쪽의 사업장 주소 칸에 적힙니다.서울특별시 예시구 예시로 1

급여명세서 엑셀 생성 요청 칸

필드타입필수설명예시
attribution_ymstring필수귀속연월입니다. YYYYMM 여섯 자리 숫자 문자열입니다(예: 202608). 파일 위쪽의 귀속연월 칸에 적힙니다.202608
payment_datestring필수급여 지급일입니다. YYYY-MM-DD 형식이고 달력에 있는 날짜여야 합니다(예: 2026-08-25). 급여명세서에만 있습니다.2026-08-25
employeesarray필수직원 목록입니다. 한 명 이상이어야 하고 한 명이 급여대장의 한 행, 급여명세서의 한 시트가 됩니다. 직원 한 명의 칸은 아래 표에 있습니다.[{ ... }, { ... }]
workplace_namestring필수사업장명입니다. 파일 위쪽의 사업장명 칸에 적힙니다. company_name 도 같은 뜻으로 읽고, 둘 다 있으면 workplace_name 이 우선합니다.예시상사
company_namestring선택workplace_name 의 다른 이름입니다. workplace_name 이 없을 때만 씁니다.—
business_registration_nostring필수사업자등록번호입니다. 입력한 글이 그대로 적힙니다.000-00-00000
workplace_addressstring필수사업장 주소입니다. 파일 위쪽의 사업장 주소 칸에 적힙니다.서울특별시 예시구 예시로 1

직원 한 명의 칸

두 작업의 employees 항목은 같은 칸을 씁니다.

직원 한 명의 칸(employees 의 한 항목)

필드타입필수설명예시
employee_keystring필수요청 안에서 직원을 가르는 값입니다. 필수이지만 파일에는 적히지 않습니다.E001
employee_namestring필수직원 이름입니다. 급여대장의 이름 열, 급여명세서의 성명과 시트 이름에 적힙니다.홍길동
birth_datestring필수생년월일입니다. YYYY-MM-DD 또는 YYYYMMDD 문자열이고 입력한 글이 그대로 적힙니다.1990-01-15
dependent_countinteger필수부양가족 수입니다. 본인을 포함해 1 이상의 정수입니다.1
children_count_8_to_20integer선택8세 이상 20세 이하 자녀 수입니다. 0 이상의 정수입니다. 월 급여(monthly_pay_won 또는 monthly_salary_won)를 보내면 생략할 수 있고 생략하면 0 으로 적힙니다. 급여명세서에 월 급여 없이 gross_pay_won 만 보낼 때는 생략할 수 없습니다(payroll_artifact.missing_children_count_8_to_20 으로 거절됩니다). 값은 급여대장에만 나옵니다.0
monthly_pay_woninteger필수월 급여(지급합계)입니다. 급여대장의 월지급액과 급여명세서의 지급합계에 적힙니다. monthly_salary_won 과 함께 보내면 이 값이 우선합니다.3000000
monthly_salary_woninteger선택monthly_pay_won 의 다른 이름입니다. monthly_pay_won 이 없을 때만 씁니다.—
base_salary_woninteger선택기본급입니다. 급여대장의 기본급 열과 급여명세서의 기본급에 적힙니다. 급여대장의 기본급 열에는 받은 값이 그대로 적힙니다.3000000
basic_salary_woninteger선택급여명세서의 기본급으로 읽습니다. base_salary_won 과 함께 보내면 이 값이 우선합니다. 급여대장의 기본급 열은 이 칸을 읽지 않고 base_salary_won 만 읽습니다.—
national_pension_woninteger필수국민연금 공제액입니다(원 단위 정수).135000
health_insurance_woninteger필수건강보험 공제액입니다(원 단위 정수).106350
long_term_care_woninteger필수장기요양보험 공제액입니다(원 단위 정수).13770
employment_insurance_woninteger필수고용보험 공제액입니다(원 단위 정수).27000
income_tax_woninteger필수소득세 공제액입니다(원 단위 정수). 이 작업은 세액을 계산하지 않고 보낸 값을 적습니다.74350
local_income_tax_woninteger필수지방소득세 공제액입니다(원 단위 정수). 이 작업은 세액을 계산하지 않고 보낸 값을 적습니다.7430
total_deduction_woninteger필수공제계입니다(원 단위 정수). 보낸 값이 그대로 적히고 공제 항목의 합과 맞는지는 검사하지 않습니다.363900
net_pay_woninteger필수실수령액입니다(원 단위 정수). 보낸 값이 그대로 적히고 월 급여에서 공제계를 뺀 값과 맞는지는 검사하지 않습니다.2636100
gross_pay_woninteger선택급여명세서의 지급합계입니다. monthly_pay_won 과 monthly_salary_won 이 모두 없을 때만 쓰고, 이때는 children_count_8_to_20 도 보내야 합니다. 급여대장은 이 칸을 월 급여로 읽지 않습니다.—

급여명세서는 공제 항목 여섯 칸(국민연금, 건강보험, 장기요양보험, 고용보험, 소득세, 지방소득세)을 검사하지 않습니다.

급여대장 파일

직원 한 명이 한 행이 되고 직원 행은 5행부터 채워집니다. 파일 위쪽에는 아래 표의 네 칸이 적힙니다.

파일 위쪽 칸

이름값이 적히는 칸입력 칸예시 값
귀속연월B1attribution_ym202608
사업장명G1workplace_name(없으면 company_name)예시상사
사업자등록번호M1business_registration_no000-00-00000
사업장 주소B2workplace_address서울특별시 예시구 예시로 1

열은 아래와 같습니다. 입력 칸이 선언돼 있지 않은 수당 열은 보내지 않으면 비어 있고, 이 안내는 그 열에 값을 보내는 방법을 다루지 않습니다.

열

열머리 글입력 칸비고
A번호-1 부터 세는 번호가 자동으로 적힙니다
B이 름employee_name
C생년월일birth_date
D부양가족수dependent_count
E8세 이상 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 입니다.

json
{
  "번호": 1,
  "이 름": "홍길동",
  "생년월일": "1990-01-15",
  "부양가족수": 1,
  "8세 이상 20세 이하 자녀수": 0,
  "기본급": 3000000,
  "식대": null,
  "시간외 수당": null,
  "성과수당": null,
  "직책수당": null,
  "연차수당": null,
  "월지급액": 3000000,
  "국민연금": 135000,
  "건강보험": 106350,
  "고용보험": 27000,
  "장기노인 요양보험": 13770,
  "소득세": 74350,
  "지방 소득세": 7430,
  "공제계": 363900,
  "차인지급액": 2636100
}

직원 행 다음 줄(예시에서는 7행)에 「총 합 계」 행이 붙습니다.

급여명세서 파일

직원마다 시트가 하나씩 만들어지고 시트 이름은 직원 이름입니다. 아래는 위 요청 예시의 첫 직원 시트입니다.

시트 이름 규칙

상황예시결과
이름이 같은 직원홍길동, 홍길동홍길동, 홍길동_2 (뒤의 시트에 번호가 붙습니다)
시트 이름에 쓸 수 없는 글자이름에 \ / * ? : [ ] 가 들어 있을 때그 글자를 지우고 남은 글로 시트 이름을 만듭니다
긴 이름42글자28글자까지만 시트 이름으로 씁니다

위쪽 칸

이름값이 적히는 칸입력 칸예시 값
귀속연월A4attribution_ym202608
급여지급일C4payment_date2026-08-25
사업장명E4workplace_name(없으면 company_name)예시상사

근로자 정보 칸

이름값이 적히는 칸입력 칸예시 값비고
성명B6employee_name홍길동
사원번호D6--선언된 입력 칸이 없어 보내지 않으면 비어 있습니다
부서F6--선언된 입력 칸이 없어 보내지 않으면 비어 있습니다
직급H6--선언된 입력 칸이 없어 보내지 않으면 비어 있습니다
생년월일B7birth_date1990-01-15
입사일D7--선언된 입력 칸이 없어 보내지 않으면 비어 있습니다
사업자등록번호B8business_registration_no000-00-00000
사업장 주소F8workplace_address서울특별시 예시구 예시로 1

지급 항목

항목입력 칸예시 금액(원)계산방법 칸의 글
기본급base_salary_won(또는 basic_salary_won)3,000,000월 고정급
직책수당---
식대---
연장근로수당---
야간근로수당---
휴일근로수당---
상여금---
성과금---
기타수당---

공제 항목

항목입력 칸예시 금액(원)산정기준/비고 칸의 글
국민연금national_pension_won135,000기관 조회 결과 반영
건강보험health_insurance_won106,350기관 조회 결과 반영
장기요양보험long_term_care_won13,770기관 조회 결과 반영
고용보험employment_insurance_won27,000기관 조회 결과 반영
소득세income_tax_won74,350입력된 세액 반영
지방소득세local_income_tax_won7,430입력된 세액 반영
기타공제---

합계

항목값이 적히는 칸입력 칸예시 금액(원)
지급합계B20monthly_pay_won(없으면 monthly_salary_won, 그것도 없으면 gross_pay_won)3,000,000
공제합계F20total_deduction_won363,900
차인지급액(실지급액)G22net_pay_won2,636,100

지급 항목의 계산방법 칸과 공제 항목의 산정기준/비고 칸에 적히는 글은 항목마다 정해진 문장이고 금액이 정수로 읽히며 0 이 아닐 때 적힙니다.

결과 받기

REST 로 제출하면 곧바로 job_id 가 오고(HTTP 202), 파일은 작업이 끝난 뒤 결과 조회로 받습니다.

json
{
    "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 와 파일 이름, 크기만 다릅니다.

json
{
  "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분 동안만 유효합니다.

MCP 로 부르면 도구가 파일이 만들어질 때까지 기다렸다가 결과를 한 번에 돌려줍니다.

json
{
  "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.codeerror.provider_error_ref
귀속연월(attribution_ym)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_attribution_ym
귀속연월이 YYYYMM 여섯 자리가 아닐 때(예: 2026-08)VALIDATION_INVALID_FIELDpayroll_artifact.invalid_attribution_ym
직원 목록(employees)이 비어 있을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_employees
직원 항목이 객체가 아닐 때VALIDATION_INVALID_FIELDpayroll_artifact.invalid_employee
직원의 employee_key 가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_employee_key
직원의 employee_name 이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_employee_name
직원의 birth_date 가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_birth_date
공제계(total_deduction_won)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
실수령액(net_pay_won)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
부양가족 수(dependent_count)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_dependent_count
부양가족 수가 1 보다 작을 때(본인을 포함해 1 이상)VALIDATION_INVALID_FIELDpayroll_artifact.invalid_dependent_count
자녀 수(children_count_8_to_20)가 0 보다 작을 때VALIDATION_INVALID_FIELDpayroll_artifact.invalid_children_count_8_to_20
생년월일(birth_date)에 글이나 숫자가 아닌 값(목록이나 객체)을 넣었을 때INTERNAL_ERRORpayroll_artifact.internal
월 급여(monthly_pay_won)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
금액 칸(예: national_pension_won)이 정수로 읽히지 않을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
공제 금액 칸(예: health_insurance_won)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount

급여명세서 엑셀 생성의 입력 오류

조건error.codeerror.provider_error_ref
귀속연월(attribution_ym)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_attribution_ym
귀속연월이 YYYYMM 여섯 자리가 아닐 때(예: 2026-08)VALIDATION_INVALID_FIELDpayroll_artifact.invalid_attribution_ym
직원 목록(employees)이 비어 있을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_employees
직원 항목이 객체가 아닐 때VALIDATION_INVALID_FIELDpayroll_artifact.invalid_employee
직원의 employee_key 가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_employee_key
직원의 employee_name 이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_employee_name
직원의 birth_date 가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_birth_date
공제계(total_deduction_won)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
실수령액(net_pay_won)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
부양가족 수(dependent_count)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_dependent_count
부양가족 수가 1 보다 작을 때(본인을 포함해 1 이상)VALIDATION_INVALID_FIELDpayroll_artifact.invalid_dependent_count
자녀 수(children_count_8_to_20)가 0 보다 작을 때VALIDATION_INVALID_FIELDpayroll_artifact.invalid_children_count_8_to_20
생년월일(birth_date)에 글이나 숫자가 아닌 값(목록이나 객체)을 넣었을 때INTERNAL_ERRORpayroll_artifact.internal
급여 지급일(payment_date)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_payment_date
급여 지급일이 YYYY-MM-DD 가 아닐 때(예: 2026/08/25)VALIDATION_INVALID_FIELDpayroll_artifact.invalid_payment_date
급여 지급일이 달력에 없는 날짜일 때(예: 2026-02-30)VALIDATION_INVALID_FIELDpayroll_artifact.invalid_payment_date
사업장명(workplace_name 또는 company_name)이 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_company_name
사업자등록번호(business_registration_no)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_business_registration_no
사업장 주소(workplace_address)가 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_workplace_address
월 급여(monthly_pay_won)와 지급합계(gross_pay_won)가 모두 없을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount
실수령액(net_pay_won)이 정수로 읽히지 않을 때VALIDATION_INVALID_FIELDpayroll_artifact.missing_or_invalid_amount

아래는 REST 로 상태를 조회했을 때 받는 실패 응답입니다. 급여명세서도 같은 모양이고 action 과 provider_error_ref 는 조건에 따라 다릅니다.

json
{
  "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)로 옵니다.

json
{
  "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 으로 풀면 아래와 같습니다.

json
{
  "job_id": "job_01JC8YH8R0E1ABCDEFGHJKMNPQ",
  "error_code": "VALIDATION_INVALID_FIELD",
  "safe_to_retry": false
}

우리 쪽에서 파일을 만들지 못했을 때

입력 표의 INTERNAL_ERROR 두 행은 값의 형식이 어긋난 경우라 값을 고치면 풀립니다.

상황error.codeerror.provider_error_refretryable
파일을 만드는 중 예상하지 못한 오류가 난 경우INTERNAL_ERRORpayroll_artifact.internalfalse
제출한 뒤 파일 저장소가 준비되지 않아 작업을 시작하지 않은 경우INTERNAL_ERRORjob_artifacts.storage_not_readyfalse
만든 파일을 저장하지 못한 경우INTERNAL_ERRORjob_artifacts.store_failedfalse

알아 둘 점

파일에 적히는 글이 =, +, -, @ 로 시작하면 엑셀이 수식으로 실행하지 않도록 글 칸으로 적습니다.

코드로 급여대장 받기(REST)

payroll-register.mjs
javascript
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()));