시작하기

QuickBank 는 계좌이체 입금을 자동으로 확인해 주문에 연결해요. 우리가 돈을 받지 않아요 — 고객이 사장님 계좌로 직접 보내고, 우리는 그 입금을 확인해 알려드려요.

  1. 1콘솔에서 수집 계좌를 등록하고 1원 인증을 마쳐요.
  2. 2입금 문자를 받을 폰에 수집 앱을 설치해요.
  3. 3API 키를 발급받아요 (콘솔 > 개발자).
  4. 4주문이 생길 때 /v1/intents 를 호출해 안내 금액을 받아요.
  5. 5입금이 확인되면 웹훅으로 알려드려요.

실제로 돈을 보내지 않고 전 과정을 시험할 수 있어요 — 콘솔 > 개발자 > 테스트 입금 보내보기.

고유 금액 — 가장 중요해요

고객에게 보여 줄 금액은 amount 가 아니라 amount_to_pay 예요. 이걸 틀리면 입금이 자동으로 확인되지 않아요. 그런데 조용히 실패해요 — 요청은 200 을 받고 주문도 만들어지는데, 입금만 영원히 미매칭으로 남아요.

같은 금액의 주문이 둘 있으면 어느 쪽에 들어온 돈인지 가릴 수 없어요. 입금 문자에는 보낸 사람 이름과 금액만 찍히고, 주문번호가 없으니까요.

그래서 주문마다 끝자리를 조금씩 다르게 배정해요. 30,000원 주문 두 건이면 하나는 30,007원, 다른 하나는 30,041원이 돼요. 고객이 그 금액을 정확히 보내면 어느 주문인지 한 번에 확정돼요.

// 응답에서 이 두 값이 다를 수 있어요
{
  "id": "a1b2c3...",
  "base_amount":   30000,   // 원래 주문 금액
  "amount_to_pay": 30007    // ← 고객에게 이 금액을 보여 주세요
}

고객이 끝자리를 빼고 원래 금액(30,000원)을 보내도 포기하지 않아요 — 입금자명으로 한 번 더 맞춰 봐요. 그래서 expected_depositor 를 같이 넘기면 자동 확인율이 올라가요.

인증

Authorization: Bearer <key_id>:<secret> 형식이에요. 콜론으로 이어 붙여요.

curl https://api.quickbank.co.kr/v1/intents \
  -H "Authorization: Bearer qb_live_xxxx:sk_yyyy" \
  -H "Content-Type: application/json" \
  -d '{"merchant_order_id":"ORDER-1","amount":30000}'

키는 qb_test_ 와 qb_live_ 두 종류예요. 테스트 키로 만든 주문은 테스트로 표시되고 요금 청구에서 빠져요. 실제 입금과 섞이지도 않아요.

비밀키는 발급할 때 한 번만 보여드려요. 서버에만 두시고, 브라우저 코드에는 절대 넣지 마세요. 유출되면 콘솔에서 바로 폐기할 수 있어요.

주문

POST/v1/intents주문을 만들고 안내 금액을 받아요
항목필수설명
merchant_order_id필수우리 쪽 주문번호. 같은 값으로 다시 부르면 같은 주문이 돌아와요 (멱등)
amount필수주문 금액 (원). 1 이상
expected_depositor선택입금하실 분 이름. 넣으면 자동 확인율이 올라가요
expires_in선택유효 시간(초). 300~259200, 기본 3600
bank_account_id선택받을 계좌. 없으면 자동으로 골라요
buyer_email / buyer_phone선택구매자 연락처
metadata선택우리가 보관만 하는 객체. 웹훅에 그대로 돌려드려요
// 응답
{
  "id": "9f2c...",                  // QuickBank 주문 id
  "merchant_order_id": "ORDER-1",
  "status": "pending",
  "base_amount": 30000,
  "amount_to_pay": 30007,          // ← 고객에게 보여 줄 금액
  "expected_depositor": "홍길동",
  "expires_at": "2026-10-04T13:00:00+09:00"
}

멱등: 같은 merchant_order_id 로 다시 부르면 새 주문을 만들지 않고 기존 주문을 돌려드려요 (idempotent_replay: true). 네트워크가 끊겨 재시도하는 경우에 안전해요. Idempotency-Key 헤더를 같이 보내면 본문까지 같은지 확인해요.

GET/v1/intents/{id}주문 상태를 봐요
POST/v1/intents/{id}/cancel주문을 취소하고 금액을 돌려줘요
GET/v1/intents대기 중인 주문 목록

취소하면 그 고유 금액이 다른 주문에 다시 쓰여요. 안 쓰는 주문을 방치하면 동시에 받을 수 있는 주문 수가 줄어드니, 결제창을 닫은 고객의 주문은 취소해 주시면 좋아요.

입금

GET/v1/payments확인된 입금 목록
검색 조건설명
statusmatched · unmatched · manual_matched · ignored
from / toYYYY-MM-DD
test1 이면 테스트 건만 (기본은 실제 건만)
limit / cursor쪽 나눔
GET/v1/payments/{id}입금 한 건
GET/v1/accounts수집 계좌 목록 (읽기 전용)

계좌 등록·변경은 API 로 할 수 없어요. 콘솔에서만 가능해요 — 키가 유출됐을 때 돈이 흘러가는 방향을 바꿀 수 있으면 안 되니까요.

웹훅

콘솔 > 개발자에서 수신 주소를 등록하면, 입금이 확인될 때 POST 로 알려드려요. HTTPS 만 받아요.

이벤트언제
payment.matched입금이 주문에 연결됐어요
payment.unmatched입금이 들어왔지만 주문을 못 찾았어요
account.verified수집 계좌 1원 인증이 끝났어요

서명 확인

QB-Signature 헤더로 우리가 보낸 것임을 확인하실 수 있어요.반드시 확인해 주세요 — 안 하면 아무나 그 주소로 가짜 입금을 보낼 수 있어요.

QB-Signature: t=1791080000,v1=3a7f9c...
QB-Event: payment.matched
QB-Delivery-Id: 8f0c...        // 중복 판정에 쓰세요
Idempotency-Key: 8f0c...       // 같은 값

// 검증 (PHP)
[$t, $v1] = /* QB-Signature 에서 t=, v1= 를 뽑아요 */;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $webhookSecret);
if (!hash_equals($expected, $v1)) { http_response_code(400); exit; }

// 오래된 서명을 거부해 재전송 공격을 막아요 (5분 권장)
if (abs(time() - (int) $t) > 300) { http_response_code(400); exit; }

서명은 원문 그대로(raw body)에 대해 계산해요. JSON 으로 파싱한 뒤 다시 직렬화한 문자열로 계산하면 공백·순서가 달라져 맞지 않아요.

재시도

2xx 가 아니면 다시 보내요. 최대 12번이고 간격은 이렇게 늘어나요 (±20% 지터):

10초 → 30초 → 1분 → 5분 → 15분 → 30분
  → 1시간 → 2시간 → 4시간 → 6시간 → 6시간 → 6시간

같은 이벤트가 두 번 올 수 있어요 (우리가 응답을 못 받은 경우).QB-Delivery-Id 로 중복을 걸러 주세요 — 같은 값이면 같은 건이에요.

응답은 10초 안에 주세요. 무거운 일은 큐에 넣고 바로 200 을 돌려주시는 편이 안전해요. 연속 20번 실패하면 그 주소를 자동으로 꺼 두고 메일로 알려드려요.

payload

{
  "event": "payment.matched",
  "payment_id": 12345,
  "payment_intent_id": 678,
  "merchant_order_id": "ORDER-1",
  "amount": 30007,            // 실제로 들어온 금액
  "base_amount": 30000,       // 원래 주문 금액
  "assigned_amount": 30007,   // 안내한 금액
  "depositor_name": "홍길동",
  "occurred_at": "2026-10-04 12:34:56.789",
  "match_method": "exact_amount",   // 또는 amount_and_name · manual
  "match_score": 100,
  "metadata": { /* 주문 만들 때 넘긴 값 */ }
}

오류

오류는 항상 같은 모양이에요. HTTP 상태와 code 로 분기해 주세요.

{
  "error": {
    "code": "NO_VERIFIED_ACCOUNT",
    "message": "등록한 계좌의 1원 인증이 끝나지 않았어요...",
    "detail": { }
  }
}
상태code뜻
401UNAUTHORIZED키 형식이 틀렸거나 없는 키예요
403FORBIDDEN그 작업을 할 권한(scope)이 없어요
409NO_VERIFIED_ACCOUNT1원 인증을 마친 계좌가 없어요
409AMOUNT_UNAVAILABLE고유 금액 칸이 다 찼어요 (아래 참고)
409MERCHANT_INACTIVE심사가 끝나지 않았거나 이용이 중지됐어요
422MISSING_FIELD / INVALID_FIELD필수 항목이 없거나 형식이 틀려요
429TOO_MANY_REQUESTS잠시 뒤에 다시 시도해 주세요

AMOUNT_UNAVAILABLE 는 그 계좌에서 동시에 받을 수 있는 주문 수를 넘었다는 뜻이에요. 끝난 주문을 취소하거나, 콘솔에서 고유 금액 범위를 넓히거나, 계좌를 하나 더 등록하면 풀려요.

테스트

실제로 돈을 보내지 않고 전 과정을 시험할 수 있어요. 콘솔 > 개발자 > 테스트 입금 보내보기.

시나리오확인할 것
정상 입금payment.matched 가 오고 match_method 가 exact_amount
원래 금액 + 이름amount_and_name 으로 확정
금액이 어긋남payment.unmatched 처리 — 여기서 대부분 빠뜨려요
입금자명이 다름자동 확정하지 않고 미매칭
출금 문자아무 주문도 건드리지 않아요

성공만 시험하지 마세요. 미매칭 웹훅을 처리하지 않은 채 배포하면, 실제로 금액이 어긋난 입금이 들어왔을 때 사장님이 손으로 찾아야 해요.

모의 입금은 실제 주문을 찾지도 못해요 — 저장 단계에서부터 갈라져 있어요. 테스트가 실제 주문을 확정할 수는 없어요.

막히는 부분이 있으면

[email protected] 로 알려 주세요. 연동하면서 걸린 곳을 적어 보내시면 그 부분을 같이 봐드려요.