개발자/연동

휴대폰 충전

휴대폰 충전은 선불 휴대폰 번호에 요금을 바로 충전하는 상품입니다. 고객의 회선에 통화 요금이나 데이터가 충전되며, 따로 전달할 코드는 없습니다. 다른 주문과 마찬가지로 CardV 지갑에서 결제됩니다.

관련 문서: 인증 · 공통 규칙 · Webhook

#전체 흐름

Text
GET  /recharge/countries            충전할 수 있는 국가
GET  /recharge/operators?country=US  통신사, 충전 유형, 금액
POST /recharge/quote                귀사 가격과 300초 동안 유효한 quote_token
POST /recharge/orders               주문(지갑에서 결제)
GET  /recharge/orders/{order_id}    상태 조회 또는 Webhook 대기
  • 모든 경로는 /api/v1로 시작합니다. 요청 헤더는 다른 요청과 똑같이 보냅니다.
  • 두 POST 요청에는 서명이 필요합니다. POST/orders와 같은 방법으로 서명하되, 각 요청의 경로를 사용합니다. 예: /api/v1/recharge/quote
  • 충전 주문은 기프트카드 주문과 별개입니다. 조회할 때는 /recharge 엔드포인트를 사용하세요.
  • 직접 충전만 제공합니다. PIN 상품(고객이 코드를 직접 입력하는 상품)은 제공하지 않습니다.

#국가

GET/api/v1/recharge/countries는 지금 충전할 수 있는 국가 목록을 반환합니다.

JSON
{
  "count": 2,
  "results": [
    {"code": "MX", "name": "Mexico", "currency_codes": ["MXN"], "operator_count": 4, "offer_count": 37},
    {"code": "US", "name": "United States", "currency_codes": ["USD"], "operator_count": 6, "offer_count": 52}
  ]
}
  • code는 ISO 3166-1 alpha-2 국가 코드입니다. 이후 요청에서 country로 보냅니다.
  • currency_codes는 이 국가의 통신사들이 판매에 쓰는 현지 통화입니다.
  • 통신사가 추가되거나 이용할 수 없게 되면 목록이 바뀝니다. 몇 시간마다 다시 불러오세요.

#통신사

GET/api/v1/recharge/operators?country=US는 한 국가의 통신사 목록을 반환합니다. search=att를 추가하면 통신사 이름으로 걸러 낼 수 있습니다.

JSON
{
  "count": 1,
  "results": [
    {
      "operator_key": "us-att",
      "name": "AT&T",
      "country": "US",
      "country_name": "United States",
      "logo_url": "https://b2b.cardv.net/api/v1/catalog-assets/3f5c...a1.png",
      "subtypes": ["airtime", "data"],
      "amount_model": "range",
      "currency_codes": ["USD"],
      "amounts": [
        {"min": "5.0000", "max": "100.0000", "currency": "USD", "subtype": "airtime", "label": "5.0000-100.0000 USD"},
        {"min": "15.0000", "max": "15.0000", "currency": "USD", "subtype": "data", "label": "15.0000 USD"}
      ],
      "offer_count": 3
    }
  ]
}
필드설명
operator_key가격 조회와 주문에 보내는 ID입니다. 예: us-att. 문자열로 저장하세요.
subtypes구매할 수 있는 유형입니다. airtime(통화 요금), data, bundle(통화와 데이터) 중 하나입니다.
amount_model모든 금액이 정해진 값이면 fixed, 범위 금액이 하나라도 있으면 range입니다.
amounts[]선택할 수 있는 금액입니다. min과 max가 같으면 고정 금액이고, 다르면 그 사이의 어떤 금액이든 됩니다.
amounts[].currency해당 금액의 현지 통화입니다. local_currency로 보냅니다.
logo_urlCardV가 호스팅하는 이미지이거나 ""입니다.
  • 금액은 현지 금액입니다. 즉 휴대폰 회선에 충전되는 금액이며, 현지 통화로 표시됩니다.
  • country가 없는 국가이거나 형식이 잘못되면 HTTP 400이 반환됩니다.

#가격 조회

POST/api/v1/recharge/quote는 충전 1건에 대한 귀사 가격을 반환합니다. 이 요청에는 서명이 필요합니다.

JSON
{
  "country": "US",
  "operator_key": "us-att",
  "amount": "10.00",
  "local_currency": "USD",
  "subtype": "airtime"
}
필드필수 여부의미
country필수국가 목록의 국가 코드입니다.
operator_key필수통신사 목록에서 가져온 값입니다.
amount필수현지 금액(문자열)입니다. 고정 금액이면 목록에 있는 값 중 하나, 범위 금액이면 min과 max 사이 값입니다.
local_currency권장amount의 ISO 4217 통화 코드로, amounts[].currency에서 가져옵니다. 통신사가 통화를 둘 이상 제공할 때는 꼭 보내세요.
subtype선택airtime(기본값), data, bundle 중 하나입니다.

응답:

JSON
{
  "country": "US",
  "country_name": "United States",
  "operator": {"operator_key": "us-att", "name": "AT&T", "logo_url": ""},
  "subtype": "airtime",
  "local_amount": "10.0000",
  "local_currency": "USD",
  "merchant_price": "9.6200",
  "merchant_currency": "USD",
  "expires_at": "2026-09-30T08:20:30.123456+00:00",
  "quote_token": "eyJ2ZXJzaW9uIjox...:1uXyZa:8c1f..."
}
  • merchant_price는 지갑에서 결제될 금액이며, 통화는 merchant_currency입니다.
  • quote_token은 이 가격을 expires_at까지 300초 동안 고정합니다. 주문할 때 그대로 보내세요.
  • 이 토큰은 귀사 계정과 이번 국가, 통신사, 유형, 금액에 묶여 있습니다.
  • 주문하기 전에 local_currency가 예상한 통화인지 확인하세요.
  • 가격 조회는 금액을 미리 잡아 두지 않습니다. 언제든 가격을 다시 조회할 수 있습니다.

#충전 주문하기

POST/api/v1/recharge/orders는 휴대폰을 충전하고 지갑에서 결제합니다. 이 요청에는 서명이 필요합니다.

JSON
{
  "external_order_id": "SHOP-RC-20260930-0001",
  "country": "US",
  "operator_key": "us-att",
  "amount": "10.00",
  "local_currency": "USD",
  "subtype": "airtime",
  "account": "12125550100",
  "quote_token": "eyJ2ZXJzaW9uIjox...:1uXyZa:8c1f..."
}
필드필수 여부의미
external_order_id필수자체 주문번호입니다. 기프트카드 주문을 포함한 모든 주문에서 중복될 수 없습니다.
country, operator_key, amount, local_currency, subtype필수가격 조회 때 보낸 값과 같은 값입니다.
account필수충전할 휴대폰 번호입니다. 국가 번호를 포함한 숫자만 쓰고, +나 공백은 넣지 않습니다.
quote_token필수가격 조회에서 받은 값으로, 만료되기 전에 보냅니다.

휴대폰 번호 예: 12125550100(미국), 525512345678(멕시코). 번호가 선택한 통신사의 번호인지 확인하세요. 잘못된 번호로 보낸 충전은 되돌릴 수 없습니다.

CardV는 가격 조회 결과를 확인하고, 지갑에서 merchant_price를 바로 결제한 뒤, 백그라운드에서 충전을 시작합니다. 새 주문이면 HTTP 201이 반환됩니다.

JSON
{
  "idempotent_replay": false,
  "order": {
    "order_id": "O-00005678",
    "external_order_id": "SHOP-RC-20260930-0001",
    "status": "accepted",
    "status_title": "Recharge accepted",
    "poll_after_seconds": 12,
    "account": "12***00",
    "local_amount": "10.0000",
    "local_currency": "USD",
    "merchant_price": "9.6200",
    "merchant_currency": "USD",
    "...": "more fields"
  }
}
  • order.order_id를 저장하세요.
  • 휴대폰 번호는 마스킹되어 반환되며, 전체 번호는 절대 반환되지 않습니다.

#안전한 재시도

external_order_id가 있으면 같은 번호가 두 번 충전되는 일을 막을 수 있습니다.

보낸 요청결과
새 주문번호HTTP 201. 새 주문이 생성되고 지갑에서 결제됩니다.
같은 주문번호, 같은 충전 내용HTTP 200과 "idempotent_replay": true. 기존 주문이 반환되며 결제되지 않습니다.
같은 주문번호, 다른 충전 내용external_order_id에 대한 HTTP 400. 아무 일도 일어나지 않습니다.

"같은 충전 내용"이란 국가, 통신사, 유형, 금액, 휴대폰 번호가 모두 같다는 뜻입니다. 중복 주문 여부는 가격 조회 결과를 확인하기 전에 판단하므로, quote_token이 만료되었더라도 처음 주문이 반환됩니다.

  • 타임아웃, 5xx, 연결 끊김이 발생하면 같은 주문번호로 같은 본문을 다시 보내세요. 타임스탬프와 nonce는 새로 만들어 다시 서명합니다.
  • 응답을 받지 못했다고 새 주문번호를 만들면 안 됩니다. 같은 휴대폰이 두 번 충전될 수 있습니다.

#충전 주문 조회

GET/api/v1/recharge/orders/{order_id}는 주문 1건을 반환합니다. CardV 주문 ID(O-00005678)를 사용할 수 있습니다.

JSON
{
  "order_id": "O-00005678",
  "external_order_id": "SHOP-RC-20260930-0001",
  "status": "processing",
  "order_status": "processing",
  "status_title": "Recharge processing",
  "status_message": "The recharge request is being processed. Delivery is not confirmed yet.",
  "next_step": "Keep this order open and wait for confirmation before placing another recharge.",
  "poll_after_seconds": 12,
  "country": "US",
  "operator": {"operator_key": "us-att", "name": "AT&T", "logo_url": ""},
  "subtype": "airtime",
  "account": "12***00",
  "local_amount": "10.0000",
  "local_currency": "USD",
  "merchant_price": "9.6200",
  "merchant_currency": "USD",
  "attempts": [{"attempt_no": 1, "status": "processing", "error_message": "", "submitted_at": "2026-09-30T08:16:02.511201Z", "completed_at": null}],
  "created_at": "2026-09-30T08:16:01.004211Z",
  "updated_at": "2026-09-30T08:16:02.611978Z",
  "...": "more fields"
}
  • 존재하지 않는 주문 ID이거나 다른 계정의 주문이면 HTTP 404가 반환됩니다.
  • status_title, status_message, next_step은 영어 문구이며, 귀사 직원에게 그대로 보여 줄 수 있습니다.
  • poll_after_seconds는 다음 조회까지 기다릴 시간입니다. 0이면 주문이 완료된 것입니다.

#충전 주문 목록

GET/api/v1/recharge/orders는 귀사의 충전 주문을 최신순으로 반환합니다.

HTTP
GET /api/v1/recharge/orders?status=processing&limit=50&offset=0
JSON
{"count": 3, "limit": 50, "results": ["..."]}
  • 필터: status, search(CardV 주문 ID, 자체 주문번호 또는 통신사 이름).
  • limit 기본값은 20, 최대값은 100입니다. 100보다 크면 100으로 처리됩니다.
  • limit이나 offset이 음수이거나 숫자가 아니면 HTTP 400이 반환됩니다.

#상태

Text
accepted ──► processing ──► succeeded
                  │
                  ├──► manual_review ──► succeeded 또는 refunded
                  │
                  └──► failed ──► refunded   (지갑으로 환불됨)
상태완료 여부할 일
accepted아니요기다립니다. 지갑 결제는 끝났고 충전은 아직 시작되지 않았습니다.
processing아니요기다립니다. 몇 분 걸릴 수 있습니다. 같은 주문을 다시 넣지 마세요.
manual_review아니요CardV가 통신사와 결과를 확인하고 있습니다. 기다립니다.
succeeded예휴대폰이 충전되었습니다. 고객에게 알려 주세요.
failed아직 아님충전이 이루어지지 않았습니다. refunded가 될 때까지 기다립니다.
refunded예대금이 지갑으로 돌아왔습니다. 새 주문을 넣어도 됩니다.
  • poll_after_seconds만큼 기다린 뒤 조회하고, 이후에는 간격을 늘려 30초, 60초, 그다음부터는 5분마다 조회하세요. 요청 한도를 넘지 않도록 주의하세요.
  • 모르는 상태는 "아직 완료되지 않음"으로 처리하세요.
  • 주문이 완료되지 않은 동안에는 같은 번호에 새 주문번호로 다른 충전을 보내지 마세요. 첫 주문도 성공하면 같은 휴대폰이 두 번 충전됩니다.

#Webhook과 환불

충전 주문도 다른 주문과 같은 Webhook을 보냅니다. order.succeeded, order.failed, order.refunded입니다. Webhook에는 CardV 주문 ID와 자체 주문번호가 들어 있고, items 목록은 비어 있습니다. Webhook을 받은 뒤 GET/api/v1/recharge/orders/{order_id}로 주문을 조회하세요.

환불은 자동으로 처리됩니다. 통신사가 실패를 확인하면 CardV가 merchant_price 전액을 지갑으로 돌려주고, 주문은 refunded가 됩니다. 환불 내역은 포털의 거래 내역 페이지에서 확인할 수 있습니다. 성공한 충전은 환불하거나 취소할 수 없습니다.

#오류

오류 형식은 공통 규칙을 따릅니다. 거절된 가격 조회나 주문은 HTTP 400을 반환하며, 결제되지 않습니다.

키발생 위치해결 방법
detail가격 조회, 주문국가, 통신사, 유형 또는 금액을 이용할 수 없습니다. 통신사 목록을 확인하세요.
amount가격 조회, 주문숫자가 아니거나, 0이거나, 범위를 벗어났습니다.
local_currency가격 조회, 주문3자리 ISO 4217 코드가 아닙니다.
account주문휴대폰 번호가 없습니다.
quote_token주문없거나, 만료되었거나, 변경되었거나, 내용과 맞지 않습니다. code를 확인한 뒤 가격을 다시 조회하세요.
balance주문포털에서 지갑을 충전하세요.
risk주문주문 한도에 걸렸습니다. CardV에 문의하세요.
external_order_id주문다른 주문에 이미 쓰인 번호입니다. 안전한 재시도를 참고하세요.
wallet주문활성화된 지갑이 없습니다. CardV에 문의하세요.

quote_token 오류에는 code가 함께 반환됩니다.

JSON
{"code": "quote_expired", "quote_token": "This quote expired. Refresh the price and confirm again."}
code의미
quote_requiredquote_token을 보내지 않았습니다.
quote_expired300초가 지났습니다. 가격을 다시 조회하세요.
quote_invalid변경되었거나 다른 충전 내용의 토큰입니다. 가격을 다시 조회하세요.
price_changed가격 조회 후 귀사 가격이 바뀌었습니다. 다시 조회하고 새 가격을 확인하세요.

HTTP 403은 인증 정보, 서명, IP 또는 승인 문제입니다. 인증을 참고하세요.

연동 관련 문의는 Merchant ID와 주문 ID 또는 요청 ID를 포함해 [email protected]로 보내 주세요.