가맹점 API 설명서
가맹점 사이트에 EGG PAY 를 붙이면, 사용자가 결제한 금액만큼 USDE 가 가맹점 지갑으로 전송됩니다.
1. 시작하기
- 가맹점 신청을 하면 받을 지갑 주소의 SJO 지갑 앱으로 서명 요청이 갑니다. 지갑 앱에서 서명하시면 신청이 접수되고, 검토 후 알려 드립니다.
- 승인되면 가맹점 코드와 API 키를 드립니다.
- 사용자가 지갑 연동을 마쳐야 결제할 수 있습니다 (3절).
- 사용자가 거래소 회원이어야 결제할 수 있습니다. 연동 확인의
exchange.ready로 보실 수 있습니다 (3절).
결제 순서
- 가맹점 사이트에서 충전 요청 — 가맹점 서버가 결제 요청을 부릅니다 (5절)
- 로그인 (회원이 아니면 회원가입) — 방식마다 다릅니다 (5절)
- 구매 등록
- 매칭
- 지갑 앱으로 이동
- 서명
- 컨트랙트 시작 — 끝나면 결과를 보내 드립니다 (6절)
가맹점이 하시는 곳은 결제 요청, API 만 방식의 구매 등록, 그리고 결과 받기(웹훅 · 상태 조회)입니다.
mode)도 골라 주십시오 (5절).
API 만 방식(api)이면 사용자가 보는 화면도 가맹점이 만드십니다 —
「지갑 앱에서 승인하세요」 안내, [구매] 버튼, 매칭 대기와 [취소] 버튼, 「지갑 앱에서 서명하세요」 안내.
거래소 회원이 아닌 사용자에게는 거래소 가입을 안내해 주십시오.2. 인증
환율(4절) 밖의 모든 요청에 두 헤더를 넣습니다.
| 헤더 | 무엇 |
|---|---|
X-EggPay-Merchant-Id | 가맹점 코드 |
X-EggPay-Api-Key | 비밀 키 |
키는 둘입니다. API 키는 가맹점이 우리를 부를 때,
웹훅 비밀키는 우리가 가맹점을 부를 때 씁니다 (6절).
API 키는 관리자 웹 「API」에서 [재발급], 웹훅 비밀키는 마이페이지의 「웹훅 서명 키」 [새로 발급]으로
가맹점이 직접 새로 받으실 수 있습니다 (9-1절).
둘 중 하나라도 없거나 틀리면 401 unauthorized 입니다 — 어느 쪽이 틀렸는지는
알려 드리지 않습니다. 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점이면 403 merchant_inactive 입니다.
요청 공통
| 헤더 | 언제 | 무엇 |
|---|---|---|
X-EggPay-Merchant-Id · X-EggPay-Api-Key | 환율 밖 모두 | 위의 두 헤더 |
Content-Type: application/json | 본문이 있는 요청 (연동 시작 · 결제 요청) | 본문은 UTF-8 JSON 입니다. 다른 형식이면 400 invalid_request 입니다 |
Accept-Language | 골라서 | ko 또는 en — 오류의 message 언어입니다.
없으면 한국어입니다 |
Accept | 골라서 | 보내실 때는 application/json 을 받게 해 주십시오.
JSON 을 받지 않는다고 하시면 본문 없는 406 이 갑니다 |
다시 보내기 키는 따로 헤더로 받지 않습니다. 결제 요청 본문의 merchantRequestId 가 그 키입니다 (5절).
GET /api/v1/payments/pay_Q2x7Lr0kVb3sT9mWzA1cN-8e
X-EggPay-Merchant-Id: (가맹점 코드)
X-EggPay-Api-Key: (API 키)
Accept-Language: ko
호출 한도 (가맹점 하나당, 분당)
| API | 분당 | 세는 창구 |
|---|---|---|
| 결제 요청 | 60 | 결제 요청 · 구매 등록 |
| 결제 상태 조회 | 600 | 결제 상태 조회 |
| 지갑 연동 | 60 | 연동 시작 · 연동 확인 · 연동 해제 |
| 전체 합 | 1,200 | 모든 창구의 합. 지갑 잔액 조회와 매칭 전 취소는 이 몫만 셉니다 |
표의 값은 기본값입니다. 넘으면 429 와 함께 retryAfterSeconds 를
돌려드립니다. 한도는 1분 창(매 분 0초부터)으로 셉니다.
공통 모양
| 무엇 | 모양 |
|---|---|
| 성공 | HTTP 200 과 JSON 본문입니다. 창구마다 아래에 본문 예를 적었습니다.
값이 없는 필드도 빠지지 않고 null 로 옵니다 |
| 오류 | { "error": { "code": "…", "message": "…" } } 와 HTTP 상태.
code 로 가르십시오. 429 에는 retryAfterSeconds 도 붙습니다 (10절) |
| 금액 | 소수 둘째 자리까지의 문자열입니다 (예 "12.50").
셋째 자리가 오면 400 입니다 — 버리거나 반올림하지 않습니다 |
| 시각 | ISO-8601 UTC 문자열입니다 (예 2026-09-28T03:04:05.123Z).
웹훅 헤더 X-EggPay-Timestamp 만 초입니다 (6절) |
| 모르는 필드 | 보내시면 400 invalid_request 입니다 |
| 다시 보내기 | 결제 요청에는 가맹점이 정하는 키 merchantRequestId 를 넣습니다.
답을 못 받으셨으면 같은 키, 같은 내용으로 다시 보내 주십시오 — 처음 결과를 드립니다.
같은 키에 다른 내용이면 409 conflict 입니다 |
오류를 받으면
창구마다 오류 표의 「하실 일」 칸은 아래 셋 가운데 하나입니다.
| 하실 일 | 뜻 |
|---|---|
| 고쳐서 다시 | 요청이 틀렸습니다. 그대로 다시 보내도 같은 오류입니다. 고친 뒤 보내 주십시오 |
| 잠시 뒤 다시 | 잠깐 생긴 문제입니다. 같은 요청을 잠시 뒤 다시 보내 주십시오.
429 는 retryAfterSeconds 초가 지난 뒤 보내 주십시오 |
| 다시 보내지 마십시오 | 다시 보내도 바뀌지 않습니다. 칸에 적힌 대로 먼저 다른 일을 해 주십시오 |
어느 창구에서나 오는 것은 이렇습니다.
- 없는 주소를 부르시면
404not_found, 방식이 틀리면(예:POST창구를GET으로)405method_not_allowed와 맞는 방식을 적은Allow헤더입니다 — 고쳐서 다시. 키가 필요한 주소에서는 키 검사가 먼저라, 키가 없으면 이 둘보다401이 먼저 옵니다. - 주소 안에
%2F(/) ·%5C(\) 가 있으면 저희 웹 서버가 JSON 이 아닌400을 줍니다 — 그런 글자는 회원 아이디에 넣지 마십시오 (3절). Accept가 JSON 을 받지 않으면 본문 없는406입니다.
3. 지갑 연동
사용자가 SJO 지갑앱을 설치하고, 계정 주소를 만들고, 가맹점 회원 아이디에 그 주소를 연동해 두어야 결제할 수 있습니다.
연동 시작
POST /api/v1/merchant/wallet-links
사용자의 SJO 지갑 주소를 가맹점 회원 아이디에 연동해 달라고 요청합니다. 한도는 「지갑 연동」 몫입니다.
요청
| 필드 | 형식 | 필수 | 무엇 | 예 |
|---|---|---|---|---|
merchantUserId | 문자열 | 예 | 가맹점 사이트의 회원 아이디 (1~64자). 연동 확인·해제에서 주소의 한 칸으로 쓰이므로
/ · \ · ; 는 넣을 수 없고 . · .. 만으로 된 아이디도 안 됩니다
(400 invalid_request). 주소에 실을 때는 퍼센트 인코딩해 주십시오(예: 빈칸은 %20) |
"member-1024" |
walletAddress | 문자열 | 예 | 사용자가 입력한 SJO 지갑 계정 주소 (sjo1…). 체크섬을 검사합니다 |
"sjo1…" |
{ "merchantUserId": "member-1024", "walletAddress": "sjo1…" }
성공 응답 · 200 OK
{ "linkId": "lnk_Hk3Pq9Ws2LmX7vR0aTe5Yc_J", "status": "PENDING", "expiresAt": "2026-10-21T03:09:05.123Z" }
| 필드 | 형식 | 무엇 |
|---|---|---|
linkId | 문자열 | 연동 요청 번호 (lnk_ 로 시작) |
status | 문자열 | PENDING(사용자 승인 기다림) 또는 LINKED(이미 연동됨) |
expiresAt | 시각 | 이 요청의 마감 — 만든 때부터 5분 |
이 뒤로 사용자의 지갑앱에 알림이 가고, 사용자가 지갑앱에서
서명으로 승인하면 연동이 끝납니다. 옮겨 칠 숫자는 없습니다.
요청은 5분 동안 살아 있습니다 (expiresAt). 결과는 연동 확인으로 보십시오.
지갑 앱에 알림을 곧바로 전하지 못해도 답은 PENDING 이고, 마감 전까지 저희가 다시 보냅니다.
같은 회원·같은 주소로 다시 부르시면 살아 있는 요청을 그대로 드립니다. 같은 회원·같은 주소가 이미 연동돼 있으면
새 요청을 만들지 않고 그 연동을 "status": "LINKED" 로 드립니다. 이때 expiresAt 은 그 연동을 만든
요청의 마감(이미 지난 시각)입니다.
{ "linkId": "lnk_Hk3Pq9Ws2LmX7vR0aTe5Yc_J", "status": "LINKED", "expiresAt": "2026-10-20T01:05:00.000Z" }
한 가맹점 안에서 지갑 하나는 회원 하나에만 연동됩니다. 그 회원이 이미 다른 주소로,
또는 그 주소가 다른 회원으로 연동돼 있으면 409 wallet_link_conflict 입니다.
살아 있는 요청(5분 안)도 같습니다 — 그 회원의 다른 주소 요청, 또는 그 주소의 다른 회원
요청이 살아 있으면 409 wallet_link_conflict 입니다. 5분이 지난 요청은 막지 않습니다.
바꾸시려면 연동은 연동 해제 뒤, 요청은 expiresAt 이 지난 뒤 다시 시작해 주십시오.
그 주소의 지갑 사용자가 없으면 422 wallet_undeliverable 입니다.
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
invalid_request | 400 | 본문이 JSON 이 아님 · 필드가 빠짐 · 모르는 필드 · 문자열이 아닌 값.
merchantUserId 가 비었거나 64자를 넘거나 / · \ · ; 가 있거나 . · .. 뿐임 |
고쳐서 다시 |
invalid_address | 400 | walletAddress 가 SJO 계정 주소가 아님 (체크섬이 틀림 · 컨트랙트 주소 등) |
사용자에게 주소를 다시 받아 고쳐서 다시 |
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
wallet_link_conflict | 409 | 그 회원이 다른 주소로, 또는 그 주소가 다른 회원으로 연동됐거나 그런 요청이 살아 있음 (5분 안) | 다시 보내지 마십시오 — 연동 해제 뒤, 또는 그 요청의 expiresAt 이 지난 뒤 다시 시작 |
wallet_undeliverable | 422 | 그 주소를 쓰는 SJO 지갑 사용자가 없음. 그 요청은 거절(REJECTED)로 닫힙니다 |
다시 보내지 마십시오 — 사용자에게 주소와 지갑 앱을 확인받은 뒤 다시 시작 |
rate_limited | 429 | 「지갑 연동」 또는 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
연동 확인
GET /api/v1/merchant/wallet-links/{merchantUserId}
결제 버튼을 그리기 전에 부르십시오. 연동되고 거래소 준비가 된 사용자만 결제할 수 있습니다. 한도는 「지갑 연동」 몫입니다.
요청
| 자리 | 이름 | 형식 | 필수 | 무엇 |
|---|---|---|---|---|
| 주소 | merchantUserId | 문자열 | 예 | 연동 시작에 쓴 회원 아이디. 퍼센트 인코딩해서 넣습니다
(예: /api/v1/merchant/wallet-links/member-1024). 본문은 없습니다 |
성공 응답 · 200 OK
{ "linked": true, "status": "LINKED", "walletAddress": "sjo1…", "linkedAt": "2026-10-20T01:01:12.345Z",
"exchange": { "ready": true, "reason": null } }
| 필드 | 무엇 |
|---|---|
linked | status 가 LINKED 면 true |
status | LINKED(연동됨) · PENDING(승인 기다림) ·
EXPIRED(5분 지남) · REJECTED(거절됨) · NONE(지금 연동 없음 —
연동한 적이 없거나 연동 해제함) |
walletAddress · linkedAt | LINKED 일 때만. 그 밖에는 null |
exchange | LINKED 일 때만. ready: true 여야 그 주소로 결제를 받습니다.
false 면 reason 에 거래소가 준 까닭(회원 아님 · 본인 확인 전 · 가입 승인 대기 · 정지 등)이 옵니다.
거래소에 닿지 못했으면 exchange 가 null 입니다 — 잠시 뒤 다시 조회해 주십시오 |
연동이 없는 회원도 오류가 아니라 이렇게 답합니다.
{ "linked": false, "status": "NONE", "walletAddress": null, "linkedAt": null, "exchange": null }
가맹점마다 따로 봅니다 — 다른 가맹점의 회원은 보이지 않습니다. 사용자가 지갑 앱에서 거절했거나 지갑 앱에 전할 수
없었으면 REJECTED, 5분 안에 승인이 없었거나 지갑 서명을 확인하지 못했으면 EXPIRED 입니다.
이름 · 거래소 아이디 같은 개인정보는 싣지 않습니다.
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
invalid_request | 400 | 회원 아이디가 64자를 넘거나 . · .. 뿐이거나,
주소에 인코딩하지 않은 ; 가 있음 (그 뒤가 잘려 다른 아이디가 되므로 읽지 않습니다) | 고쳐서 다시 |
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
rate_limited | 429 | 「지갑 연동」 또는 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
연동 해제
DELETE /api/v1/merchant/wallet-links/{merchantUserId}
회원의 연동을 풉니다. 한도는 「지갑 연동」 몫입니다.
요청
| 자리 | 이름 | 형식 | 필수 | 무엇 |
|---|---|---|---|---|
| 주소 | merchantUserId | 문자열 | 예 | 연동 확인과 같습니다 (퍼센트 인코딩). 본문은 없습니다 |
성공 응답 · 200 OK
{ "linked": false }
이 가맹점에서 그 지갑의 팝업 결제가 아직 요청 기록(REQUESTED)이면 먼저 닫고 풉니다 — 그 결제는 거래취소
(request_closed)가 되고 웹훅이 갑니다. 연동 기록은 지우지 않지만, 해제한 뒤 연동 확인은 NONE 입니다.
새 주소는 연동 시작부터 다시 해 주십시오.
그 지갑으로 진행 중인 결제가 있으면 409 payment_in_flight 입니다.
연동된 것이 없으면 404 입니다.
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
invalid_request | 400 | 회원 아이디가 64자를 넘거나 . · .. 뿐이거나,
주소에 인코딩하지 않은 ; 가 있음 — 풀지 않았습니다 | 고쳐서 다시 |
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
not_found | 404 | 이 가맹점에 그 회원의 연동이 없음 (이미 해제한 뒤 다시 불러도 이것입니다) | 다시 보내지 마십시오 — 이미 풀려 있습니다 |
payment_in_flight | 409 | 그 지갑으로 이 가맹점에 진행 중인 결제(TRADING · DISPUTED)가 있음.
팝업 결제가 이미 거래소 주문을 가졌을 때도 이것입니다 | 그 결제가 끝난 뒤(웹훅 · 결제 상태 조회) 다시 |
buyer_busy | 409 | 같은 지갑의 다른 요청(결제 요청 · 연동 해제)을 지금 처리하고 있음 | 잠시 뒤 같은 요청을 다시 |
rate_limited | 429 | 「지갑 연동」 또는 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
exchange_unavailable | 503 | 요청 기록을 닫으려 했는데 거래소가 답하지 않음 — 풀지 않았습니다 | 잠시 뒤 다시 |
4. 환율
GET /api/v1/rates
키가 필요 없습니다. 사용자가 금액을 치는 동안 브라우저에서 바로 불러 환산해 보여주실 수 있습니다 (다른 사이트에서 불러도 됩니다). IP 하나당 분당 120회입니다 (기본값).
요청
헤더 · 주소 값 · 본문 모두 없습니다.
성공 응답 · 200 OK
{ "base": "USDE", "rates": { "USD": 1.0, "KRW": 1342.34042 }, "timestamp": 1791528601, "next_update": 1791550201 }
| 필드 | 형식 | 무엇 |
|---|---|---|
base | 문자열 | 늘 "USDE" |
rates | 객체 | 통화 코드 → 1 USDE 가 그 통화로 얼마인지 (숫자). 통화와 자릿수는 재단이 준 그대로입니다 |
timestamp | 숫자 | 재단이 정한 기준 시각 (유닉스 초) |
next_update | 숫자 | 재단의 다음 갱신 시각 (유닉스 초) |
rates 에 있습니다.
가맹점은 이 환율을 쓰셔야 합니다.환율은 매일 한국 시간 0시와 12시에 재단에서 받아 바뀝니다. timestamp 와
next_update 는 재단이 정한 기준 시각과 재단의 다음 갱신 시각입니다(유닉스 초).
그 시각에 재단 환율을 받지 못하면 앞서 받은 환율을 그대로 드립니다.
받아 둔 환율이 하나도 없을 때만 503 과 rate_unavailable 을 드립니다 —
잠시 후 다시 불러 주십시오.
{ "error": { "code": "rate_unavailable", "message": "…" } }
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
rate_unavailable | 503 | 저장된 환율이 없음 — 받아 둔 환율이 하나도 없을 때만입니다 | 잠시 뒤 다시 |
rate_limited | 429 | IP 하나당 분당 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
5. 결제
방식 셋
결제를 요청할 때 mode 로 고르십니다. 로그인과 구매 등록만 다르고,
매칭부터는 같습니다.
mode | 화면 | 로그인 | 구매 등록 |
|---|---|---|---|
api | API 만 — 가맹점 화면 | 사용자가 지갑 앱에서 승인 | 가맹점 서버가 구매 등록을 부릅니다 |
popup_wallet | 팝업 | 사용자가 지갑 앱에서 승인 | 사용자가 팝업에서 |
popup_password | 팝업 | 거래소 아이디 · 비밀번호 + OTP | 사용자가 팝업에서 |
결제 요청
POST /api/v1/payments
한도는 「결제 요청」 몫입니다.
요청
| 필드 | 형식 | 필수 | 무엇 | 예 |
|---|---|---|---|---|
merchantRequestId | 문자열 | 예 | 가맹점이 정하는 다시 보내기 키. 가맹점마다 유일.
1~64자, 영문 · 숫자 · _ - . : | "order-20261021-0001" |
buyerWalletAddress | 문자열 | 예 | 사용자의 지갑 주소 — 이 가맹점에 연동돼 있어야 합니다 | "sjo1…" |
merchantWalletAddress | 문자열 | 예 | 가맹점의 지갑 주소 — 등록하신 지급 주소와 같아야 합니다 | "sjo1…" |
amountUsde | 문자열 | 예 | 구매할 USDE 수량, 소수 둘째 자리까지 (예 "12.50").
0 보다 커야 하고 정수부는 16자리까지입니다. 숫자(12.5)로 보내시면 400 입니다 | "100.00" |
mode | 문자열 | 예 | api · popup_wallet · popup_password | "popup_wallet" |
{ "merchantRequestId": "order-20261021-0001", "buyerWalletAddress": "sjo1…", "merchantWalletAddress": "sjo1…",
"amountUsde": "100.00", "mode": "popup_wallet" }
성공 응답 · 200 OK
{ "paymentId": "pay_Q2x7Lr0kVb3sT9mWzA1cN-8e", "status": "REQUESTED", "statusReason": null,
"popupUrl": "https://…", "created": true }
api 방식이면 이렇습니다.
{ "paymentId": "pay_Q2x7Lr0kVb3sT9mWzA1cN-8e", "status": "TRADING", "statusReason": null,
"popupUrl": null, "created": true }
| 필드 | 무엇 |
|---|---|
paymentId | 결제 번호 (pay_ 로 시작). 구매 등록 · 취소 · 조회 · 웹훅에서 이 번호를 씁니다 |
status | api 는 TRADING — 거래소 회원이 아니면 곧바로
CANCELLED 입니다. 팝업 둘은 REQUESTED(요청 기록) |
statusReason | CANCELLED 일 때 까닭 (7절) |
popupUrl | 팝업 둘만. 아직 준비되지 않았으면 null —
결제 상태 조회로 다시 읽어 주십시오. api 는 늘 null |
created | 이 호출로 처음 만들었으면 true, 같은
merchantRequestId 의 처음 결과면 false |
- 이때는 거래소 주문이 생기지 않습니다. 주문은 구매 등록에서 생깁니다.
- 같은
merchantRequestId로 다시 보내시면 같은 결제입니다 — 그 결제의 지금 값을created: false로 드립니다. 같은 내용인지는 구매자 지갑 · 가맹점 지갑 · 수량 · 방식으로 봅니다 ("12.5"와"12.50"은 같습니다). 결제를 만들지 않은 거절은 남기지 않습니다 — 같은 키로 다시 부르시면 그때 상태로 다시 봅니다. - 구매자 지갑 하나에 진행 중인 결제는 1건입니다 — 있으면
409payment_in_flight. 같은 구매자의 팝업 결제가 아직 요청 기록이면 그것을 닫고(request_closed) 새 결제를 만듭니다. - USDE 가 멈췄으면
503usde_paused, 멈춤 상태를 읽지 못했으면503chain_unavailable입니다. 둘 다 결제를 만들지 않았습니다 —chain_unavailable은 같은merchantRequestId로 다시 부르시면 됩니다. - 처리 중에 지갑 앱에 전할 수 없거나(
wallet_undeliverable) 거래소가 팝업 결제를 받지 않으면 (amount_out_of_range등) 어느 방식이든 답이 곧바로CANCELLED일 수 있습니다. - 수량은 지갑 앱 · 팝업에서 바꿀 수 없습니다.
필드가 하나라도 없거나 잘못되면 거절합니다. 그때는 가맹점
화면에서 경고를 띄워 주십시오. 저희는 아래 차례로 봅니다 — 형식 → 같은 merchantRequestId →
가맹점 지갑 → 연동 → USDE 멈춤 → 진행 중 결제 → 거래소 회원 자격(api 만).
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
invalid_request | 400 | 본문이 JSON 이 아님 · 필드가 빠짐 · 모르는 필드 · 형식이 틀림
(merchantRequestId 글자 · mode 이름 · 수량이 문자열이 아니거나 소수 셋째 자리 · 0 이하) | 고쳐서 다시 |
invalid_address | 400 | 구매자 지갑이 SJO 계정 주소가 아니거나 가맹점 지갑이 SJO 주소가 아님 (체크섬) | 고쳐서 다시 |
merchant_wallet_mismatch | 400 | merchantWalletAddress 가 등록하신 지급 주소와 다름 |
등록된 지급 주소로 고쳐서 다시 |
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 (결제를 만드는 순간에 정지된 경우도 — 결제를 만들지 않았습니다) | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
wallet_not_linked | 404 | 구매자 지갑이 이 가맹점에 연동되지 않음 | 다시 보내지 마십시오 — 먼저 지갑 연동 (3절) |
conflict | 409 | 같은 merchantRequestId 로 다른 내용이 이미 왔음 |
다시 보내지 마십시오 — 새 결제라면 새 merchantRequestId 로 |
buyer_busy | 409 | 같은 구매자 지갑의 다른 요청(결제 요청 · 연동 해제)을 지금 처리하고 있음 | 잠시 뒤 같은 요청을 그대로 다시 |
payment_in_flight | 409 | 그 지갑에 진행 중인 결제가 있음 (지갑당 1건 — 다른 가맹점의 결제도 셉니다). 앞의 팝업 결제가 이미 거래소 주문을 가졌을 때도 이것입니다 | 그 결제가 끝난 뒤(웹훅 · 결제 상태 조회) 다시 |
rate_limited | 429 | 「결제 요청」 또는 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 같은 merchantRequestId 로 다시 |
usde_paused | 503 | USDE 가 멈춰 있음 — 결제를 만들지 않았습니다 | 잠시 뒤 다시 (멈춤이 풀려야 받습니다) |
chain_unavailable | 503 | USDE 멈춤 상태를 체인에서 읽지 못함 — 결제를 만들지 않았습니다 | 잠시 뒤 같은 merchantRequestId 로 다시 |
exchange_unavailable | 503 | 거래소가 답하지 않음 (앞 요청 기록 닫기 · api 의 회원 자격 확인) — 결제를 만들지 않았습니다 |
잠시 뒤 같은 merchantRequestId 로 다시 |
팝업 열기
popup_wallet · popup_password 만 해당합니다.
window.open(popupUrl, '_blank', 'noopener,noreferrer')
새 창으로 여십시오. 틀(iframe) 안에 넣지 마십시오 — 팝업은 거래소
화면입니다. PC 와 모바일 둘 다 됩니다.
| 사용자가 | 결제는 |
|---|---|
| 구매 등록 전에 팝업을 닫음 | 요청 기록 그대로입니다. 같은 popupUrl 로 다시 여시거나
매칭 전 취소로 닫으십시오 |
| 구매 화면에서 [Cancel] | 거래취소 (request_closed) |
| 구매 등록 뒤, 매칭 전에 진행 화면을 닫음 | 거래취소 (popup_closed) |
popupUrl 은 받은 그대로 여십시오.
수량은 저희가 들고 있는 값을 씁니다 — 브라우저가 금액을 들고 있으면 사용자가
고칠 수 있습니다.구매 등록
api 만 해당합니다. 한도는 「결제 요청」 몫입니다.
POST /api/v1/payments/{paymentId}/purchase
요청
| 자리 | 이름 | 형식 | 필수 | 무엇 |
|---|---|---|---|---|
| 주소 | paymentId | 문자열 | 예 | 결제 요청이 준 결제 번호. 본문은 없습니다 |
사용자가 지갑 앱에서 로그인을 승인한 뒤 5분 안에
가맹점 화면의 [구매] 를 누르면 부르십시오. 409 login_required 면 아직 승인
전입니다 — 잠시 뒤 다시 부르십시오. 승인 뒤 5분 안에 [구매] 가 없으면 결제는 거래취소
(login_expired)입니다.
성공 응답 · 200 OK
{ "paymentId": "pay_Q2x7Lr0kVb3sT9mWzA1cN-8e", "status": "TRADING", "statusReason": null, "orderStatus": "WAITING" }
| 필드 | 무엇 |
|---|---|
paymentId | 결제 번호 |
status | TRADING, 거래소가 주문을 거절했으면 CANCELLED |
statusReason | CANCELLED 일 때 거래소가 준 까닭 (7절). 그 밖에는 null |
orderStatus | 거래소 주문 상태 (예 WAITING). 아직 모르면 null |
거래소가 주문을 거절하면 status 가 CANCELLED 이고 까닭이
statusReason 에 옵니다 — 오류가 아니라 200 입니다. 한 번 받아들인 구매 등록은 다시 부르셔도
다른 확인 없이 그 결제의 지금 값을 드립니다.
방식이 api 가 아니면 409 wrong_mode, 결제가 거래중이 아니거나
취소 요청이 있으면 409 payment_not_open 입니다.
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
not_found | 404 | 없는 결제이거나 다른 가맹점의 결제 | 다시 보내지 마십시오 — paymentId 를 확인하십시오 |
wrong_mode | 409 | 방식이 api 가 아님 (팝업 방식은 사용자가 팝업에서 구매 등록) | 다시 보내지 마십시오 |
payment_not_open | 409 | 결제가 거래중(TRADING)이 아니거나 매칭 전 취소를 받은 뒤임 |
다시 보내지 마십시오 — 결제 상태 조회로 지금 상태를 보십시오 |
login_required | 409 | 지갑 앱 로그인 승인이 아직 없거나 승인 뒤 5분이 지남 | 잠시 뒤 다시. 5분이 지났으면 결제는 거래취소(login_expired)가 됩니다 |
rate_limited | 429 | 「결제 요청」 또는 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
exchange_unavailable | 503 | 거래소가 답하지 않음. 구매 등록은 받아 두었고 저희가 거래소에 다시 보냅니다 — 결제는 거래중 그대로 | 잠시 뒤 다시 부르시거나 웹훅 · 결제 상태 조회로 결과를 보십시오 |
매칭 전 취소
POST /api/v1/payments/{paymentId}/cancel
매칭 전에만 됩니다. 매칭 뒤면 409
already_matched — 결제는 그대로 진행됩니다. 이미 거래완료면 409
payment_finished 입니다. 한도는 「전체 합」 몫만 셉니다.
요청
| 자리 | 이름 | 형식 | 필수 | 무엇 |
|---|---|---|---|---|
| 주소 | paymentId | 문자열 | 예 | 결제 번호. 본문은 없습니다 |
성공 응답 · 200 OK
{ "paymentId": "pay_Q2x7Lr0kVb3sT9mWzA1cN-8e", "status": "CANCELLED", "statusReason": "cancelled_before_match" }
| 필드 | 무엇 |
|---|---|
paymentId | 결제 번호 |
status | CANCELLED |
statusReason | 보통 cancelled_before_match. 그 사이 거래소에서 먼저 끝났으면 그 까닭
(request_closed · popup_closed 등, 7절) |
이미 거래취소된 결제면 같은 답을 다시 드립니다. 거래취소도 웹훅이 갑니다.
취소를 받은 뒤에는 구매 등록이 409 payment_not_open 입니다.
거래소나 지갑이 곧바로 답하지 않으면 503 입니다. 이때 취소 요청은 받아 두었고
저희가 다시 보냅니다 — 거래취소가 되면 웹훅으로 알려 드립니다. 그 사이 매칭되면 결제는 그대로 진행됩니다.
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
not_found | 404 | 없는 결제이거나 다른 가맹점의 결제 | 다시 보내지 마십시오 — paymentId 를 확인하십시오 |
already_matched | 409 | 이미 매칭됐거나 분쟁 중 — 취소하지 않았고 결제는 그대로 진행됩니다 | 다시 보내지 마십시오 — 결과는 웹훅 · 결제 상태 조회로 |
payment_finished | 409 | 이미 거래완료 | 다시 보내지 마십시오 |
rate_limited | 429 | 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
wallet_unavailable | 503 | 지갑이 답하지 않음 — 취소 요청은 받아 두었고 저희가 다시 보냅니다 | 웹훅을 기다리시거나 잠시 뒤 다시 |
exchange_unavailable | 503 | 거래소가 답하지 않음 — 취소 요청은 받아 두었고 저희가 다시 보냅니다 | 웹훅을 기다리시거나 잠시 뒤 다시 |
결제 상태 조회
GET /api/v1/payments/{paymentId}
웹훅을 못 받으셨을 때 따라잡는 창구입니다. 한도는 「결제 상태 조회」 몫입니다.
요청
| 자리 | 이름 | 형식 | 필수 | 무엇 |
|---|---|---|---|---|
| 주소 | paymentId | 문자열 | 예 | 결제 번호. 본문은 없습니다 |
성공 응답 · 200 OK
{ "paymentId": "pay_Q2x7Lr0kVb3sT9mWzA1cN-8e", "mode": "popup_wallet", "popupUrl": "https://…",
"status": "COMPLETED", "statusReason": null, "merchantUserId": "member-1024",
"buyerWalletAddress": "sjo1…", "merchantWalletAddress": "sjo1…", "amountUsde": "100.00",
"tradeNo": "…", "contract": { "address": "sjo1…", "paymentId": "pay_Q2x7Lr0kVb3sT9mWzA1cN-8e",
"status": "paid", "merchantAmountUsde": "99.73" }, "arrivedUsde": "99.73", "txHash": "…",
"createdAt": "2026-10-21T03:04:05.123Z", "updatedAt": "2026-10-21T03:12:40.551Z" }
| 필드 | 무엇 |
|---|---|
paymentId | 결제 번호 |
mode | api · popup_wallet · popup_password |
popupUrl | 팝업 둘의 주소. REQUESTED 면 다시 열 수 있습니다. api 는 null |
status | 7절의 다섯 중 하나 |
statusReason | CANCELLED 일 때 까닭 (7절) |
merchantUserId | 연동된 가맹점 회원 아이디 |
buyerWalletAddress | 구매자 지갑 |
merchantWalletAddress | 가맹점 지갑 |
amountUsde | 요청 수량 (예 "100.00") |
tradeNo | 거래소 거래번호. 매칭 전에는 null. 한 번 받으면 바뀌지 않습니다 |
contract | 결제 컨트랙트 — { address, paymentId, status, merchantAmountUsde }.
status 는 registered · paid · cancelled. 등록 전에는 null |
arrivedUsde | 가맹점 지갑에 도착한 수량 (예 "99.73"). 확인 전에는 null |
txHash | 블록체인 기록. 확인 전에는 null |
createdAt · updatedAt | 만든 때 · 마지막으로 바뀐 때 |
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
not_found | 404 | 없는 결제이거나 다른 가맹점의 결제 | 다시 보내지 마십시오 — paymentId 를 확인하십시오 |
rate_limited | 429 | 「결제 상태 조회」 또는 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
지갑 잔액 조회
GET /api/v1/merchant/wallets/{walletAddress}/balance
한도는 「전체 합」 몫만 셉니다.
요청
| 자리 | 이름 | 형식 | 필수 | 무엇 |
|---|---|---|---|---|
| 주소 | walletAddress | 문자열 | 예 | 잔액을 볼 SJO 주소 (sjo1…). 본문은 없습니다 |
성공 응답 · 200 OK
{ "walletAddress": "sjo1…", "balanceUsde": "12.50", "height": 1234567 }
| 필드 | 형식 | 무엇 |
|---|---|---|
walletAddress | 문자열 | 보내신 주소 |
balanceUsde | 문자열 | USDE 잔액, 소수 둘째 자리 |
height | 숫자 | 잔액을 읽은 블록 높이 |
그때 체인에서 읽은 USDE 잔액과 블록 높이입니다. 한 번 더 확인하는 용도입니다 —
어느 결제의 입금인지는 가르지 못합니다. 결제의 입금은 웹훅 · 상태 조회의
arrivedUsde · txHash 로 보십시오. 체인을 읽지 못하면 503
chain_unavailable 입니다.
오류
| code | HTTP | 언제 | 하실 일 |
|---|---|---|---|
invalid_address | 400 | SJO 주소가 아님 (체크섬) | 고쳐서 다시 |
unauthorized | 401 | 헤더가 없거나 가맹점 코드 · API 키가 틀림 | 다시 보내지 마십시오 — 키를 확인하십시오 |
merchant_inactive | 403 | 승인된 가맹점이 아니거나 정지 · 삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터에 문의해 주십시오 |
rate_limited | 429 | 「전체 합」 한도를 넘음 | 잠시 뒤 다시 (retryAfterSeconds 뒤) |
internal | 500 | 저희 쪽 문제 | 잠시 뒤 다시 |
chain_unavailable | 503 | 체인에서 잔액을 읽지 못함 | 잠시 뒤 다시 |
6. 웹훅 — 저희가 보내드리는 것
결제 상태가 바뀌면 저희가 등록해 주신 주소(마이페이지의 웹훅 주소, https://)로 POST 합니다.
웹훅 주소가 비어 있으면 보내지 않습니다 — 그때는 결제 상태 조회로 보십시오. 사설망 같은 내부 주소로 가는 웹훅 주소에는 보내지 않습니다.
언제
상태가 바뀔 때마다 한 번 보냅니다 — 요청 기록에서 거래중(REQUESTED → TRADING), 거래완료(COMPLETED),
거래취소(CANCELLED), 분쟁(DISPUTED).
결제를 만들 때는 보내지 않습니다 — 요청 기록(REQUESTED), api 방식의
TRADING, 곧바로 거래취소(CANCELLED) 모두 결제 요청의 답으로 아십니다.
만든 뒤에 바뀐 것은 보냅니다 — 결제 요청 처리 중 뒤이어 거래취소가 된 경우(wallet_undeliverable 등)도 웹훅이 갑니다.
저희가 보내는 요청
| 헤더 | 무엇 |
|---|---|
Content-Type | application/json (UTF-8) |
X-EggPay-Signature | 보낸시각 + "." + 본문 을 웹훅 비밀키로
HMAC-SHA256 한 값 — 소문자 16진수 64자 |
X-EggPay-Timestamp | 보낸 시각 — 1970년부터 흐른 초
(예: 1789545372) |
{"paymentId":"pay_Q2x7Lr0kVb3sT9mWzA1cN-8e","status":"COMPLETED","statusReason":null,"amountUsde":"100.00","tradeNo":"…","contract":{"address":"sjo1…","paymentId":"pay_Q2x7Lr0kVb3sT9mWzA1cN-8e","status":"paid","merchantAmountUsde":"99.73"},"arrivedUsde":"99.73","txHash":"…","occurredAt":"2026-10-21T03:12:40.551Z"}
본문의 필드는 결제 상태 조회와 같은 뜻입니다. 모든 필드가 늘 오고, 값이 없으면 null 입니다.
| 필드 | 무엇 |
|---|---|
paymentId | 결제 번호 |
status | 바뀐 뒤 상태 — TRADING · COMPLETED · CANCELLED · DISPUTED |
statusReason | 거래취소 까닭 (7절). incident 면 충전을 취소하라는 신호입니다 |
amountUsde | 요청 수량 |
tradeNo | 거래소 거래번호 |
contract | { address, paymentId, status, merchantAmountUsde } |
arrivedUsde | 거래완료면 가맹점 지갑에 도착한 수량 |
txHash | 거래완료면 블록체인 기록 |
occurredAt | 상태가 바뀐 때 |
상태마다 하실 일은 7절에 있습니다.
서명 확인
signature = HMAC-SHA256(웹훅 비밀키, X-EggPay-Timestamp + "." + 받은 본문)
- 본문을 받은 바이트 그대로 둡니다 (JSON 으로 읽기 전).
X-EggPay-Timestamp값, 점(.) 하나, 받은 본문을 이어 붙입니다.- 웹훅 비밀키 글자(UTF-8)를 키로 HMAC-SHA256 을 계산해 소문자 16진수로 씁니다.
X-EggPay-Signature와 같은지 봅니다. 시간 차가 드러나지 않는 비교를 쓰십시오. 다르면 버리십시오.
⚠ 받은 본문 그대로 서명해 주십시오. JSON 을 다시 만들면 띄어쓰기 하나로 서명이 달라집니다. 시각이 서명 안에 들어 있어, 한 번 가로챈 알림을 나중에 다시 보내도 가맹점이 걸러낼 수 있습니다.
계산이 맞는지 볼 때 쓰는 예입니다 (실제 본문이 아닙니다).
| 무엇 | 값 |
|---|---|
| 웹훅 비밀키 | test_secret |
X-EggPay-Timestamp | 1789545372 |
| 본문 | {"paymentId":"pay_test"} |
X-EggPay-Signature | 09b3c7c727cf96bfd81efd65741ffc2cdf0ba4474b355e63df6c406d5b160bf5 |
// Java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
mac.update((timestampHeader + ".").getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(mac.doFinal(rawBody)); // rawBody: 받은 본문 byte[]
boolean ok = MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
signatureHeader.getBytes(StandardCharsets.UTF_8));
가맹점의 답
2xx 를 돌려주시면 됩니다. 3xx 는 따르지 않고 실패로 봅니다. 그 밖이면 6회까지 다시 보냅니다 — 30초 · 2분 · 8분 · 32분 · 2시간 · 8시간 뒤입니다 (기본값).
| 가맹점의 답 | 저희는 |
|---|---|
2xx | 전달됨으로 적고 끝냅니다. 답의 본문은 보지 않습니다 |
3xx | 따라가지 않고 실패로 봅니다 |
4xx · 5xx | 실패로 보고 다시 보냅니다 |
| 10초(기본값) 안에 답이 없음 · 연결 안 됨 | 실패로 보고 다시 보냅니다 |
다시 보내기 간격은 앞 실패에서 잽니다. 다시 보낼 때도 본문은 처음과 같고, X-EggPay-Timestamp 와
서명만 새로 만듭니다. 처음 1회와 다시 보내기 6회가 모두 실패하면 그 알림은 더 보내지 않습니다 — 결제 상태 조회로 보십시오.
답이 늦어 실패로 본 뒤 다시 보내면 같은 알림을 두 번 받으실 수 있습니다 — paymentId 와 status 로
한 번만 처리해 주십시오.
순서는 보장하지 않습니다. occurredAt 과 상태 조회로 최신을 가려 주십시오.
웹훅은 못 갈 수 있습니다. 서버가 잠깐 죽거나 네트워크가 끊기면 그 줄기가 끊깁니다. 상태 조회로 따라잡아 주십시오 — 그래서 둘 다 드리는 것입니다.
7. 결제 상태
| 값 | 뜻 | 가맹점이 하실 일 |
|---|---|---|
REQUESTED | 요청 기록 — 팝업 방식에서 구매 등록 전입니다. 진행 중이 아닙니다 | 기다립니다. 같은 popupUrl 로 다시 열 수 있습니다. 원하시면 매칭 전 취소로 닫으십시오 |
TRADING | 거래중 | 기다립니다 |
COMPLETED | 거래완료 | arrivedUsde 를 확인하고 충전해 주십시오. 원하시면 지갑 잔액 조회로 한 번 더 보십시오 |
CANCELLED | 거래취소 | 충전건을 취소 처리해 주십시오. 까닭은 statusReason |
DISPUTED | 분쟁 | 결말이 날 때까지 기다립니다 |
결제는 거래완료나 거래취소 가운데 하나로 반드시 끝납니다.
거래취소 까닭 (statusReason)
| 값 | 언제 |
|---|---|
popup_closed | 팝업 방식 — 구매 등록 뒤 매칭 전에 진행 화면을 닫음 |
cancelled_before_match | 매칭 전 취소 (가맹점 취소 · 진행 화면 [취소] · 거래소 앱) |
request_closed | 팝업 방식의 요청 기록이 닫힘 (같은 구매자의 새 충전 · 연동 해제 · 구매 화면 [Cancel]) |
login_rejected · login_expired | api 방식 — 지갑 앱 로그인 거절 · 5분 지남 (로그인 뒤 [구매] 없이 지남 포함) |
not_member · account_withdrawn · account_inactive · kyc_required · grade_not_allowed |
거래소 회원 자격이 없음 (거래소가 준 이름) |
amount_out_of_range · payment_closed · exchange_halted · account_locked ·
wallet_suspended · login_invalid · member_busy | 거래소가 주문을 거절함 (거래소가 준 이름) |
wallet_undeliverable | 지갑 앱에 전할 수 없음 |
mismatch | 결제 번호 · 수량 · 주소가 맞지 않아 진행하지 않음 |
trade_cancelled | 거래가 취소됨 (서명 마감 등) |
incident | 거래소 사고처리 — 충전 취소 신호 |
registration_mismatch | 결제 컨트랙트 등록이 맞지 않음 |
pay_failed | 가맹점 송금이 체인에서 확정되지 못함 |
merchant_suspended | 가맹점이 정지됨 — 그때 매칭 전이던 결제. 이미 매칭된 결제는 끝까지 갑니다.
정지된 동안에는 로그인과 API 가 모두 막히고(403 merchant_inactive), 정지된 동안에도 웹훅은 보내 드립니다 |
거래소가 준 까닭은 그 이름 그대로 싣습니다. 위 표에 없는 이름이 오더라도 CANCELLED 면
충전건을 취소 처리해 주십시오.
8. 수수료
간편결제는 수수료를 받지 않습니다. 도착 수량에서 체인 전송 수수료와 가스만 빠집니다 — 100.00 USDE 미만은 전송 수수료 0.17 + 가스 0.02 USDE 고정, 100.00 USDE 부터는 전송 수수료 0.17% 와 가스 0.1% 를 각각 올림(0.01 USDE 단위)합니다.
결제 수량 amountUsde | 전송 수수료 | 가스 | 도착 arrivedUsde |
|---|---|---|---|
"12.50" | 0.17 | 0.02 | "12.31" |
"50.00" | 0.17 | 0.02 | "49.81" |
"99.99" | 0.17 | 0.02 | "99.80" |
"100.00" | 0.17 | 0.10 | "99.73" |
"100.01" | 0.18 | 0.11 | "99.72" |
"1000.00" | 1.70 | 1.00 | "997.30" |
"1234.56" | 2.10 | 1.24 | "1231.22" |
도착 수량은 체인이 정한 값(arrivedUsde)이 기준입니다. 식으로 다시
계산해 맞추지 않으셔도 됩니다.
100개 결제의 예
| 가맹점 지갑에 도착 | 99.73 USDE |
| 가맹점이 충전해 주실 것 | 100개에 대한 머니 |
9. 관리자 웹
승인되면 가맹점 계정도 함께 드립니다. 관리자 웹에서 가맹점 계정으로 하실 수 있는 일은 이렇습니다.
| 무엇 | 내용 |
|---|---|
| 거래내역 | 누가(가맹점 회원 아이디 merchantUserId) ·
어디서(출발 지갑) · 어디로(도착 지갑) ·
몇 개 입금했는지 · 상태(요청 기록 · 거래중 · 거래완료 · 거래취소 · 분쟁) ·
알림(전달됨 · 보내는 중 · 전달 실패) |
| 결제 찾기 | 손님 문의가 들어왔을 때 지갑 주소나 회원 아이디(둘 다 정확히 일치) 와 기간으로 찾으실 수 있습니다. 찾으시면 결제 건수 · 등록 합계 · 도착 합계 · 마지막 결제가 함께 보입니다 |
| 마이페이지 | 내 가맹점 정보(이름·받을 지갑 주소·웹훅 주소·전화·연락 방법 — 맨 아래 [저장] 하나) · 웹훅 서명 키 [새로 발급] · 지급 지갑 잔액 · 비밀번호 바꾸기 |
| API | API 키 목록(발급한 때·상태) · [재발급] · 새 API 키 받기 |
9-1. 가맹점 정보를 바꾸려면
가맹점 이름·받을 지갑 주소·웹훅 주소·전화·연락 방법은 마이페이지에서 직접 바꾸실 수 있습니다. 「내 가맹점 정보」에서 고치고 맨 아래 [저장]을 누르시면 됩니다. 신청과 승인은 없습니다. 틀린 칸이 있으면 그 칸 아래에 까닭이 뜨고 아무것도 저장되지 않습니다.
| 바꾸는 것 | 어떻게 |
|---|---|
| 가맹점 이름 · 전화 · 연락 방법 | 마이페이지에서 고치고 [저장] — 곧바로 바뀝니다 |
| 받을 지갑 주소 | 마이페이지에서 새 주소를 넣고 [저장] → 그 주소의 SJO 지갑 앱에서 [서명] — 서명하면 곧바로 바뀝니다. 취소하시거나 5분이 지나면 바뀌지 않습니다. 그 주소를 쓰는 SJO 지갑이 없으면 등록되지 않습니다. 이미 진행 중인 결제는 요청 때의 주소로 끝납니다 |
| 웹훅 받을 주소 | 마이페이지에서 고치고 [저장] — 곧바로 바뀝니다. https:// 로 시작해야 하고,
비우시면 웹훅을 보내지 않습니다 |
| 웹훅 비밀키 | 마이페이지 웹훅 주소 아래 「웹훅 서명 키」 [새로 발급] — [저장]과 따로 곧바로 됩니다. 새 키는 그 화면에 한 번만 보입니다. 지금 키는 곧바로 못 쓰고, 그 뒤 웹훅은 모두 새 키로 서명합니다 |
| API 키 재발급 | 관리자 웹 「API」에서 [재발급] — 신청 없이 곧바로 새 키가 나오고 그 화면에 한 번만 보입니다. 그 키는 곧바로 못 씁니다. 저희가 발급해 드린 새 키가 있으면 같은 화면에서 [새 키 받기]로 한 번 받아 가실 수 있습니다 |
| 가맹점 탈퇴 | 고객센터로 요청해 주십시오. 저희가 확인하고 처리합니다. 처리되면 그때부터 로그인과 API 가 막히고 목록에서 사라집니다. 이미 들어온 결제 내역은 남습니다 |
10. 오류
{ "error": { "code": "invalid_address", "message": "…" } }
message 는 사람이 읽는 글입니다(Accept-Language 에 따라 한국어 · 영어). 프로그램은 code 로 가르십시오.
429 에는 error 옆에 retryAfterSeconds(다시 보내도 되는 때까지의 초)가 붙고,
같은 값이 Retry-After 헤더에도 옵니다.
{ "error": { "code": "rate_limited", "message": "…" }, "retryAfterSeconds": 12 }
아래는 가맹점 API 가 돌려드리는 모든 오류 코드입니다. 창구마다 언제 오는지는 각 창구의 오류 표에 있습니다.
| code | HTTP | 창구 | 뜻 | 하실 일 |
|---|---|---|---|---|
invalid_request | 400 | 연동 시작 · 연동 확인 · 연동 해제 · 결제 요청 | 형식 · 필수 칸 · 길이 · 모르는 필드 · 소수 셋째 자리 | 고쳐서 다시 |
invalid_address | 400 | 연동 시작 · 잔액 조회 · 결제 요청 | SJO 주소 체크섬이 틀림 | 고쳐서 다시 |
merchant_wallet_mismatch | 400 | 결제 요청 | 가맹점 지갑이 등록한 지급 주소와 다름 | 고쳐서 다시 |
unauthorized | 401 | 환율 밖 모두 | 키가 없거나 틀림 | 다시 보내지 마십시오 — 키 확인 |
merchant_inactive | 403 | 환율 밖 모두 | 승인된 가맹점이 아니거나 정지·삭제된 가맹점 | 다시 보내지 마십시오 — 고객센터 |
not_found | 404 | 연동 해제 · 구매 등록 · 매칭 전 취소 · 결제 상태 조회 | 없는 결제 · 남의 결제, 해제할 연동이 없음. 없는 주소를 부르셔도 이것입니다 | 다시 보내지 마십시오 |
wallet_not_linked | 404 | 결제 요청 | 구매자 지갑이 이 가맹점에 연동되지 않음 | 다시 보내지 마십시오 — 먼저 지갑 연동 |
method_not_allowed | 405 | 모두 | 부르는 방식이 틀림 (예: POST 창구를 GET 으로). Allow 헤더에 맞는 방식이 있습니다 | 고쳐서 다시 |
conflict | 409 | 결제 요청 | 같은 merchantRequestId 에 다른 내용 | 다시 보내지 마십시오 — 새 결제면 새 키 |
wallet_link_conflict | 409 | 연동 시작 | 그 회원이 다른 주소로, 또는 그 주소가 다른 회원으로 연동됐거나 그런 요청이 살아 있음 (5분 안) | 다시 보내지 마십시오 — 해제 뒤 또는 마감 뒤 다시 시작 |
payment_in_flight | 409 | 연동 해제 · 결제 요청 | 그 지갑에 진행 중인 결제가 있음 | 그 결제가 끝난 뒤 다시 |
buyer_busy | 409 | 연동 해제 · 결제 요청 | 같은 구매자의 다른 요청을 처리 중 — 같은 요청을 다시 보내 주십시오 | 잠시 뒤 다시 |
wrong_mode | 409 | 구매 등록 | 방식이 api 가 아님 | 다시 보내지 마십시오 |
payment_not_open | 409 | 구매 등록 | 결제가 거래중이 아니거나 취소 요청이 있음 | 다시 보내지 마십시오 |
login_required | 409 | 구매 등록 | 지갑 앱 로그인 승인 전이거나 5분이 지남 | 잠시 뒤 다시 |
already_matched | 409 | 매칭 전 취소 | 이미 매칭됨 — 결제는 그대로 | 다시 보내지 마십시오 |
payment_finished | 409 | 매칭 전 취소 | 이미 거래완료 | 다시 보내지 마십시오 |
wallet_undeliverable | 422 | 연동 시작 | 그 주소의 지갑 사용자가 없음 | 다시 보내지 마십시오 — 주소 확인 |
rate_limited | 429 | 모두 | 호출 한도 초과 — retryAfterSeconds 를 함께 드립니다 | 잠시 뒤 다시 |
internal | 500 | 모두 | 저희 쪽 문제입니다. 다시 부르셔도 됩니다 | 잠시 뒤 다시 |
exchange_unavailable | 503 | 연동 해제 · 결제 요청 · 구매 등록 · 매칭 전 취소 | 거래소가 답하지 않음. 연동 해제 · 결제 요청은 처리하지 않았고, 구매 등록 · 매칭 전 취소는 받아 두고 저희가 다시 보냅니다. 다시 부르셔도 됩니다 | 잠시 뒤 다시 |
wallet_unavailable | 503 | 매칭 전 취소 | 지갑이 답하지 않음 — 취소 요청은 받아 두고 저희가 다시 보냅니다. 다시 부르셔도 됩니다 | 잠시 뒤 다시 |
chain_unavailable | 503 | 잔액 조회 · 결제 요청 | 체인을 읽지 못함 (결제 요청은 USDE 멈춤 상태) — 처리하지 않았습니다. 다시 부르셔도 됩니다 | 잠시 뒤 다시 |
usde_paused | 503 | 결제 요청 | USDE 가 멈춤 | 잠시 뒤 다시 |
rate_unavailable | 503 | 환율 | 저장된 환율이 없음 — 이번 회차를 받지 못해도 앞서 저장된 값은 계속 드립니다 | 잠시 뒤 다시 |
JSON 오류 본문 없이 오는 답도 있습니다 — 주소 안의 %2F · %5C 에 저희 웹 서버가 주는 400,
Accept 가 JSON 을 받지 않을 때의 406 이 그렇습니다 (2절). HTTP 상태로 가르십시오.
사용자가 거래소 회원이 아니거나 거래소 계정으로 결제할 수 없는 경우는 오류가 아닙니다 —
연동 확인의 exchange.ready · reason 과, 결제의 CANCELLED 와
statusReason(not_member 등)으로 알려 드립니다.