도움말 / 리셀러 API
리셀러 API
내 주문·고객 관리 시스템에서 지갑 조회, 솜씨 크레딧 지급, 웹호스팅 신청, 화이트라벨 고객 크레딧 배정을 자동으로 처리하는 REST API입니다. 대시보드에서 하는 것과 같은 규칙(단가, 잔액 확인, 원장 기록, 메일)으로 처리됩니다. 쇼츠·이미지·시리즈를 내 서비스에서 직접 만드는 연동은 솜씨 API v1을 쓰세요.
개요
| 기본 주소 | https://reseller.somssii.com/api/v1 |
| 인증 | Authorization: Bearer rsk_live_… (대시보드 계정 → API에서 발급) |
| 형식 | 요청·응답 모두 JSON, UTF-8. POST 본문은 Content-Type: application/json, 64KB 이하 |
| 응답 | 성공 {"ok":true,"data":…} · 실패 {"ok":false,"error":{"code":"…","message":"…"}} |
| 금액·수량 | 금액은 원(정수), 크레딧은 정수. 요율은 bp(1bp = 0.01%, 1000 = 10%) |
| 시각 | 한국 시간(KST) YYYY-MM-DD HH:MM:SS, 시간대 표시 없음 |
| 범위 | 키를 만든 리셀러 본인의 데이터만. 다른 리셀러의 번호를 넣으면 404 |
| 메서드 | 경로 | 권한 | 설명 |
|---|---|---|---|
| GET | /me | read | 내 계정, 적용 단가·요율, 잔액 |
| GET | /wallet | read | 지갑 잔액, 출금 가능액 |
| GET | /wallet/ledger | read | 지갑 원장 |
| GET | /referral | read | 추천 링크, 클릭·가입, 수수료 합계 |
| GET | /commissions | read | 수수료 내역 |
| GET | /credits/lookup?email= | read | 솜씨 회원 지급 가능 여부 |
| POST | /credits/transfers | credits | 크레딧 지급(지갑 차감) |
| GET | /credits/transfers | read | 지급 내역 |
| GET | /credits/transfers/{id} | read | 지급 한 건 |
| GET | /hosting/plans | read | 판매 중인 요금제와 내 가격 |
| GET | /hosting/accounts | read | 내 호스팅 계정 목록 |
| GET | /hosting/accounts/{id} | read | 계정 한 건 + 최근 작업 |
| POST | /hosting/accounts | hosting | 호스팅 신청(첫 달 지갑 차감) |
| GET | /wl/customers | read | 내 스토어 고객 |
| POST | /wl/customers/{id}/credits | wl | 고객 크레딧 지급·차감 |
키 발급·권한·인증
키 발급
- 대시보드 계정 → API의 리셀러 API 키에서 키 이름(1~60자, 예: "주문 서버")을 적고 필요한 권한만 골라 만듭니다.
- 키는
rsk_live_+ 소문자 16진수 40자(모두 49자)이며, 만든 직후 한 번만 보입니다. 바로 서버의 비밀 저장소(환경 변수 등)에 보관하세요. 이 사이트에는 키의 해시와 앞 16자만 남습니다. - 활성 키는 최대 5개입니다. 목록에서 키별 마지막 사용 시각을 확인하고, 필요 없는 키는 [폐기]합니다. 폐기한 키는 바로 거절됩니다.
권한(scope)
| 권한 | 할 수 있는 일 |
|---|---|
read | 모든 GET(조회). 아무 권한도 고르지 않으면 read만 갖습니다 |
credits | POST /credits/transfers — 지갑에서 바로 차감됩니다 |
hosting | POST /hosting/accounts — 첫 달 요금이 지갑에서 바로 차감됩니다 |
wl | POST /wl/customers/{id}/credits |
조회(GET)에는 언제나 read 권한이 필요합니다. 예를 들어 지급만 하고 결과를 조회하지 않는 서버라면 credits만, 지급 후 내역도 확인한다면 read와 credits를 함께 줍니다.
인증 헤더
Authorization: Bearer rsk_live_0123456789abcdef0123456789abcdef01234567 Authorization헤더를 보낼 수 없는 환경에서는X-Api-Key: rsk_live_…를 씁니다.- 쿠키·세션·CSRF 토큰은 쓰지 않습니다. 대시보드 로그인 쿠키가 함께 와도 키로만 인증합니다.
- 계정이 승인 대기·정지·반려 상태면 키가 있어도
403 account_inactive입니다(키는 그대로 남고, 정지가 풀리면 다시 쓸 수 있습니다).
응답 형식·오류
// 성공
{ "ok": true, "data": { "balance": 1250000, "withdrawable": 32000 } }
// 실패 — 입력 오류는 fields 에 필드별 문구
{ "ok": false, "error": { "code": "validation", "message": "지급 크레딧(credits)을 양의 정수로 보내세요.",
"fields": { "credits": "지급 크레딧(credits)을 양의 정수로 보내세요." } } } | HTTP | error.code | 뜻·대응 |
|---|---|---|
| 200 | — | 성공(조회, 또는 같은 client_token 재전송에 대한 원래 결과) |
| 201 | — | 새로 처리됨(지급·신청·배정) |
| 400 | bad_request | JSON 객체 본문이 없거나 형식이 틀림(Content-Type 확인, 64KB 이하) |
| 401 | unauthorized | 키를 보내지 않음 |
| 401 | invalid_key | 없는 키, 형식이 틀린 키, 폐기된 키 |
| 403 | scope_denied | 그 요청에 필요한 권한이 키에 없음 |
| 403 | account_inactive | 리셀러 계정이 이용 중이 아님 |
| 404 | not_found | 없는 경로, 없는 번호, 다른 리셀러의 번호 |
| 405 | method_not_allowed | 그 경로에 없는 메서드(Allow 헤더 참고) |
| 409 | conflict | 같은 client_token으로 다른 내용을 보냄 → 새 요청에는 새 토큰 |
| 422 | validation | 입력값·업무 규칙 오류. message(와 fields)를 그대로 보여 주면 됩니다 |
| 422 | insufficient_balance | 지갑 잔액 부족(크레딧 지급·호스팅), 또는 고객 크레딧보다 많이 차감 |
| 429 | rate_limited | 요청 제한 초과 → Retry-After(초) 뒤에 다시 |
| 500 | server_error | 서버 오류. error.request_id를 함께 문의 |
| 503 | unavailable | 지금 받지 않는 기능(예: 호스팅 신청 중단) 또는 점검 중 |
error.code로 분기하고, 사용자에게는 한국어 message를 보여 주세요. 쓰기 요청이 시간 초과·5xx로 끝났다면 같은 client_token으로 다시 보내면 안전합니다(중복 방지).
요청 제한
- 키마다 분당 120회(모든 요청), 그 가운데 쓰기(POST)는 분당 10회입니다.
- 솜씨 회원 조회(
GET /credits/lookup)는 대시보드와 같은 한도(리셀러당 10분에 30회)를 함께 씁니다. 지급 요청에서 회원 확인에 실패한 경우도 이 한도에 더해집니다. - 넘으면
429 rate_limited와Retry-After헤더(초),error.retry_after가 옵니다. 그 시간만큼 기다린 뒤 다시 보내세요.
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{ "ok": false, "error": { "code": "rate_limited", "message": "쓰기 요청이 너무 많습니다(키당 분당 10회). 잠시 후 다시 시도하세요.", "retry_after": 37 } } 중복 방지(client_token)
모든 POST 요청에는 client_token이 필요합니다. 네트워크 오류로 같은 요청을 다시 보내도 지급·신청·배정은 한 번만 처리됩니다.
- 형식: 영문·숫자·
_·-16~64자. 예:bin2hex(random_bytes(16)), UUID,order-1042-credit - 새 요청마다 새 값을 만들고, 보내기 전에 내 주문 기록에 함께 저장하세요. 재시도할 때는 저장해 둔 같은 값을 보냅니다.
- 이미 처리된 토큰을 같은 내용으로 다시 보내면 처리하지 않고 원래 결과를
200,"replayed": true로 돌려줍니다. - 같은 토큰으로 다른 내용(다른 회원·수량·고객·아이디 등)을 보내면
409 conflict입니다. - 토큰은 리셀러 단위로 구분됩니다(키를 바꿔도 같은 토큰은 같은 요청으로 봅니다).
목록·페이지
목록 조회는 ?page=1&per_page=50을 받습니다. per_page는 1~100(기본 50)이고, 최신순입니다. 마지막 쪽보다 큰 page를 주면 마지막 쪽을 돌려줍니다.
{ "ok": true, "data": { "items": [ … ], "page": 1, "per_page": 50, "total": 128 } } 계정·지갑
내 계정 정보와 지금 적용되는 단가·요율, 지갑 잔액, 이 키의 정보입니다. 연동 점검용 첫 호출로 쓰세요.
{
"ok": true,
"data": {
"id": 12,
"company": "(주)예시상사",
"status": "active",
"ref_code": "ab3kq7xz",
"rates": { "credit_unit_price": 90, "commission_rate_bp": 1000, "hosting_discount_bp": 500 },
"balance": 1250000,
"somssii_linked": true,
"key": { "prefix": "rsk_live_0123456", "label": "주문 서버", "scopes": ["read", "credits"] }
}
} credit_unit_price는 크레딧 1개당 원, commission_rate_bp·hosting_discount_bp는 bp입니다(위 예시 값: 10%, 5%).
{ "ok": true, "data": { "balance": 1250000, "withdrawable": 32000 } } withdrawable은 지금 출금 신청할 수 있는 금액입니다(출금 규칙). 출금 신청 자체는 대시보드에서 합니다.
지갑 원장입니다. 검색 조건: kind(아래 구분), from·to(YYYY-MM-DD), page, per_page.
GET /wallet/ledger?kind=credit_transfer&from=2026-10-01&to=2026-10-31&per_page=20
{ "ok": true, "data": { "items": [
{ "id": 5531, "kind": "credit_transfer", "amount": -45000, "balance_after": 1205000,
"ref_type": "credit_transfer", "ref_id": 881, "memo": "솜씨 회원 cu****@example.com 500크레딧",
"created_at": "2026-10-02 10:15:03" }
], "page": 1, "per_page": 20, "total": 1 } } kind | 구분 | kind | 구분 |
|---|---|---|---|
topup | 충전 | topup_cancel | 충전 취소 |
credit_transfer | 크레딧 지급 | credit_reversal | 크레딧 지급 취소 |
commission | 추천 수수료(회수는 음수) | admin_adjust | 관리자 조정 |
withdrawal | 출금 | withdrawal_refund | 출금 반려·취소 |
hosting_charge | 호스팅 요금 | hosting_refund | 호스팅 환불 |
domain_charge | 도메인 구매(견적 결제) | domain_refund | 도메인 환불(결제 뒤 반려) |
wl_agency_share | 운영대행 수익쉐어(플랫폼 몫) |
{ "ok": true, "data": {
"ref_code": "ab3kq7xz",
"ref_link": "https://reseller.somssii.com/r/ab3kq7xz",
"clicks_30d": 214,
"signups": 37,
"commission": { "rate_bp": 1000, "hold_days": 14, "held": 12000, "credited": 86500 }
} } clicks_30d는 최근 30일 클릭, signups는 누적 추천 가입자, held는 보류 중 수수료 합계, credited는 적립 수수료 누적(회수 반영)입니다.
{ "ok": true, "data": { "items": [
{ "id": 301, "order_ref": "…a1b2c3", "approved_at": "2026-09-20 14:02:11",
"gross_amount": 33000, "cancel_amount": 0, "base_amount": 30000, "rate_bp": 1000,
"target_amount": 3000, "credited_amount": 0, "status": "held", "hold_until": "2026-10-04 14:02:11" }
], "page": 1, "per_page": 50, "total": 1 } } status: held(보류) · credited(적립) · void(무효). target_amount는 현재 계산된 수수료, credited_amount는 지갑에 실제로 반영된 금액입니다. 계산 방법은 추천 수수료를 보세요.
크레딧 판매
솜씨 회원에게 지급할 수 있는지 미리 확인합니다. 이메일은 정확히 같아야 합니다. 가입하지 않았거나 지급 대상이 아닌 회원은 같은 답(eligible: false)을 돌려줍니다.
GET /credits/lookup?email=customer%40example.com
{ "ok": true, "data": { "eligible": true, "email_masked": "cu******@example.com", "name_masked": "홍*동",
"unit_price": 90, "min": 10, "max": 100000 } }
{ "ok": true, "data": { "eligible": false, "message": "솜씨에 가입된 활성 회원이 아닙니다." } } 이메일 형식이 틀리면 422 validation(fields.email)입니다. 이 조회는 리셀러당 10분에 30회까지입니다.
솜씨 회원에게 크레딧을 지급하고 지갑에서 credits × 내 단가를 차감합니다. 대시보드 지급과 같은 처리이며, 지급 완료 메일이 나에게 갑니다(회원에게는 메일 없음).
| 필드 | 형식 | 설명 |
|---|---|---|
email | 문자열, 필수 | 받을 솜씨 회원의 가입 이메일(이용 중인 일반 회원) |
credits | 정수, 필수 | 지급 크레딧. 1회 10 ~ 100,000 |
client_token | 문자열, 필수 | 중복 방지 토큰 |
memo | 문자열, 선택 | 100자 이내. 지급 내역에 남습니다 |
POST /credits/transfers
{ "email": "customer@example.com", "credits": 500, "client_token": "6f1c0d2a9b8e4f7a8c3d2e1f0a9b8c7d", "memo": "주문 #1042" }
HTTP/1.1 201 Created
{ "ok": true, "data": {
"transfer": { "id": 881, "status": "done", "email_masked": "cu******@example.com", "credits": 500,
"unit_price": 90, "amount": 45000, "memo": "주문 #1042",
"created_at": "2026-10-02 10:15:03", "reversed_at": null },
"balance_after": 1205000,
"replayed": false
} } - 지급 대상이 아니면
422 validation(fields.email), 수량이 범위를 벗어나면422 validation, 잔액이 부족하면422 insufficient_balance입니다. 실패한 요청은 아무것도 차감하지 않습니다. - 같은
client_token재전송은200과"replayed": true, 원래 지급 정보입니다. 이때balance_after는 원래 지급 직후의 잔액입니다. - 지급 취소는 API로 할 수 없습니다. 고객센터에 지급 번호(
transfer.id)로 요청하세요(취소 조건). - 지급할 크레딧은 고객이 살 상품에 맞춰 계산합니다: 솜씨 쇼츠 1편 25 · 솜씨 이미지 1세트 10 · 솜씨 시리즈 페이지당 1(안내용 기준값 — 크레딧 계산표). 예: 쇼츠 4편 주문 →
"credits": 100.
내 지급 내역(최신순). 검색 조건: status(done 완료 · reversed 취소됨), page, per_page. 각 항목은 위 transfer와 같은 모양입니다.
GET /credits/transfers?status=done&per_page=2
{ "ok": true, "data": { "items": [
{ "id": 881, "status": "done", "email_masked": "cu******@example.com", "credits": 500, "unit_price": 90,
"amount": 45000, "memo": "주문 #1042", "created_at": "2026-10-02 10:15:03", "reversed_at": null },
{ "id": 874, "status": "done", "email_masked": "le***@example.com", "credits": 100, "unit_price": 90,
"amount": 9000, "memo": "", "created_at": "2026-10-01 16:40:51", "reversed_at": null }
], "page": 1, "per_page": 2, "total": 15 } }
{ "ok": true, "data": { "id": 881, "status": "reversed", "email_masked": "cu******@example.com", "credits": 500,
"unit_price": 90, "amount": 45000, "memo": "주문 #1042",
"created_at": "2026-10-02 10:15:03", "reversed_at": "2026-10-02 11:40:00" } } 웹호스팅
판매 중인 요금제와 내 할인율을 반영한 월 요금(price)입니다. enabled가 false면 지금은 신청을 받지 않습니다.
{ "ok": true, "data": {
"enabled": true,
"hosting_discount_bp": 500,
"items": [
{ "id": 2, "code": "basic", "name": "베이직", "description": "소규모 사이트용",
"list_price": 20000, "price": 19000, "disk_quota_mb": 10240, "php_max_children": 5 }
]
} } disk_quota_mb는 안내용 값이고, php_max_children는 PHP 동시 처리 수입니다.
내 호스팅 계정 목록(최신순, page·per_page). 항목 모양은 아래 상세와 같고 jobs가 없습니다. 목록에서는 plan_code가 null일 수 있으니 plan_id를 쓰세요.
GET /hosting/accounts?per_page=1
{ "ok": true, "data": { "items": [
{ "id": 57, "plan_id": 2, "plan_code": null, "username": "shop1042", "domain": "shop1042.example.com",
"with_www": false, "status": "pending_approval", "status_label": "승인 대기", "monthly_price": 19000,
"auto_renew": true, "paid_until": null, "grace_until": null, "suspend_reason": null,
"ssl_status": "none", "ssl_names": [], "cert_not_after": null, "ssl_message": null, "reject_reason": null,
"created_at": "2026-10-02 10:20:00", "provisioned_at": null, "deleted_at": null }
], "page": 1, "per_page": 1, "total": 3 } }
{ "ok": true, "data": {
"id": 57, "plan_id": 2, "plan_code": "basic", "username": "shop1042", "domain": "shop1042.example.com",
"with_www": false, "status": "active", "status_label": "이용 중", "monthly_price": 19000, "auto_renew": true,
"paid_until": "2026-11-02 10:20:00", "grace_until": null, "suspend_reason": null,
"ssl_status": "issued", "ssl_names": ["shop1042.example.com"], "cert_not_after": "2026-12-31 09:00:00",
"ssl_message": null, "reject_reason": null,
"created_at": "2026-10-02 10:20:00", "provisioned_at": "2026-10-02 10:24:31", "deleted_at": null,
"jobs": [
{ "id": 910, "type": "host_create", "type_label": "호스팅 생성", "status": "done", "status_label": "완료",
"result": "ok", "result_label": "성공", "created_at": "2026-10-02 10:22:05", "finished_at": "2026-10-02 10:24:31" }
]
} } | 필드 | 값 |
|---|---|
status | pending_approval 승인 대기 · provisioning 생성 중 · active 이용 중 · grace 미납 유예 · suspending 정지 처리 중 · suspended 정지됨 · unsuspending 재개 처리 중 · deleting 삭제 중 · deleted 삭제됨 · failed 실패 · rejected 반려 |
suspend_reason | nonpayment 미납 · admin 관리자 · null |
ssl_status | none 없음 · dns_pending DNS 대기 · issued 발급됨 · failed 실패. 대기·실패일 때 ssl_message에 안내 문구 |
paid_until | 다음 결제일(이용 기간 끝). grace_until은 유예 종료 시각 |
jobs[] | 최근 작업 20건(유형·상태·결과 라벨만) |
비밀번호는 어떤 응답에도 나오지 않습니다. 연장·자동 연장 설정·비밀번호 변경·SSL 재시도는 대시보드에서 합니다(호스팅 매뉴얼).
호스팅을 신청하고 첫 달 요금(price)을 지갑에서 차감합니다. 대시보드 신청과 같은 검사·결제·승인 절차를 거칩니다.
| 필드 | 형식 | 설명 |
|---|---|---|
plan_id | 정수, 필수 | /hosting/plans의 id |
username | 문자열, 필수 | 영문 소문자로 시작, 영문 소문자·숫자 3~16자. 예약어 불가, 변경 불가 |
domain | 문자열, 필수 | example.com 형식(스킴·경로 없이). 다른 계정·리셀러가 쓰는 도메인과 그 상·하위 도메인 불가 |
with_www | 불리언, 선택 | www.도 함께 서비스. 대표 도메인(예: example.com, example.co.kr)만 |
password | 문자열, 필수 | 공백 없는 ASCII 10~64자, 영문+숫자 포함, 아이디 포함 불가. 리눅스·JupyterLab·DB 공통이며 응답·로그에 남지 않습니다 |
client_token | 문자열, 필수 | 중복 방지 토큰 |
POST /hosting/accounts
{ "plan_id": 2, "username": "shop1042", "domain": "shop1042.example.com", "with_www": false,
"password": "S3cure-Example-Pass", "client_token": "host-order-1042-2f9a1c" }
HTTP/1.1 201 Created
{ "ok": true, "data": {
"account": { "id": 57, "plan_id": 2, "plan_code": "basic", "username": "shop1042",
"domain": "shop1042.example.com", "status": "pending_approval", "status_label": "승인 대기",
"monthly_price": 19000, "auto_renew": true, "paid_until": null, "…": "…" },
"price": 19000,
"balance_after": 1186000,
"replayed": false
} } - 새 계정은 보통
pending_approval(관리자 승인 후 생성)로 시작합니다.GET /hosting/accounts/{id}로active가 될 때까지 몇 분 간격으로 확인하세요. 반려되면rejected가 되고 첫 달 요금이 지갑으로 돌아옵니다. - 입력 오류는
422 validation과fields(plan_id·username·domain·password), 잔액 부족은422 insufficient_balance, 신청 중단은503 unavailable입니다. - 같은
client_token을 같은 요금제·아이디·도메인·www로 다시 보내면 원래 계정(200,"replayed": true)을 돌려줍니다. 비밀번호는 비교하지 않습니다. - 도메인 DNS에 A 레코드
58.121.85.179를 미리 추가해 두면 생성 직후 SSL이 발급됩니다(DNS·SSL).
화이트라벨 고객
{ "ok": true, "data": { "items": [
{ "id": 77, "email_masked": "bu****@example.com", "name": "김고객", "balance": 40,
"status": "active", "last_login_at": "2026-10-01 18:22:40", "created_at": "2026-09-28 09:00:12" }
], "page": 1, "per_page": 50, "total": 1 } } balance는 고객의 스토어 크레딧, status는 active(이용 중) 또는 suspended(정지)입니다. 고객 등록·비밀번호·정지는 대시보드에서 합니다.
내 스토어 고객의 크레딧을 지급(양수)하거나 차감(음수)합니다. 대시보드의 "크레딧 지급·차감"과 같은 처리이며, 지갑에서는 아무것도 빠지지 않습니다(생성 비용은 고객이 실제로 만들 때 연결한 솜씨 계정에서 나갑니다).
| 필드 | 형식 | 설명 |
|---|---|---|
amount | 정수, 필수 | 0이 아닌 ±1,000,000 이내. 음수 = 차감(고객 보유량까지만) |
client_token | 문자열, 필수 | 중복 방지 토큰 |
memo | 문자열, 선택 | 300자까지 저장. 고객 크레딧 내역에 보입니다 |
POST /wl/customers/77/credits
{ "amount": 100, "client_token": "wl-77-topup-20261002-01", "memo": "10월 이용권" }
HTTP/1.1 201 Created
{ "ok": true, "data": {
"entry": { "id": 4410, "customer_id": 77, "delta": 100, "balance_after": 140,
"memo": "10월 이용권", "created_at": "2026-10-02 10:30:00" },
"customer": { "id": 77, "balance": 140 },
"replayed": false
} } - 다른 리셀러의 고객 번호는
404, 보유량보다 많이 차감하면422 insufficient_balance입니다. - 같은
client_token재전송은200,"replayed": true와 원래 기록, 그리고 고객의 현재 잔액을 돌려줍니다.
예제(curl · PHP)
curl
# 키는 환경 변수에 둔다(예시 값)
export RS_KEY="rsk_live_0123456789abcdef0123456789abcdef01234567"
BASE="https://reseller.somssii.com/api/v1"
# 연동 점검
curl -s "$BASE/me" -H "Authorization: Bearer $RS_KEY"
# 잔액 · 출금 가능액
curl -s "$BASE/wallet" -H "Authorization: Bearer $RS_KEY"
# 이번 달 크레딧 지급 원장
curl -s "$BASE/wallet/ledger?kind=credit_transfer&from=2026-10-01&to=2026-10-31&per_page=100" \
-H "Authorization: Bearer $RS_KEY"
# 지급 전 회원 확인 (@ 는 %40 으로)
curl -s "$BASE/credits/lookup?email=customer%40example.com" -H "Authorization: Bearer $RS_KEY"
# 크레딧 지급 — client_token 은 주문 기록에 먼저 저장하고, 재시도에도 같은 값을 쓴다
TOKEN=$(openssl rand -hex 16)
curl -s -X POST "$BASE/credits/transfers" \
-H "Authorization: Bearer $RS_KEY" -H "Content-Type: application/json" \
-d "{\"email\":\"customer@example.com\",\"credits\":500,\"client_token\":\"$TOKEN\",\"memo\":\"주문 #1042\"}"
# 요금제 확인 후 호스팅 신청
curl -s "$BASE/hosting/plans" -H "Authorization: Bearer $RS_KEY"
curl -s -X POST "$BASE/hosting/accounts" \
-H "Authorization: Bearer $RS_KEY" -H "Content-Type: application/json" \
-d '{"plan_id":2,"username":"shop1042","domain":"shop1042.example.com","password":"S3cure-Example-Pass","client_token":"host-order-1042-2f9a1c"}'
# 생성 완료 확인
curl -s "$BASE/hosting/accounts/57" -H "Authorization: Bearer $RS_KEY"
# 화이트라벨 고객에게 100 크레딧
curl -s -X POST "$BASE/wl/customers/77/credits" \
-H "Authorization: Bearer $RS_KEY" -H "Content-Type: application/json" \
-d '{"amount":100,"client_token":"wl-77-topup-20261002-01","memo":"10월 이용권"}'
PHP — 재시도에 안전한 작은 클라이언트
<?php
// 키는 코드에 적지 말고 환경 변수·비밀 저장소에서 읽는다.
final class ResellerApiError extends RuntimeException
{
public function __construct(public readonly int $status, public readonly string $errorCode,
string $message, public readonly array $fields = [])
{
parent::__construct($message);
}
}
final class ResellerApi
{
public function __construct(private string $key, private string $base = "https://reseller.somssii.com/api/v1") {}
public function get(string $path, array $query = []): array
{
return $this->call("GET", $path . ($query !== [] ? "?" . http_build_query($query) : ""));
}
/** 쓰기 — $body 에 client_token 을 넣는다. 같은 토큰이면 몇 번 보내도 한 번만 처리된다. */
public function post(string $path, array $body): array
{
return $this->call("POST", $path, $body);
}
private function call(string $method, string $path, ?array $body = null): array
{
for ($try = 1; ; $try++) {
$ch = curl_init($this->base . $path);
$headers = ["Authorization: Bearer " . $this->key, "Accept: application/json"];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = is_string($raw) ? json_decode($raw, true) : null;
// 네트워크 오류·5xx: 같은 요청(같은 client_token)으로 다시 — 중복 처리되지 않는다
if (($raw === false || $status >= 500) && $try < 3) {
sleep(2 * $try);
continue;
}
// 요청 제한: 알려 준 시간만큼 기다렸다가 다시
if ($status === 429 && $try < 3) {
sleep(max(1, (int) ($json["error"]["retry_after"] ?? 5)));
continue;
}
if (!is_array($json)) {
throw new ResellerApiError($status, "transport", "응답을 해석할 수 없습니다(HTTP {$status}).");
}
if (($json["ok"] ?? false) !== true) {
$e = $json["error"] ?? [];
throw new ResellerApiError($status, (string) ($e["code"] ?? "error"), (string) ($e["message"] ?? ""), (array) ($e["fields"] ?? []));
}
return $json["data"];
}
}
}
// ── 사용 예: 주문 1042 에 대해 솜씨 크레딧 500 지급 ──
$api = new ResellerApi((string) getenv("RS_API_KEY"));
$token = bin2hex(random_bytes(16)); // 새 요청마다 새 값
// saveOrderToken(1042, $token); // 보내기 전에 내 주문 기록에 저장 → 재시도·재실행 때 같은 값을 쓴다
try {
$check = $api->get("/credits/lookup", ["email" => "customer@example.com"]);
if (!$check["eligible"]) {
exit("지급할 수 없는 회원입니다: " . $check["message"] . "\n");
}
$res = $api->post("/credits/transfers", [
"email" => "customer@example.com",
"credits" => 500,
"client_token" => $token,
"memo" => "주문 #1042",
]);
printf("지급 #%d, 차감 %d원, 잔액 %s원%s\n",
$res["transfer"]["id"], $res["transfer"]["amount"], $res["balance_after"] ?? "-",
$res["replayed"] ? " (이미 처리된 요청)" : "");
} catch (ResellerApiError $e) {
// insufficient_balance → 지갑 충전 안내, validation → $e->fields 표시, conflict → 새 토큰으로 새 요청
fwrite(STDERR, "[{$e->status} {$e->errorCode}] {$e->getMessage()}\n");
} 보안 수칙
- 키는 서버에서만 쓰세요. 브라우저·모바일 앱·공개 저장소(Git)에 넣으면 누구나 내 지갑으로 지급·신청할 수 있습니다.
- 용도별로 키를 나누고 필요한 권한만 주세요(조회 전용 서버에는
read만). - 키를 이메일·메신저로 주고받지 마세요. 노출이 의심되면 바로 폐기하고 새로 만드세요. 대시보드의 마지막 사용 시각으로 이상 사용을 확인할 수 있습니다.
- 호스팅 비밀번호는 HTTPS로만 보내고, 내 시스템 로그에도 남기지 마세요.
- 쓰기 요청의
client_token을 주문과 함께 저장해 두면, 장애 뒤 다시 실행해도 이중 지급·이중 신청이 생기지 않습니다.