모든 계좌를 한 번에 호출하기
은행 연동에 계좌가 여러 개일 때, 계좌마다 따로 부르지 않고 한 번의 호출로 접수하는 방법입니다. 요청, 응답, 이어받기, 멈춘 호출, MCP 사용법을 안내합니다.
이 방식이 하는 일
은행 연동에 계좌가 여러 개일 때 계좌마다 작업을 따로 요청하지 않고, 한 번의 호출로 모두 접수하는 방법입니다.호출 한 번이 계좌마다 작업 하나를 만들고, 작업마다 상태와 결과가 따로 있습니다. 한 계좌가 실패해도 다른 계좌의 결과는 그대로 받습니다.
같은 연동(account_link_id)으로 낸 작업은 한 번에 하나씩 실행됩니다.앞선 작업이 RUNNING 인 동안 뒤 작업은 QUEUED 로 기다립니다.요청에서 account_link_id 를 빼도 서버가 대표 연동을 골라 채우면 그 연동의 순서에 들고, 연동 없이 실행되는 작업(예: 로그인)만 이 순서에 들지 않습니다.다른 연동의 작업이 먼저 접수돼 있으면 그 뒤에서도 QUEUED 로 기다릴 수 있습니다. 한 연동에 계좌가 많으면 뒤 계좌일수록 결과가 늦게 나옵니다.
결과를 한꺼번에 돌려주지는 않습니다. REST 응답에는 계좌마다 작업 번호가 있고, 그 번호로 기존 조회(GET /v1/jobs/{job_id}, GET /v1/jobs/{job_id}/result)를 불러 결과를 읽습니다.MCP 는 같은 호출 안에서 최대 60초 동안 낸 작업들을 함께 기다리고, 그 안에 끝난 줄은 결과를 바로 줍니다.
- STEP 01계좌 확인
은행의 계좌 목록 액션으로 연결된 계좌를 확인합니다.
- STEP 02한 번에 요청
모든 계좌 호출을 한 번 보냅니다.
- STEP 03계좌마다 접수
계좌마다 작업이 하나씩 만들어집니다.
- STEP 04결과 읽기
작업 번호로 계좌마다 결과를 읽습니다.
| 접수 | POST /v1/jobs/all-accounts (202) |
|---|---|
| 결과 읽기 | GET /v1/jobs/{job_id}/result |
| 인증 | Authorization: Basic |
| 환경 헤더 | X-Env-Scope (필수) |
| 멱등 키 | Idempotency-Key 헤더 (필수) |
| 사용 토큰 | 계좌마다 차감 |
| MCP 입력 | all_accounts 또는 bank_account_credential_ids |
계좌마다 사용 토큰이 하나씩 듭니다. 기업정보 브리프처럼 한 번에 여러 사용 토큰을 쓰는 업무는 그 값만큼 계좌마다 듭니다.
접수 전에 확인한 사용 토큰이 모자라면 일부만 접수하지 않고 하나도 접수하지 않으며, 요청 전체를 거절합니다.거절 문장에 필요한 토큰과 지금 남은 토큰이 있습니다. 확인한 뒤 다른 호출이 토큰을 써서, 한 줄 이상 접수한 뒤에 모자라게 되면 그때까지 접수한 줄은 그대로 두고 호출이 멈춥니다(아래 「호출이 중간에 멈췄을 때」).첫 줄부터 모자라면 아무것도 접수하지 않고 요청 전체를 거절합니다.
부를 수 있는 상품
| 상품 | provider | action | MCP 도구 |
|---|---|---|---|
| IBK기업은행 빠른조회 계좌 잔액 요약 조회 | ibk_fast_account | ibk_fast_account.accounts.quick_summary.get | ibk_fast_account__accounts__quick_summary__get |
| KB국민은행 빠른조회 거래내역 조회 | kb_fast_account | kb_fast_account.accounts.transaction_history.search | kb_fast_account__accounts__transaction_history__search |
| 신한은행 간편조회 거래내역 조회 | shinhan_easy | shinhan_easy.accounts.transaction_history.search | shinhan_easy__accounts__transaction_history__search |
| 우리은행 빠른조회 거래내역 조회 | woori_bank | woori_bank.accounts.transaction_history.search | - |
MCP 도구 칸이 - 인 상품은 지금 공개 MCP 도구 목록에 그 도구가 없습니다.
계좌를 하나 고르는 칸은 위 상품 모두 bank_account_credential_id 입니다.계좌를 고르지 않는 액션(로그인, 계좌 목록 등)은 이 호출로 부를 수 없고, 부르면 거절합니다. 계좌의 식별자는 은행의 계좌 목록 액션이 돌려주는 값입니다.
계좌를 고르는 세 가지 방법
| 방법 | REST | MCP |
|---|---|---|
| 계좌 하나 | 이 호출을 쓰지 않고 POST /v1/jobs 의 params 에 bank_account_credential_id 를 넣습니다. | 도구 입력 bank_account_credential_id 를 보냅니다. 전과 같습니다. |
| 고른 계좌 여럿 | 본문에 bank_account_credential_ids 를 넣습니다. | 도구 입력 bank_account_credential_ids 를 보냅니다. |
| 모든 계좌 | bank_account_credential_ids 를 빼고 부릅니다. 이 경로가 「모든 계좌」 입니다. | 도구 입력 all_accounts: true 를 보냅니다. |
연동 하나의 계좌만 부르려면 account_link_id 를 함께 보냅니다(REST 는 본문 칸, MCP 는 도구 입력).모든 계좌 호출은 이 경로(REST)나 all_accounts 입력(MCP)을 직접 보냈을 때만 일어납니다. 계좌를 고르지 않은 단건 호출이 조용히 모든 계좌로 바뀌는 일은 없습니다.
접수 - POST /v1/jobs/all-accounts
새로 접수한 줄이 하나라도 있으면 HTTP 202, 새로 접수한 줄이 없으면 200 입니다.새로 접수한 줄이 없는 경우는 같은 Idempotency-Key 로 다시 보냈거나 모든 줄을 접수하지 못한 때입니다. 접수는 계좌마다 작업을 만들고 바로 돌려줍니다. 기관의 결과를 기다리지 않습니다.
요청 헤더
| 헤더 | 필수 | 설명 |
|---|---|---|
| Authorization | 필수 | API 자격증명입니다. client_id 와 client_secret 을 콜론으로 이어 Base64 로 인코딩해 보냅니다. |
| X-Env-Scope | 필수 | 호출 환경입니다. |
| Idempotency-Key | 필수 | 재시도 식별 키입니다. 같은 키와 같은 본문으로 다시 호출하면 기존 작업이 그대로 반환됩니다. ASCII 문자만 사용하며 UUID v4 를 권장합니다. 고객 정보는 넣지 않습니다. |
| Content-Type | 필수 | 요청 본문 형식입니다. |
요청 본문
| 필드 | 타입 | 필수 | 설명 | 예시 |
|---|---|---|---|---|
| provider | string | 필수 | 은행 상품의 provider 입니다(위 표의 provider 열). | — |
| action | string | 필수 | 위 표의 action 을 보냅니다. 계좌를 고르지 않는 액션(로그인, 계좌 목록 등)은 거절합니다. | — |
| account_link_id | string | 선택 | 연동 하나로 좁힐 때 보냅니다. 보내면 그 연동의 계좌만 처리하고, 보내지 않으면 이 은행의 모든 연동을 처리합니다. | — |
| bank_account_credential_ids | array | 선택 | 처리할 계좌의 식별자 목록입니다. 보내지 않으면 계좌 전부를 처리합니다. 빈 목록은 전부로 읽지 않고 거절하며, 1개 이상이어야 합니다. 아직 접수하지 못한 계좌를 이어서 접수할 때 응답의 next.bank_account_credential_ids 를 그대로 넣습니다. | — |
| params | object | 선택 | 액션의 조회 조건입니다. 칸의 뜻은 상품 페이지의 요청 표를 봅니다. 계좌를 하나 고르는 칸(bank_account_credential_id)은 넣지 않습니다. 이 호출이 계좌마다 채우며, 값을 넣으면 거절합니다. 계좌 목록(bank_account_credential_ids)도 여기에 넣지 않고 요청 맨 위에 둡니다. 넣으면 거절합니다. | — |
본문에서 모르는 이름을 보내면 조용히 무시하지 않고 거절합니다.단건 호출의 bank_account_credential_id 를 본문 최상위에 잘못 보내도 마찬가지입니다. 한 번의 실수가 계좌 수만큼의 사용 토큰이 되지 않게 하려는 규칙입니다.
접수 요청
curl --request POST \
--url 'https://api.xdata.kr/v1/jobs/all-accounts' \
--header 'Authorization: Basic {base64(client_id:client_secret)}' \
--header 'X-Env-Scope: real_test' \
--header 'Idempotency-Key: demo-all-accounts-001' \
--header 'Content-Type: application/json' \
--data '{
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"params": {
"start_date": "2026-09-01",
"end_date": "2026-09-30"
}
}'응답
응답은 계좌마다 한 줄을 담은 봉투입니다. 아래 표의 「있음」 칸은 그 필드가 늘 있는지, 값이 null 일 수 있는지, 칸 자체가 없을 수 있는지를 말합니다.
응답 봉투
| 필드 | 타입 | 있음 | 설명 |
|---|---|---|---|
| env_scope | string | 항상 | 요청한 환경입니다. 요청 헤더 X-Env-Scope 의 값과 같습니다. |
| provider | string | 항상 | 요청한 은행 상품의 provider 입니다. |
| action | string | 항상 | 요청한 액션입니다. |
| submitted_at | string | 항상 | 호출을 받은 시각입니다(ISO 8601, KST +09:00). 계좌마다 작업을 접수한 시각은 GET /v1/jobs/{job_id} 에 있습니다. |
| account_count | number | 항상 | 이 호출이 다룬 계좌 수입니다. |
| submitted_count | number | 항상 | 작업을 접수한 줄 수입니다. 같은 Idempotency-Key 로 다시 보내 기존 작업이 돌아온 줄도 셉니다. |
| accounts | array | 항상 | 계좌마다 한 줄입니다. 최근 등록한 연동의 계좌부터, 같은 연동 안에서는 최근 등록한 계좌부터 나옵니다. |
| next | object | 항상(값이 없으면 null) | 아직 접수하지 못한 계좌가 있을 때만 값이 있고, 없으면 null 입니다. 이어받는 방법은 아래 「이어받기」 를 봅니다. |
| interruption | object | 없을 수 있음 | 호출이 중간에 멈췄을 때만 있습니다. 멈추지 않았으면 이 칸 자체가 없습니다. |
accounts 의 한 항목(계좌 한 줄)
| 필드 | 타입 | 있음 | 설명 |
|---|---|---|---|
| account_link_id | string | 항상 | 계좌가 속한 연동의 식별자입니다. |
| bank_account_credential_id | string | 항상 | 계좌의 식별자입니다. 은행의 계좌 목록 액션이 돌려주는 값과 같습니다. |
| masked_account_no | string | 항상 | 가려진 계좌번호입니다(예: 110-***-123456). |
| job_id | string | 항상(값이 없으면 null) | 접수한 작업 번호입니다. 이 번호로 GET /v1/jobs/{job_id} 와 GET /v1/jobs/{job_id}/result 를 부릅니다. 접수하지 못한 줄은 null 입니다. |
| status | string | 항상 | 그 작업의 지금 상태이거나 접수하지 못했다는 NOT_SUBMITTED 입니다. 아래 「줄의 상태」 를 봅니다. |
| reason | string | 항상(값이 없으면 null) | NOT_SUBMITTED 줄에만 값이 있습니다. 접수하지 못한 까닭이며 아래 표의 두 값 중 하나입니다. |
줄의 상태
status 는 그 작업의 지금 상태이거나 NOT_SUBMITTED 입니다.작업 상태 여섯 가지(CANCELED, FAILED, QUEUED, RUNNING, SUCCEEDED, TIMEOUT)는 작업 처리 흐름과 같은 낱말입니다.이 응답의 status 는 응답을 만든 순간의 상태이고, 그 뒤의 상태는 GET /v1/jobs/{job_id} 에서 읽습니다. NOT_SUBMITTED 는 아직 작업을 만들지 못한 줄입니다.이 줄은 job_id 가 null 이고 reason 이 까닭을 말합니다.
접수하지 못한 까닭
| reason | 뜻 | 다음에 할 일 |
|---|---|---|
| over_concurrency_limit | 같은 은행 상품으로 동시에 처리할 수 있는 작업 수에 막혀 아직 접수하지 못했습니다. 이 호출이 낸 작업뿐 아니라 같은 상품으로 최근 접수해 아직 끝나지 않은 다른 작업도 셉니다. | 다른 줄의 접수에는 영향이 없습니다. 응답의 next 가 이 계좌들을 줍니다. 앞선 작업이 끝난 뒤 같은 호출을 그 목록으로 다시 보냅니다. |
| interrupted | 앞선 줄을 접수한 뒤 동시 작업 상한이 아닌 다른 거절로 호출이 멈춰, 이 줄과 그 뒤 줄을 접수하지 못했습니다. | 응답의 interruption 이 멈춘 까닭을 말합니다. 까닭을 해결한 뒤 next 의 목록으로 다시 보냅니다. |
next 블록
| 필드 | 타입 | 있음 | 설명 |
|---|---|---|---|
| retry_after_seconds | number | 항상 | 다시 부르기 전에 기다릴 만한 시간(초)입니다. 동시 작업 상한에 막힌 줄을 이어받을 때 뜻이 있습니다. 호출이 멈춘 경우(interruption 이 있을 때)에는 기다려도 풀리지 않을 수 있습니다. |
| bank_account_credential_ids | array | 항상 | 아직 접수하지 못한 계좌의 식별자입니다. 같은 이름의 요청 칸에 그대로 넣어 다시 보냅니다. |
| message | string | 항상 | 아직 접수하지 못한 계좌 수와 다시 보내라는 안내입니다. 까닭과 관계없이 같은 문장이라, interruption 이 있으면 interruption.message 를 먼저 보여 줍니다. |
interruption 블록
| 필드 | 타입 | 있음 | 설명 |
|---|---|---|---|
| code | string | 항상 | 표준 오류 코드입니다. 같은 거절이 계좌 하나로 부른 호출에서 났다면 오류 응답이 싣는 코드와 같습니다. |
| message | string | 항상 | 고객에게 보여 줄 수 있는 문장입니다. |
| reason | string | 항상(값이 없으면 null) | 세부 까닭 코드입니다. 거절이 싣고 있을 때만 값이 있고, 없으면 null 입니다. |
모두 접수된 응답
모든 줄이 새로 접수되면 HTTP 202 이고 next 는 null 입니다.
{
"env_scope": "real_test",
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"submitted_at": "2026-08-25T09:30:00+09:00",
"account_count": 3,
"submitted_count": 3,
"accounts": [
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "3f2b8c1e-6a4d-4e7b-9c15-0a1b2c3d4e51",
"masked_account_no": "110-***-123456",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN01",
"status": "QUEUED",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"masked_account_no": "110-***-654321",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN02",
"status": "QUEUED",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35",
"masked_account_no": "110-***-246810",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN03",
"status": "QUEUED",
"reason": null
}
],
"next": null
}일부를 아직 접수하지 못한 응답
은행에서 동시에 처리할 수 있는 작업 수에 막힌 줄은 오류가 아니라 NOT_SUBMITTED 줄로 남습니다.다른 줄은 그대로 접수되고(HTTP 202), next 가 접수하지 못한 계좌 목록을 줍니다.
{
"env_scope": "real_test",
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"submitted_at": "2026-08-25T09:30:00+09:00",
"account_count": 3,
"submitted_count": 2,
"accounts": [
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "3f2b8c1e-6a4d-4e7b-9c15-0a1b2c3d4e51",
"masked_account_no": "110-***-123456",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN01",
"status": "QUEUED",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"masked_account_no": "110-***-654321",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN02",
"status": "QUEUED",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35",
"masked_account_no": "110-***-246810",
"job_id": null,
"status": "NOT_SUBMITTED",
"reason": "over_concurrency_limit"
}
],
"next": {
"retry_after_seconds": 10,
"bank_account_credential_ids": [
"c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35"
],
"message": "아직 접수하지 못한 계좌가 1개 있습니다. 앞선 작업이 끝난 뒤 같은 호출을 이 계좌들로 다시 하세요."
}
}이어받기
next 가 null 이 아니면 접수하지 못한 계좌가 남은 것입니다. 앞선 작업이 끝난 뒤 같은 호출을 그 계좌들로 다시 보내 이어서 접수합니다.
- next 를 확인합니다
next 가 null 이면 모든 계좌를 접수한 것입니다. 값이 있으면 아래로 이어갑니다.
- 잠시 기다립니다
next.retry_after_seconds 만큼 기다린 뒤 다시 보냅니다.기다리는 동안 앞선 작업이 끝나면 은행의 동시 작업 수에 여유가 생깁니다. 응답에 interruption 이 있으면 기다려도 풀리지 않을 수 있으니 그 까닭을 먼저 해결합니다.
- 같은 호출을 목록으로 다시 보냅니다
본문은 그대로 두고 bank_account_credential_ids 에 next.bank_account_credential_ids 를 넣습니다.회차마다 Idempotency-Key 를 따로 쓰면 어느 회차를 다시 보내도 안전합니다.
같은 요청을 다시 보낼 때
응답을 받지 못했다면 같은 Idempotency-Key 와 같은 본문으로 다시 보냅니다.키는 계좌마다 따로 만들어 쓰므로, 이미 접수한 줄은 새 작업을 만들지 않고 그 작업의 지금 상태로 돌아옵니다. 새로 접수한 줄이 하나도 없으면 HTTP 200 입니다. 그 사이 연동에 계좌가 늘었다면 늘어난 계좌는 새로 접수를 시도합니다.하나라도 새로 접수되면 응답은 202 이고, 동시 작업 상한에 막혀 새로 접수한 줄이 없으면 그 줄은 NOT_SUBMITTED 로 남고 응답은 200 입니다. 늘어난 계좌 몫의 사용 토큰이 모자라면 요청 전체를 거절합니다.작업이 실패로 끝난 뒤 다시 낼 때는 새 키를 쓰는 규칙이 계좌마다 똑같이 적용됩니다. 자세한 규칙은 재시도(멱등성)에 있습니다. 일부 계좌만 실패했다면 그 계좌만 다시 냅니다.새 Idempotency-Key 와 함께 bank_account_credential_ids 에 실패한 계좌만 넣어 보내세요. 이미 끝난 계좌까지 새 키로 다시 보내면 그 계좌들도 새로 조회하는 것이라 사용 토큰이 다시 듭니다.
{
"env_scope": "real_test",
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"submitted_at": "2026-08-25T09:30:00+09:00",
"account_count": 3,
"submitted_count": 3,
"accounts": [
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "3f2b8c1e-6a4d-4e7b-9c15-0a1b2c3d4e51",
"masked_account_no": "110-***-123456",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN01",
"status": "SUCCEEDED",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"masked_account_no": "110-***-654321",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN02",
"status": "RUNNING",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35",
"masked_account_no": "110-***-246810",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN03",
"status": "QUEUED",
"reason": null
}
],
"next": null
}호출이 중간에 멈췄을 때
줄을 몇 개 접수한 뒤 동시 작업 상한이 아닌 다른 거절로 호출이 멈추면 오류로 돌려주지 않습니다.이미 접수한 줄은 그대로 두고, 멈춘 줄과 그 뒤 줄은 NOT_SUBMITTED 와 interrupted 로 남기며, 응답에 interruption 블록이 더해집니다. 새로 접수한 줄이 있으면 응답 코드는 그대로 202 입니다.아래는 접수 도중에 사용 토큰이 모자라게 된 경우입니다.
{
"env_scope": "real_test",
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"submitted_at": "2026-08-25T09:30:00+09:00",
"account_count": 3,
"submitted_count": 1,
"accounts": [
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "3f2b8c1e-6a4d-4e7b-9c15-0a1b2c3d4e51",
"masked_account_no": "110-***-123456",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN01",
"status": "QUEUED",
"reason": null
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"masked_account_no": "110-***-654321",
"job_id": null,
"status": "NOT_SUBMITTED",
"reason": "interrupted"
},
{
"account_link_id": "7d9e5a40-3b1c-4f62-8e07-52c1a9d4b630",
"bank_account_credential_id": "c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35",
"masked_account_no": "110-***-246810",
"job_id": null,
"status": "NOT_SUBMITTED",
"reason": "interrupted"
}
],
"next": {
"retry_after_seconds": 10,
"bank_account_credential_ids": [
"9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35"
],
"message": "아직 접수하지 못한 계좌가 2개 있습니다. 앞선 작업이 끝난 뒤 같은 호출을 이 계좌들로 다시 하세요."
},
"interruption": {
"code": "POLICY_QUOTA_EXCEEDED",
"message": "오늘 쓸 수 있는 사용 토큰을 모두 썼습니다. 내일 0시(한국 시간)에 다시 채워지며, 기다리는 데 비용은 들지 않습니다.",
"reason": null
}
}계좌마다 결과 읽기
job_id 가 있는 줄마다 GET /v1/jobs/{job_id} 로 상태를 보고, 상태가 SUCCEEDED 가 되면 GET /v1/jobs/{job_id}/result 로 결과를 가져옵니다.QUEUED 나 RUNNING 이면 retry_after_seconds 만큼 기다렸다 다시 확인합니다. 한 계좌가 실패해도 다른 계좌의 작업과 결과에는 영향이 없습니다.자세한 흐름은 작업 처리 흐름에 있습니다.
MCP 로 부르기
계좌를 고르는 은행 도구에 입력 두 개가 더해집니다.위 표의 상품이 모두 같은 두 입력을 받습니다. bank_account_credential_id(한 계좌)는 필수에서 빠지지만 입력은 그대로 있어, 한 계좌만 부르는 호출은 전과 같이 됩니다.
더해지는 도구 입력
| 입력 | 타입 | 필수 | 설명 |
|---|---|---|---|
| all_accounts | boolean | 선택 | true 이면 이 은행에 연결된 모든 계좌로 부릅니다. true 나 false 만 받으며, false 이거나 보내지 않으면 청하지 않은 것과 같습니다. |
| bank_account_credential_ids | array | 선택 | 부를 계좌의 식별자 목록입니다. 1개 이상이어야 합니다. |
bank_account_credential_id(한 계좌), bank_account_credential_ids(고른 계좌 여럿), all_accounts(모든 계좌) 중 정확히 하나만 보냅니다.둘 이상 보내면 어느 쪽이 맞는지 알 수 없어 아무것도 처리하지 않고 거절합니다. 아래 「요청 단계의 오류」 를 봅니다.
도구 호출
# 모든 계좌로 한 번에 부르기
# 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": "kb_fast_account__accounts__transaction_history__search",
"arguments": {
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"all_accounts": true
}
}
}'MCP 결과 읽기
호출 하나가 결과 한 덩어리를 돌려줍니다. 낸 작업들을 함께 기다리며(최대 60초), 하나가 끝나 은행의 동시 작업 수에 여유가 생기면 접수하지 못한 줄을 이어서 냅니다.시간이 다 되면 끝난 줄은 결과를, 아직 끝나지 않은 줄은 result_tool_call 을, 접수하지 못한 줄은 NOT_SUBMITTED 와 next 를 줍니다.처음에 접수된 줄이 하나도 없으면(모두 동시 작업 상한에 막힘) 기다리지 않고 바로 NOT_SUBMITTED 와 next 를 돌려줍니다. 스트리밍(SSE)으로 부르면 진행 문구가 계좌 4개 중 2개 처리 끝 꼴의 한국어로 나옵니다.
결과 본문
| 필드 | 타입 | 있음 | 설명 |
|---|---|---|---|
| all_accounts | boolean | 항상 | 「모든 계좌」 호출의 결과라는 표지입니다. 늘 true 입니다. |
| summary | string | 항상 | 계좌 수와 상태별 개수를 한 줄로 요약한 문장입니다. |
| accounts | array | 항상 | 계좌마다 한 줄입니다. 한 줄이 싣는 칸은 상태에 따라 달라집니다(아래 표). |
| next | object | 항상(값이 없으면 null) | 아직 접수하지 못한 계좌가 있을 때만 값이 있고, 없으면 null 입니다. tool_call 이 그 계좌들로 같은 도구를 다시 부르는 호출입니다. |
| interruption | object | 없을 수 있음 | 호출이 중간에 멈췄을 때만 있습니다. 칸의 뜻은 REST 응답의 interruption 과 같고, 같은 거절이면 code 와 reason 도 REST 와 같은 값입니다. message 는 MCP 쪽에 AI 가 읽는 안내가 덧붙을 수 있어 REST 의 문장과 다를 수 있습니다. |
줄의 상태마다 싣는 칸
| status | 더해지는 칸 | 뜻 |
|---|---|---|
| CANCELED, FAILED, TIMEOUT | error | 끝났지만 성공하지 못한 줄에 있습니다. 오류 코드와 문장, 다시 불러도 되는지(safe_to_retry), 먼저 부를 로그인 도구(next_action)가 담깁니다. |
| NOT_SUBMITTED | reason | 접수하지 못한 까닭입니다. over_concurrency_limit 또는 interrupted 입니다. |
| QUEUED, RUNNING | result_tool_call | 아직 끝나지 않은 줄에 있습니다. 이 호출을 그대로 부르면 그 작업의 결과를 읽습니다. |
| SUCCEEDED | result | 단건 호출과 같은 모양의 결과가 들어갑니다. 끝나서 성공한 줄에만 있습니다. |
모든 줄이 싣는 칸은 bank_account_credential_id, masked_account_no, job_id, status 넷입니다. 아래는 네 상태가 섞인 결과입니다.
{
"all_accounts": true,
"summary": "계좌 4개: 완료 1 · 실패 1 · 진행 중 1 · 아직 접수 못 함 1",
"accounts": [
{
"bank_account_credential_id": "3f2b8c1e-6a4d-4e7b-9c15-0a1b2c3d4e51",
"masked_account_no": "110-***-123456",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN01",
"status": "SUCCEEDED",
"result": {
"...": "같은 액션을 계좌 하나로 불렀을 때 받는 결과가 그대로 들어갑니다"
}
},
{
"bank_account_credential_id": "9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"masked_account_no": "110-***-654321",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN02",
"status": "FAILED",
"error": {
"code": "PROVIDER_SESSION_EXPIRED",
"message": "세션이 만료됐거나 없습니다(오류 PROVIDER_SESSION_EXPIRED). 같은 account_link_id 로 'kb_fast_account__session__login' 을 먼저 불러 세션을 연 뒤 이 도구를 다시 부르세요.",
"safe_to_retry": true,
"next_action": "kb_fast_account__session__login"
}
},
{
"bank_account_credential_id": "c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35",
"masked_account_no": "110-***-246810",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN03",
"status": "RUNNING",
"result_tool_call": {
"name": "xdata__job__result",
"arguments": {
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN03"
}
}
},
{
"bank_account_credential_id": "e8b05d13-72a9-4c46-9f1d-3a6b0c85e2f7",
"masked_account_no": "110-***-135790",
"job_id": null,
"status": "NOT_SUBMITTED",
"reason": "over_concurrency_limit"
}
],
"next": {
"tool_call": {
"name": "kb_fast_account__accounts__transaction_history__search",
"arguments": {
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"bank_account_credential_ids": [
"e8b05d13-72a9-4c46-9f1d-3a6b0c85e2f7"
]
}
},
"message": "아직 접수하지 못한 계좌가 1개 있습니다. 앞선 작업이 끝난 뒤 같은 호출을 이 계좌들로 다시 하세요."
}
}isError 는 언제 참인가
도구 결과의 isError 는 모든 줄이 끝났고 하나도 성공하지 못했을 때만 참입니다.하나라도 성공했거나, 진행 중이거나, 접수하지 못한 줄이 있으면 거짓입니다. 부분 성공을 오류로 읽고 통째로 다시 부르면 이미 낸 작업이 다시 만들어지기 때문입니다.
| 상황 | isError | 맨 위 문장 |
|---|---|---|
| 성공한 줄, 실패한 줄, 진행 중인 줄, 접수하지 못한 줄이 섞여 있을 때 | false | - |
| 모든 줄이 성공했을 때 | false | - |
| 모든 줄이 끝났고 하나도 성공하지 못했을 때(줄마다 오류가 있음) | true | 첫 오류 줄의 message |
| 모든 줄이 끝났고 하나도 성공하지 못했을 때(오류 문장이 없는 줄뿐) | true | 계좌 {count}개의 작업이 모두 실패했습니다. |
| 실패한 줄이 있어도 진행 중인 줄이 하나라도 있을 때 | false | - |
MCP 에서 호출이 중간에 멈췄을 때
MCP 도 같은 규칙입니다. 줄을 접수한 뒤 멈춘 호출은 오류가 아니라 결과이고 isError 는 거짓입니다.결과에 interruption 블록이 더해지고 next.tool_call 이 접수하지 못한 계좌 목록을 줍니다.interruption 의 code 와 reason 은 같은 거절이면 REST 와 같은 값이고, message 에는 AI 가 읽는 안내가 덧붙을 수 있습니다.
{
"all_accounts": true,
"summary": "계좌 3개: 완료 1 · 아직 접수 못 함 2",
"accounts": [
{
"bank_account_credential_id": "3f2b8c1e-6a4d-4e7b-9c15-0a1b2c3d4e51",
"masked_account_no": "110-***-123456",
"job_id": "job_01JC8YH8R0E1ABCDEFGHJKMN01",
"status": "SUCCEEDED",
"result": {
"...": "같은 액션을 계좌 하나로 불렀을 때 받는 결과가 그대로 들어갑니다"
}
},
{
"bank_account_credential_id": "9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"masked_account_no": "110-***-654321",
"job_id": null,
"status": "NOT_SUBMITTED",
"reason": "interrupted"
},
{
"bank_account_credential_id": "c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35",
"masked_account_no": "110-***-246810",
"job_id": null,
"status": "NOT_SUBMITTED",
"reason": "interrupted"
}
],
"next": {
"tool_call": {
"name": "kb_fast_account__accounts__transaction_history__search",
"arguments": {
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"bank_account_credential_ids": [
"9a7d4e22-1c8f-4b36-a5d0-6e2f81b3c704",
"c41e6b97-5d02-4a8f-b3e9-17f0d2a68c35"
]
}
},
"message": "아직 접수하지 못한 계좌가 2개 있습니다. 앞선 작업이 끝난 뒤 같은 호출을 이 계좌들로 다시 하세요."
},
"interruption": {
"code": "POLICY_QUOTA_EXCEEDED",
"message": "오늘 쓸 수 있는 사용 토큰을 모두 썼습니다. 다음 날 0시(한국 시간) 에 다시 채워지며, 기다리는 데 비용은 들지 않습니다. 정식 환경은 이 한도를 세지 않습니다. 더 필요하시면 콘솔 결제 화면에서 요금제를 올릴 수 있습니다(https://console.xdata.kr/billing). 사용자에게 두 방법을 모두 알려 주세요.",
"reason": null
}
}요청 단계의 오류
REST
| HTTP | error.code | 언제 |
|---|---|---|
| 429 | POLICY_QUOTA_EXCEEDED | 접수 전에 확인한, 새로 접수할 계좌 수만큼 필요한 사용 토큰이 남은 토큰보다 많을 때입니다. 일부만 접수하지 않고 하나도 접수하지 않으며 요청 전체를 거절합니다. 문장에 계좌 수, 필요한 토큰, 남은 토큰이 있습니다. |
| 404 | BANK_ACCOUNT_CREDENTIAL_NOT_FOUND | 연동에 사용 중인 계좌가 하나도 없거나, bank_account_credential_ids 에 적은 계좌 중 내 사용 중인 계좌가 아닌 것이 하나라도 있을 때입니다. account_link_id 를 함께 보냈으면 그 연동의 사용 중인 계좌만 인정합니다. 일부만 접수하지 않고 전부 거절합니다. |
| 404 | BANK_ACCOUNT_PARENT_NOT_FOUND | account_link_id 로 고른 연동이 내 사용 중인 연동이 아니거나, 이 은행에 연동이 하나도 없을 때입니다. 연동이 없으면 계정을 먼저 연동한 뒤 계좌를 등록합니다. |
| 400 | VALIDATION_INVALID_FIELD | 계좌를 고르지 않는 액션(로그인, 계좌 목록 등)으로 불렀을 때입니다. |
| 400 | VALIDATION_INVALID_FIELD | params 에 bank_account_credential_id 의 값을 넣었을 때입니다. 이 호출이 계좌마다 채우므로 넣지 않습니다. |
| 400 | VALIDATION_INVALID_FIELD | params 안에 bank_account_credential_ids 를 넣었을 때입니다. 값이 비어 있어도 거절하며, 계좌 목록은 요청 맨 위의 같은 이름 칸에 둡니다. |
| 422 | VALIDATION_INVALID_FIELD | 본문에 모르는 이름이 있을 때입니다. error.fields 의 loc 가 그 이름을 알려 줍니다. |
| 422 | VALIDATION_INVALID_FIELD | bank_account_credential_ids 가 빈 목록일 때입니다. |
| 422 | VALIDATION_INVALID_FIELD | provider 나 action 이 없을 때입니다. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | Idempotency-Key 헤더가 없을 때입니다. |
아래는 모르는 이름 bank_account_credential_id 를 본문에 보냈을 때의 실제 응답입니다.
{
"error": {
"code": "VALIDATION_INVALID_FIELD",
"message": "요청 값이 올바르지 않습니다.",
"retryable": false,
"provider_error_ref": null,
"trace_id": "trc_9f2c4d1e0b6a4f7c8d3e5a1b2c4d6e8f",
"fields": [
{
"loc": [
"body",
"bank_account_credential_id"
],
"msg": "Extra inputs are not permitted",
"type": "extra_forbidden"
}
]
}
}아래는 사용 토큰이 모자랄 때의 실제 응답입니다. 문장에 계좌 수, 필요한 토큰, 남은 토큰이 들어 있습니다.
{
"error": {
"code": "POLICY_QUOTA_EXCEEDED",
"message": "계좌 5개를 처리하려면 사용 토큰 5개가 필요한데 지금 3개 남아 있어 하나도 제출하지 않았습니다. 토큰이 채워진 뒤 다시 시도하거나 계좌 수를 줄여 주세요.",
"retryable": false,
"provider_error_ref": null,
"trace_id": "trc_9f2c4d1e0b6a4f7c8d3e5a1b2c4d6e8f"
}
}MCP
| 상황 | JSON-RPC code | data.error_code |
|---|---|---|
| 접수 전 확인에서 필요한 사용 토큰이 남은 토큰보다 많을 때(하나도 접수하지 않음) | -32001 | POLICY_QUOTA_EXCEEDED |
| 사용 중인 계좌가 없거나 목록에 내 계좌가 아닌 것이 있을 때 | -32602 | BANK_ACCOUNT_CREDENTIAL_NOT_FOUND |
| account_link_id 로 고른 연동이 내 사용 중인 연동이 아니거나 이 은행에 연동이 하나도 없을 때 | -32602 | BANK_ACCOUNT_PARENT_NOT_FOUND |
| bank_account_credential_id 의 값과 bank_account_credential_ids 또는 all_accounts: true 를 함께 보냈을 때 | -32602 | - |
| all_accounts: true 와 bank_account_credential_ids 를 함께 보냈을 때 | -32602 | - |
| 계좌를 고르지 않는 도구(로그인, 계좌 목록 등)에 「모든 계좌」 를 청했을 때 | -32602 | - |
접수부터 결과까지
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 sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
const body = {
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"params": {
"start_date": "2026-09-01",
"end_date": "2026-09-30"
}
};
// 1. 모든 계좌를 접수합니다. 접수하지 못한 계좌가 남으면 응답의 next 로 이어서 접수합니다.
// 회차마다 Idempotency-Key 를 따로 씁니다. 응답을 받지 못해 같은 회차를 다시 보낼 때만 같은 키를 씁니다.
// 호출이 멈췄거나(interruption) 이어받는 회차가 거절되면, 이미 접수한 작업을 잃지 않도록 여기서 멈추고 2단계로 갑니다.
// 남은 계좌는 까닭을 해결한 뒤 새 Idempotency-Key 와 next 의 목록으로 다시 보냅니다.
const lines = [];
let request = body;
for (let round = 1; ; round += 1) {
const submitted = await fetch(`${api}/v1/jobs/all-accounts`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': `monthly-2026-09-r${round}` },
body: JSON.stringify(request),
});
if (submitted.status !== 202 && submitted.status !== 200) {
if (lines.length === 0) throw new Error(`접수 실패 ${submitted.status} ${await submitted.text()}`);
console.warn('이어받기가 거절되어 멈춥니다:', submitted.status, await submitted.text());
console.warn('접수하지 못한 계좌:', request.bank_account_credential_ids);
break;
}
const batch = await submitted.json();
lines.push(...batch.accounts.filter((line) => line.job_id !== null));
if (batch.interruption) {
console.warn('호출이 중간에 멈췄습니다:', batch.interruption.message);
console.warn('접수하지 못한 계좌:', batch.next?.bank_account_credential_ids);
break;
}
if (batch.next === null) break;
if (round >= 20) {
console.warn('이어받기를 20번 했는데도 접수하지 못한 계좌가 남았습니다:', batch.next.bank_account_credential_ids);
break;
}
await sleep(batch.next.retry_after_seconds);
request = { ...body, bank_account_credential_ids: batch.next.bank_account_credential_ids };
}
// 2. 접수한 작업마다 끝날 때까지 상태를 보고 결과를 읽습니다.
async function readResult(jobId) {
for (;;) {
const response = await fetch(`${api}/v1/jobs/${jobId}`, { headers });
if (!response.ok) throw new Error(`상태 조회 실패 ${response.status} ${await response.text()}`);
const job = await response.json();
if (job.status === 'SUCCEEDED') {
const result = await fetch(`${api}/v1/jobs/${jobId}/result`, { headers });
if (!result.ok) throw new Error(`결과 조회 실패 ${result.status} ${await result.text()}`);
return result.json();
}
if (job.status !== 'QUEUED' && job.status !== 'RUNNING') return { status: job.status, error: job.error };
await sleep(job.retry_after_seconds ?? 2);
}
}
for (const line of lines) {
console.log(line.masked_account_no, await readResult(line.job_id));
}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"}
body = {
"provider": "kb_fast_account",
"action": "kb_fast_account.accounts.transaction_history.search",
"params": {
"start_date": "2026-09-01",
"end_date": "2026-09-30"
}
}
# 1. 모든 계좌를 접수합니다. 접수하지 못한 계좌가 남으면 응답의 next 로 이어서 접수합니다.
# 회차마다 Idempotency-Key 를 따로 씁니다. 응답을 받지 못해 같은 회차를 다시 보낼 때만 같은 키를 씁니다.
# 호출이 멈췄거나(interruption) 이어받는 회차가 거절되면, 이미 접수한 작업을 잃지 않도록 여기서 멈추고 2단계로 갑니다.
# 남은 계좌는 까닭을 해결한 뒤 새 Idempotency-Key 와 next 의 목록으로 다시 보냅니다.
lines = []
request = body
for round_no in range(1, 21):
submitted = requests.post(
f"{api}/v1/jobs/all-accounts",
headers={**headers, "Idempotency-Key": f"monthly-2026-09-r{round_no}"},
json=request,
timeout=30,
)
if submitted.status_code not in (200, 202):
if not lines:
raise RuntimeError(f"접수 실패 {submitted.status_code} {submitted.text}")
print("이어받기가 거절되어 멈춥니다:", submitted.status_code, submitted.text)
print("접수하지 못한 계좌:", request.get("bank_account_credential_ids"))
break
batch = submitted.json()
lines.extend(line for line in batch["accounts"] if line["job_id"] is not None)
if batch.get("interruption"):
print("호출이 중간에 멈췄습니다:", batch["interruption"]["message"])
print("접수하지 못한 계좌:", (batch.get("next") or {}).get("bank_account_credential_ids"))
break
if batch["next"] is None:
break
time.sleep(batch["next"]["retry_after_seconds"])
request = {**body, "bank_account_credential_ids": batch["next"]["bank_account_credential_ids"]}
else:
print("이어받기를 20번 했는데도 접수하지 못한 계좌가 남았습니다:", request.get("bank_account_credential_ids"))
# 2. 접수한 작업마다 끝날 때까지 상태를 보고 결과를 읽습니다.
def read_result(job_id):
while True:
response = requests.get(f"{api}/v1/jobs/{job_id}", headers=headers, timeout=30)
response.raise_for_status()
job = response.json()
if job["status"] == "SUCCEEDED":
result = requests.get(f"{api}/v1/jobs/{job_id}/result", headers=headers, timeout=30)
result.raise_for_status()
return result.json()
if job["status"] not in ("QUEUED", "RUNNING"):
return {"status": job["status"], "error": job.get("error")}
time.sleep(job.get("retry_after_seconds") or 2)
for line in lines:
print(line["masked_account_no"], read_result(line["job_id"]))