요기요 9.12.0 · 1초결제 API 자동화 흐름

요약

  • 외부 PC 에서 순수 Python + httpx 만으로 1초결제 완료 가능 (등록 카드 보유 시).
  • 전체 흐름은 7 단계 HTTP 호출. 페이로드 암호화 / HMAC 시그니처 / 자체 nonce 없음.
  • KG이니시스 (wpay.inicis.com) PG 는 단순 form chain submit 으로 통과 — JS 실행 / device fingerprint 불필요.
  • 핵심 결제 진입점: POST api.yogiyo.co.kr/order/cart/submit/food/{customer_id}/{lat}/{lng}

아키텍처 — 호스트별 역할

결제 흐름은 5개의 yogiyo 도메인 + 1개의 KG이니시스 PG 도메인을 거칩니다.

authyo.yogiyo.co.kr
JWT 발급 / 갱신 (access_token = RS256, refresh_token = HS512)
api.yogiyo.co.kr/cart
카트 생성·조회 (/v3/food/...)
api.yogiyo.co.kr/order
결제 시작 endpoint · 주문 조회 · 취소
payo.yogiyo.co.kr
결제 진입 페이지 · 등록 카드 토큰 조회 (/v2/payments/card-info)
wpay.inicis.com
KG이니시스 PG · 1초결제 인증 (/ygypay/u/v2/payreqauth)
orderyo.yogiyo.co.kr
결제 결과 callback · 최종 order_number 확정

시퀀스 다이어그램

sequenceDiagram participant C as Python Client participant A as authyo participant API as api/cart api/order participant P as payo participant W as wpay.inicis.com participant O as orderyo C->>A: POST /api/v1/auth/refresh A-->>C: { access_token, refresh_token } C->>API: POST /cart/v3/food/.../vd/ API-->>C: { cart_uuid, cart_signature } C->>API: POST /order/cart/submit/food/{cust}/{lat}/{lng} API-->>C: { payment_no, order_number, payment_url } Note over C: payment_no 발급, 주문 가예약 C->>P: GET {payment_url} P-->>C: HTML Form A (mid, wpayUserKey, wpayToken, oid, signature, ...) C->>W: POST /ygypay/u/v2/payreqauth (Form A) W-->>C: HTML Form B (wtid, wdata, ukey, uskey, ...) C->>W: POST /ygypay/u/payreqauthAction (Form B) W-->>P: redirect /certify?wpayToken=...&signature=... P-->>O: redirect /callback/payment/result/?result_code=success O-->>C: HTML { order_number, purchase_id, ygy_order_id } Note over C: 결제 완료, 매장 주문 활성화 C->>API: GET /order/api/v2/orders/{order_number}/ API-->>C: 주문 상세 (transmission_status: ACCEPTED)

API 단계 (1~4)

1 토큰 갱신 — refresh access_token POST
POST authyo.yogiyo.co.kr/api/v1/auth/refresh?customer_id={customer_id}

Headers (필수)

헤더
AuthorizationBearer <access_token> (만료된 것도 OK — 형식만 유효하면 통과)
Content-Typeapplication/json; charset=utf-8
X-ApiKeyiphoneap (하드코딩)
X-ApiSecretfe5183cc3dea12bd0ce299cf110a75a2 (하드코딩)
X-YGY-*OS, APP-VERSION, DEVICE-MODEL, DEVICE-ID, SESSION-ID, LOCALE 등 표준 헤더 셋

Request body

{ "refresh_token": "eyJhbGciOiJIUzUxMiI..." }

Response

{
  "access_token":  "eyJhbGciOiJSUzI1NiI...",  // RS256 JWT, exp = now + 2hr
  "refresh_token": "eyJhbGciOiJIUzUxMiI..."   // HS512 JWT, exp = now + 60일, rotating
}
Note 응답 body 에 새로 발급된 refresh_token 도 있으니, 매 호출 후 저장해야 합니다 (rotating token).
JWT 구조 access_token payload: {iat, exp, platform:"YGY", role:"user", sub_id, base_url, user_id}. 이게 WebView 의 authyo-token 으로도 그대로 사용됨.
2 등록 카드 토큰 조회 GET
GET payo.yogiyo.co.kr/v2/payments/card-info

Response

{
  "registered": true,
  "cards": [
    {
      "code":   "06",                                     // 카드사 코드 → payo_service 의 YGYPAY_06
      "name":   "국민카드",
      "number": "<REDACTED>",                                  // 카드 PAN — 마스킹
      "token":  "<REDACTED>",                                  // ★ ygypay_info.token — 마스킹
      "no_interests": [{ "min_amount": 50000, "month": [2, 3] }]
    }
  ],
  "result_code":    "success",
  "result_message": "정상 처리되었습니다."
}

카드사 코드 → payo_service 매핑

코드카드사payo_service 값
01하나(외환)YGYPAY_01
02우리YGYPAY_02
03롯데YGYPAY_03
04현대YGYPAY_04
06국민YGYPAY_06
11BCYGYPAY_11
12삼성YGYPAY_12
14신한YGYPAY_14
16NH농협YGYPAY_16
17하나YGYPAY_17
중요 카드의 token 값은 사용자가 앱에서 한 번 카드 등록 + 1회 결제를 완료해야 발급됩니다. 신규 카드 등록 자체는 PG 인증창을 거쳐야 하므로 100% 자동화 불가.
3 카트 생성 POST
POST api.yogiyo.co.kr/cart/v3/food/customers/{customer_id}/restaurants/{shop_id}/vd/

Request body (예시)

{
  "lat": 35.2085501,
  "lng": 126.83933026,
  "yotimedeal_offer_id": null,
  "products": [
    {
      "vendor_product_id":  "<product_id>",
      "vendor_category_id": "<category_id>",
      "quantity": 1,
      "option_sections": [
        {
          "vendor_option_section_id": "<option_section_id>",
          "options": [
            { "vendor_option_id": "<option_id>", "price": 0, "quantity": 1 }
          ]
        }
      ]
    }
  ],
  "subscription_type": null,
  "coupon": null,
  "referral": null
}

Response (핵심만)

{
  "customer_cart": {
    "uuid":      "<uuid-v4>-b24b",                          // ★ cart_uuid (이미 -b24b 포함)
    "signature": "<sha1 hex>",                              // ★ sha1 hex
    "cart": {
      "restaurant_id":  1234567,
      "items_price":    10000,
      "delivery_fee":   2000,
      "final_price":    12000          // ★ 결제 총액
    }
  }
}
메뉴 ID 사전 조회 vendor_product_id / vendor_category_id / option ID 는 GET frontyo.yogiyo.co.kr/v1/aggregation/shops/{shop_id}/menus 응답에서 추출. 필수 옵션이 있으면 (예: HOT/ICE) 반드시 포함해야 함.
4 결제 submit — payment_no 발급 트리거 POST
POST api.yogiyo.co.kr/order/cart/submit/food/{customer_id}/{lat}/{lng}

Body 최상위 필드 (12개)

필드타입설명
customer_idintcustomer.id
customerobject회원 정보 + 주소. receiveInitialDataFromApp 의 customer 그대로
delivery_infoobjectemail, street_number, phone, comment 등 (10 필드)
paymentobject결제 정보 (아래 별도 표)
agreementsobjectefinance_agreement: true, tos: true 필수
serving_typestringvd · od · od_direct · pickup · pre_order_pickup · robot
cart_uuidstringStep 3 응답의 customer_cart.uuid (이미 -b24b 포함)
cart_signaturestringStep 3 응답의 customer_cart.signature
vendorobject{ vendor_id, franchise_id }
configurationobjectfeature_configuration 의 7개 boolean flags (전부 true)
referralstring일반적으로 ""
build_typestring네이티브 앱 빌드 타입, 일반적으로 ""

payment object 상세

필드타입예시값 / 설명
payo_servicestring"YGYPAY_06" (카드사별 매핑, 위 표 참고)
checkout_method_typestring"YGYPAY" (1초결제 고정)
servicestring"ygypay"
user_benefitsobject{ subscription, point, coupons? }
totalint최종 결제 금액. 0이면 zero payment 흐름 (응답에 즉시 purchase_id)
cash_receipt_infoobject/null현금영수증 사용 시 {cash_receipt_use, cash_receipt_type, cash_receipt_number}
ygypay_infoobject{ token: <Step 2 응답>, installment_plan: 0 }

Response (일반 결제 — total > 0)

{
  "payment_no":   "<yyyyMMddHHmmss + 6자리>",       // 서버 발급, 1회용
  "order_number": "F<yyMMddHHmm>J<3alnum>4B",        // 4B = food 카테고리
  "payment_url":  "https://payo.yogiyo.co.kr/v2/payments/<payment_no>/checkout"
}

Response (zero payment — total = 0, 쿠폰 100% 할인 등)

{
  "purchase_id":  <int>,                            // ★ PG 단계 건너뛰고 즉시 발급
  "order_number": "F<yyMMddHHmm>J<3alnum>4B",
  "ygy_order_id": <int>
}
분기 처리 응답에 purchase_id 가 있으면 결제 완료 (zero payment). 없으면 payment_url 으로 가서 PG chain 을 따라야 함. Next.js bundle 의 useMutation onSuccess 핸들러에서 이 분기 확인.

5. PG (KG이니시스) chain — 핵심

Step 4 응답에 purchase_id 가 없으면 (=결제 총액 > 0), payment_url 로부터 시작하는 3단계 form chain 을 따라야 결제가 실제로 완료됩니다. 이게 자동화의 가장 어려워 보이는 부분이지만, 의외로 JS 실행 / device fingerprint 없이 단순 form chain submit 만으로 통과합니다.

① payo /checkout

GET

payo.yogiyo.co.kr/v2/payments/{payment_no}/checkout

→ HTML 응답에 자동 submit 되는 Form A (22 필드)

② wpay payreqauth

POST

wpay.inicis.com/ygypay/u/v2/payreqauth

Form A as urlencoded → HTML 응답에 Form B (6 필드)

③ wpay payreqauthAction

POST

wpay.inicis.com/ygypay/u/payreqauthAction

Form B as urlencoded → HTTP redirect chain (payo certify → orderyo callback)

④ orderyo callback

자동 redirect 도착

orderyo.yogiyo.co.kr/callback/payment/result/

→ HTML 안의 get_order_result() JS 함수에서 최종 ID 추출

검증 완료 httpx 의 follow_redirects=True 옵션으로 ③→④ 의 HTTP redirect chain 이 자동 처리됨. form chain 만 직접 submit 하면 됨.
5-1 payo /checkout — Form A 받기 GET
GET payo.yogiyo.co.kr/v2/payments/{payment_no}/checkout

응답 HTML (2,220 bytes, 핵심)

<!DOCTYPE html>
<html lang="en"><head>
  <title>YGYpay register</title>
</head>
<body onload="document.getElementById('ygypay_form').submit()">
  <form id="ygypay_form" method="post" action="https://wpay.inicis.com/ygypay/u/v2/payreqauth">
    <input type="hidden" name="mid"          value="yogiyopay1">
    <input type="hidden" name="wpayUserKey"  value="<REDACTED>">
    <input type="hidden" name="wpayToken"    value="<REDACTED>">
    <input type="hidden" name="oid"          value="<payment_no>">
    <input type="hidden" name="goodsName"    value="<url-encoded 상품명>">
    <input type="hidden" name="goodsPrice"   value="<총 결제액>">
    <input type="hidden" name="signature"    value="<sha256 hex>">
    <!-- ... 14 more fields ... -->
  </form>
</body></html>

Form A 전체 필드 (22개)

필드값 / 설명
midyogiyopay1 · 가맹점 ID (KG이니시스 등록)
wpayUserKey등록 사용자 키 (base64). 사용자가 카드 등록 시 PG가 발급
wpayToken등록 카드 토큰 (Step 2 응답의 cards[].token 과 동일)
ci일반적으로 빈 문자열
payMethod빈 문자열 (등록 카드 사용 시)
bankCardCode빈 문자열 (등록 카드 코드는 wpayToken 에 내포)
oidorder ID = Step 4 의 payment_no
goodsNameURL-encoded 상품명 (예: "<첫 상품명> 외 N건")
goodsPrice결제 총액 (원)
buyerName고정 요기요
buyerTel구매자 전화번호
buyerEmail구매자 이메일
cardQuota00 = 일시불
cardInterest무이자 할부 정보 (빈 문자열 가능)
couponCode빈 문자열 (yogiyo 쿠폰은 별도 처리)
flagPinN · PIN 검증 안 함
flagPinMsg빈 문자열
returnUrlURL-encoded payo certify URL (콜백 시 redirect 대상)
signaturesha256 hex · 위 필드들의 HMAC. payo 백엔드가 발급
cshRecpSave / cshRecpCode / cshRecpInfo현금영수증 관련 (보통 빈 문자열)
중요 signature 는 payo 서버가 만들어주는 값. 클라이언트가 위조할 수 없음. 즉 Step 4 → Step 5-1 의 흐름을 거쳐야만 valid 한 signature 를 받을 수 있음.
5-2 wpay payreqauth — Form A → Form B POST
POST wpay.inicis.com/ygypay/u/v2/payreqauth

Request

Headers:
  Content-Type: application/x-www-form-urlencoded
  Referer:      https://payo.yogiyo.co.kr/v2/payments/{payment_no}/checkout
  Origin:       https://payo.yogiyo.co.kr
  User-Agent:   (Android Chrome)

Body (urlencoded — Form A 의 22 필드 그대로):
  mid=yogiyopay1&wpayUserKey=...&wpayToken=...&oid=<payment_no>&...

응답 HTML (2,487 bytes) — Form B

<!DOCTYPE html>
<html lang="ko"><head>
  <title>요기서 1초결제</title>
  <script src="https://cdn-wpay.inicis.com/wpay/js/jquery/jquery-1.11.3.min.js"></script>
</head>
<body>
  <form id="form0" name="form0" action="/ygypay/u/payreqauthAction" method="post">
    <input type="hidden" name="wtid"      value="0+dM3w5P4rJob4LJUJR830IR+j20sbPxohlpLAFgoyM=">
    <input type="hidden" name="mid"       value="yogiyopay1">
    <input type="hidden" name="wdata"     value="..">
    <input type="hidden" name="serviceno" value="..">
    <input type="hidden" name="ukey"      value="..">
    <input type="hidden" name="uskey"     value="..">
  </form>
</body></html>

Form B 필드 (6개)

필드설명
wtidPG transaction ID (서버 발급). 패턴: NWYGY{yyMMdd}1158{nnnnn} 또는 base64 형태
midyogiyopay1 (Form A 와 동일)
wdataPG 내부 검증 데이터
serviceno서비스 번호
ukey사용자 키
uskey사용자 보조 키
관찰 응답에 jQuery 스크립트가 로드되지만, JS 실행 없이도 form 만 추출해서 다음 단계로 보내면 됨. JS 는 단순히 form0.submit() 자동 호출 용도.
5-3 wpay payreqauthAction — 결제 실행 + redirect chain POST
POST wpay.inicis.com/ygypay/u/payreqauthAction

Request

Headers:
  Content-Type: application/x-www-form-urlencoded
  Referer:      https://wpay.inicis.com/ygypay/u/v2/payreqauth

Body (urlencoded — Form B 의 6 필드):
  wtid=0%2BdM3w5P4r...&mid=yogiyopay1&wdata=...&serviceno=...&ukey=...&uskey=...

응답 — HTTP 302 redirect chain (자동)

// 1단계 redirect: PG 처리 결과를 payo 로 전달
HTTP 302 → Location: https://payo.yogiyo.co.kr/v2/payments/{payment_no}/certify?
  resultCode=0000
  &resultMsg=정상 처리되었습니다.
  &wtid=<PG transaction id>
  &wpayUserKey=<REDACTED>
  &wpayToken=<REDACTED>
  &wpayDomain=kswpay.inicis.com
  &signature=<sha256 hex>

// 2단계 redirect: payo 가 yogiyo 백엔드 확인 후 orderyo callback 으로
HTTP 302 → Location: https://orderyo.yogiyo.co.kr/callback/payment/result/?
  payment_no={payment_no}
  &merchant_payment_no={order_number}
  &result_code=success
  &result_message=정상 처리되었습니다.
httpx 동작 follow_redirects=True 면 이 두 redirect 가 자동 처리됨. 우리는 마지막 orderyo HTML 만 받음.

최종 응답 HTML — orderyo

<html><head>
  <title>결제처리 완료</title>
  <script>bazadebezolkohpepadr="937942603"</script>
  <script src="https://orderyo.yogiyo.co.kr/akam/13/37e7dcd8" defer></script> // Akamai 봇감지
</head>
<body><script>
function get_order_result() {
  return {
    "order_number": "F<yyMMddHHmm>J<3alnum>4B",
    "purchase_id":  <int>,                       // ★ 결제 완료 확정
    "ygy_order_id": <int>
  };
}
</script></body></html>

이 시점에 결제 완료. purchase_idygy_order_id 가 발급된다.

5-4 orderyo callback — 최종 ID 추출 GET

이 단계는 ③의 redirect 의 일부로 자동 도달. 클라이언트가 별도 호출하지 않음. HTML 에서 정규식으로 ID 추출:

import re
m_order    = re.search(r'"order_number":\s*"([^"]+)"', html)
m_purchase = re.search(r'"purchase_id":\s*(\d+)',    html)
m_ygyord   = re.search(r'"ygy_order_id":\s*(\d+)',    html)

완료 처리 (6~7)

6 주문 상세 조회 GET
GET api.yogiyo.co.kr/order/api/v2/orders/{order_number}/?subscription_type=

Response (핵심 필드)

{
  "order_number":         "F<yyMMddHHmm>J<3alnum>4B",
  "display_order_number": "F<yyMMdd>-<HH>-<HHmmJ...4B>",
  "submitted_at":         "<ISO-like timestamp>",
  "menu_description":     "<첫 상품명> x <총 수량>",
  "transmission_status":  "ACCEPTED",        // 매장 수락
  "rider_status":         "ACCEPTED_BY_VENDOR", // 라이더 배정 대기
  "restaurant": { "id": <int>, "name": "<매장명>", "phone": "<매장 전화>" },
  "customer":   { "phone": "<구매자 전화>", "safen_number": "<안심번호>" },
  "tracking_info": { "tracking_delay_time": 20, "tracking_polling_interval": 10 }
}
7 주문 취소 (옵션) POST
POST api.yogiyo.co.kr/order/api/v2/orders/{order_number}/customer_cancel/

Body 빈값 (POST 만 트리거). 매장이 수락 (transmission_status: ACCEPTED) 전에만 취소 가능.

참조 — Step 4 body 스키마 전체

Next.js bundle (chunk-489-*.js) 의 getFoodCartSubmitParams 함수 정적 분석으로 확정. 모든 필드 + 출처 + 예시값:

{
  // ─────── 식별자 ───────
  "customer_id": <int>,                                  // customer.id

  // ─────── 회원 정보 (receiveInitialDataFromApp 의 customer 객체) ───────
  "customer": {
    "id": <int>,
    "authyo-token":    "<RS256 JWT>",                // Step 1 의 access_token 그대로
    "location_token":  "<HS256 JWT — 위치 토큰>",
    "lat": <float>, "lng": <float>,
    "address": {
      "street_name": "서울특별시 강남구 역삼동 737-1",
      "road_name":   "서울특별시 강남구 테헤란로 521",        // ★ sido+gugun prefix
      "alias_type": 0, "alias_text": "",
      "street_number": "101동 1001호",
      "display_address_long": "테헤란로 521"
    },
    "address_detail": {
      "sido": "서울특별시",   "gugun": "강남구",
      "admin_dong": "역삼1동", "dong": "역삼동",
      "bunji": "737-1", "road": "테헤란로",
      "building": "521", "ri": ""
    },
    "adiyo_address": { /* law, admin, road, point, location_token */ },
    "isLogin": true,
    "email": "user@example.com",
    "phone": "01012345678",
    "phone_uuid":        "<uuid-v4>",                       // = DEVICE_ID
    "advertisement_id":  "<uuid-v4>",                       // GAID
    "subscription_type": null,
    "limit_ad_tracking": false,
    "app_tracking_transparency": null
  },

  // ─────── 배달/연락처 ───────
  "delivery_info": {
    "email": "...", "street_number": "...", "phone": "...",
    "use_safety_number": true,          // 픽업이면 false
    "extra_comments": [],
    "comment_to_vendor": "", "comment_to_rider": "",
    "is_cutlery_requested": false,       // food restaurant 만
    "is_contactless_delivery": true,
    "expected_takeout_time": ""           // 픽업/예약픽업이면 "YYYY-MM-DD HH:mm"
  },

  // ─────── 결제 (★ 핵심) ───────
  "payment": {
    "payo_service":         "YGYPAY_06",   // 카드사별 매핑
    "checkout_method_type": "YGYPAY",      // 1초결제 고정
    "service":              "ygypay",
    "user_benefits": {
      "subscription": { "discount_amount": 0, "discount_info_id": null, "benefit_code": null },
      "point": 0,
      "coupons": [
        { "code": "<coupon_code>", "amount": <int>, "bank_card_code": "<카드사 코드>" }
      ]
    },
    "total": <int>,
    "cash_receipt_info": null,
    "ygypay_info": {
      "token": "<REDACTED>",
      "installment_plan": 0
    }
  },

  // ─────── 동의 항목 ───────
  "agreements": {
    "email_accept": false, "sms_accept": false, "push_accept": false,
    "efinance_agreement": true,           // ★ 필수 true
    "tos":                true            // ★ 필수 true
  },

  // ─────── 카트 식별 ───────
  "serving_type":   "vd",
  "cart_uuid":      "<uuid-v4>-b24b",
  "cart_signature": "<sha1 hex>",
  "vendor": { "vendor_id": <int>, "franchise_id": <int|null> },

  // ─────── feature flags ───────
  "configuration": {
    "feature_configuration": {
      "additional_discount": true, "restaurant_discount": true,
      "delivery_fee_funding": true, "yotime_deal": true,
      "bargain_promotion": true, "bargain_vendor_promotion": true,
      "bargain_point_promotion": true
    }
  },
  "referral": "",
  "build_type": ""
}

자동화 코드 — Python 핵심부

전체 구현은 tools/scripts/replay_client.py 참조. PG chain 자동화의 핵심:

import re, httpx, html as html_lib
from urllib.parse import urljoin, urlparse

def extract_first_form(html_text):
    m = re.search(r'<form([^>]*)>(.*?)</form>', html_text, re.DOTALL | re.IGNORECASE)
    if not m: return None
    attrs, inner = m.group(1), m.group(2)
    action = re.search(r'action="([^"]+)"', attrs).group(1)
    method = (re.search(r'method="([^"]+)"', attrs) or [None, 'POST'])[1].upper()
    inputs = re.findall(r'<input[^>]*name="([^"]+)"[^>]*value="([^"]*)"', inner)
    fields = {html_lib.unescape(n): html_lib.unescape(v) for n, v in inputs}
    return action, method, fields

def follow_pg_chain(payment_url, restaurant_id, serving_type="vd"):
    pg = httpx.Client(timeout=30, follow_redirects=True)  # ★ 자동 redirect
    url, method, data = payment_url, "GET", None
    referer = f"https://checkout.yogiyo.co.kr/food-checkout/?restaurant_id={restaurant_id}&serving_type={serving_type}"
    headers = {"User-Agent": "Mozilla/5.0 ...", "Accept": "text/html"}

    for _ in range(10):
        h = {**headers, "Referer": referer}
        if data: h["Content-Type"] = "application/x-www-form-urlencoded"
        r = pg.get(url, headers=h) if method == "GET" else pg.post(url, headers=h, data=data)

        # orderyo callback 도착하면 종료
        if "orderyo.yogiyo.co.kr" in str(r.url):
            order_num   = re.search(r'"order_number":\s*"([^"]+)"', r.text).group(1)
            purchase_id = int(re.search(r'"purchase_id":\s*(\d+)', r.text).group(1))
            return {"order_number": order_num, "purchase_id": purchase_id}

        action, method, data = extract_first_form(r.text)
        if action.startswith("/"):
            p = urlparse(str(r.url))
            action = f"{p.scheme}://{p.netloc}{action}"
        referer, url = str(r.url), action

보안 / 함정 노트

토큰 위치

토큰알고리즘저장 위치 / TTL
access_token / authyo-tokenRS256EncryptedSharedPreferences 의 pref_access_token, TTL 2hr
refresh_tokenHS512EncryptedSharedPreferences, TTL 60일, rotating
wpayUserKey / wpayToken(PG 발급)payo.yogiyo.co.kr 백엔드 측 저장. 카드 1회 등록 후 영구
location_tokenHS256receiveInitialDataFromApp 으로 주입, lat/lng/region 인코딩

주의사항

  • ██ 마스킹의 정체: OkHttp 자체의 Headers.toString() 동작. 앱이 redact 하는 게 아님. Request.header(name) 후킹으로 raw 추출 가능.
  • payment_url 1회용: Step 4 의 응답 받자마자 즉시 follow 해야 함. 시간 지나면 무효화.
  • cart_uuid-b24b suffix: 응답에 이미 포함되어 있으므로 그대로 사용. (b24b = food 카테고리 magic)
  • customer 주소 정보 공급: customer-info 응답에 주소 없음. receiveInitialDataFromApp bridge 호출로만 메모리에 주입됨. 외부 자동화에서는 한 번 캡처해서 stub 으로 보유.
  • address.road_name 변형: client 가 "<sido> <gugun> <원본 road_name>" 으로 덮어쓴 형태로 보냄.
  • WebView 쿠키 mirror: WebBaseUtilKt.a() 가 OkHttp 쿠키를 www.yogiyo.co.kr 도메인에 setCookie 함. 외부 자동화는 쿠키 불필요 (Authorization 헤더만 사용).
  • zero payment vs 일반 결제: total == 0 이면 Step 5 (PG chain) 건너뜀, 응답에 즉시 purchase_id 옴. 쿠폰 100% 할인 같은 케이스.

왜 PG chain 자동화가 가능한가

KG이니시스의 1초결제(YGYPAY)는 사용자가 카드 1회 등록 + 1회 결제를 마친 후 발급되는 wpayUserKey / wpayToken 으로 작동합니다. 이 토큰은 PG 서버가 발급한 long-lived 토큰이고, 추가 인증 (PIN, 본인확인, OTP) 없이 결제를 trigger 합니다.

따라서 form chain 의 3 단계는 모두 이미 등록된 결제 정보의 검증만 수행하며, 클라이언트 측에서 새로운 비밀이나 동적 값을 만들 필요가 없습니다. signature 도 yogiyo (payo) 백엔드가 만들어 form 에 박아 보냅니다.