개발자 문서
개발자 문서

전체 예제

인증, 작업 제출, 상태 확인, 결과 조회까지 한 파일로 도는 예제입니다. 그대로 실행해 볼 수 있습니다.

이 예제는 Python(requests) 또는 Node.js 18 이상과 허용된 API 키가 필요합니다. 데모(real_test)와 정식(production)은 실제 기관을 호출하므로 실행 전 조회 기간과 권한을 확인하세요. 요청 기록을 실행 폴더의 xdata-request.json에 저장하며, 같은 파일로 재실행하면 기존 Job을 이어서 조회하거나 같은 키와 본문으로 접수를 복구합니다. 동시에 두 프로세스를 실행하지 마세요. 기록이 꼬였다고 파일을 지우고 다시 실행하면 안 됩니다 — 원래 요청부터 확인하세요.

이 예제가 부르는 hometax.etax.invoice.search_list 는 등록된 자격증명이 필요합니다 — sandbox 를 포함한 모든 환경에서 그렇습니다(기관마다 다릅니다). 이미 등록했다면 자격증명 등록에서 account_link_id 를 확인해 아래 준비 단계에서 XDATA_ACCOUNT_LINK_ID 로 설정하세요. 아직 등록하지 않았다면 콘솔에서 먼저 등록하세요 — 등록한 자격증명이 그 기관에 하나뿐이면 생략해도 자동으로 선택됩니다.

Windows PowerShell 실행 준비

아래 코드를 편집기에 저장하고 그 폴더에서 PowerShell을 여세요(파일 탐색기 주소 표시줄에 powershell 입력). Node.js와 Python 예제는 같은 폴더에서 하나만 실행합니다. Secret은 숨김 입력으로 받아 현재 프로세스에만 설정하고 실행 후 제거합니다.

powershell
$env:XDATA_ENV = Read-Host '환경: sandbox / real_test / production'
$env:XDATA_CLIENT_ID = Read-Host 'Client ID'
$env:XDATA_ACCOUNT_LINK_ID = Read-Host 'account_link_id (선택 - 등록한 자격증명이 하나뿐이면 비워 두세요)'
$secretInput = Read-Host 'Client Secret' -AsSecureString
try {
  $env:XDATA_CLIENT_SECRET = [System.Net.NetworkCredential]::new('', $secretInput).Password
  python ./xdata_quickstart.py
  # Node.js 를 선택했다면 위 python 명령 대신 node ./xdata-quickstart.mjs
} finally {
  Remove-Item Env:XDATA_CLIENT_SECRET
  $secretInput.Dispose()
}

제출부터 결과까지

bash
Node.js: 추가 패키지 설치 없음 (18 이상)
Python: python -m pip install requests
xdata-quickstart.mjs
javascript
// 아래 PowerShell 준비 명령 후: node xdata-quickstart.mjs
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const BASE = 'https://api.xdata.kr';
const ENV_SCOPE = process.env.XDATA_ENV;
if (!['sandbox', 'real_test', 'production'].includes(ENV_SCOPE)) throw new Error('XDATA_ENV 확인');
if (!process.env.XDATA_CLIENT_ID || !process.env.XDATA_CLIENT_SECRET) {
  throw new Error('XDATA_CLIENT_ID와 XDATA_CLIENT_SECRET 을 설정하세요.');
}
const requestFile = './xdata-request.json';
const body = {
  provider: 'hometax', action: 'hometax.etax.invoice.search_list',
  // 등록된 자격증명이 하나뿐이면 생략해도 자동으로 선택됩니다. hometax 는 sandbox 에서도
  // 자동 선택 대상 자격증명이 없으면 실패합니다 - 준비 단계에서 비워 뒀다면 먼저
  // [자격증명 등록](/common-guide/account-link) 부터 하세요.
  ...(process.env.XDATA_ACCOUNT_LINK_ID ? { account_link_id: process.env.XDATA_ACCOUNT_LINK_ID } : {}),
  params: { from_date: '2026-01-01', to_date: '2026-01-31' },
};
const identity = JSON.stringify({ base: BASE, environment: ENV_SCOPE, client: process.env.XDATA_CLIENT_ID, body });
// 같은 파일로 재실행하면 새 작업이 아니라 기존 접수를 이어서 확인합니다.
if (!existsSync(requestFile)) writeFileSync(requestFile, JSON.stringify({ identity, key: randomUUID() }), { flag: 'wx', mode: 0o600 });
const request = JSON.parse(readFileSync(requestFile, 'utf8'));
if (request.identity !== identity || typeof request.key !== 'string' || !request.key) throw new Error('기존 요청 기록과 다릅니다. 파일을 지우지 말고 원래 환경, 키, 본문을 확인하세요.');

const token = Buffer.from(
  `${process.env.XDATA_CLIENT_ID}:${process.env.XDATA_CLIENT_SECRET}`,
).toString('base64');

const headers = {
  Authorization: `Basic ${token}`,
  'X-Env-Scope': ENV_SCOPE,
  'Content-Type': 'application/json',
};

async function readError(response) {
  // 오류 본문은 최상위 error 입니다. 없는 주소의 404 처럼 error 가 없는 응답은 detail 문장을 message 로 씁니다.
  const payload = await response.json().catch(() => null);
  return payload?.error ?? { message: typeof payload?.detail === 'string' ? payload.detail : undefined };
}

async function submitJob(body) {
  const response = await fetch(`${BASE}/v1/jobs`, {
    method: 'POST',
    // 재시도할 때는 이 키를 그대로 다시 씁니다. 새 키를 쓰면 작업이 한 번 더 실행됩니다.
    headers: { ...headers, 'Idempotency-Key': request.key },
    signal: AbortSignal.timeout(30000),
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    const error = await readError(response);
    throw new Error(`제출 실패 ${response.status} ${error.code ?? '알 수 없음'}: ${error.message ?? ''}`);
  }
  return response.json();
}

async function waitForResult(jobId) {
  for (let attempt = 0; attempt < 20; attempt++) {
    const response = await fetch(`${BASE}/v1/jobs/${jobId}`, { headers, signal: AbortSignal.timeout(30000) });
    if (!response.ok) throw new Error(`상태 조회 HTTP ${response.status}. 같은 기록으로 재실행하세요.`);
    const job = await response.json();
    console.log(`${job.progress}% ${job.progress_message ?? job.status}`);

    if (job.status === 'SUCCEEDED') {
      const resultResponse = await fetch(`${BASE}/v1/jobs/${jobId}/result`, { headers, signal: AbortSignal.timeout(30000) });
      if (!resultResponse.ok) throw new Error(`결과 조회 HTTP ${resultResponse.status}`);
      return resultResponse.json();
    }
    if (job.status !== 'QUEUED' && job.status !== 'RUNNING') {
      throw new Error(`${job.status}: ${job.error?.code ?? ''}`);
    }
    await new Promise((resolve) => setTimeout(resolve, (job.retry_after_seconds ?? 2) * 1000));
  }
  throw new Error('조회 횟수 한도에 도달했습니다. 같은 기록으로 재실행하여 상태를 확인하세요.');
}

if (!request.job_id) {
  const submitted = await submitJob(body);
  if (typeof submitted.job_id !== 'string' || !submitted.job_id) throw new Error('Job 응답 형식 오류');
  request.job_id = submitted.job_id;
  writeFileSync(requestFile, JSON.stringify(request), { mode: 0o600 });
}
const jobId = request.job_id;

const result = await waitForResult(jobId);
console.log('result_received:', typeof result === 'object' && result !== null);
// 결과 원문은 로그에 출력하지 않습니다. 실제 값은 응답을 그대로 저장해 사용하세요.

예제가 쓰는 규칙

규칙어디서 정하나
Authorization 은 Basic 한 가지인증
account_link_id 는 선택 — 없으면 자동 선택(등록한 자격증명이 하나일 때만)자격증명 등록
X-Env-Scope 로 환경 선택환경
재시도는 같은 Idempotency-Key재시도
retry_after_seconds 만큼 기다렸다 다시 확인작업 처리 흐름

기관·작업마다 필요한 params 는 다릅니다. 예제의 provider 와 action 을 바꾸고, 그 작업의 파라미터는 상품별 API 레퍼런스에서 확인합니다.

이 실행 예제는 오류가 나면 중단합니다. 상태나 결과 조회 실패나 연결 오류는 같은 요청 기록으로 다시 실행해 복구하세요 — 자동으로 새 요청을 만들지 않습니다. 실제 서비스에서는 HTTP 상태와 XBOSS 오류 응답의 error.code 를 함께 보고 재시도할지 판단하세요 — 오류 코드.