> For the complete documentation index, see [llms.txt](https://onestore-dev.gitbook.io/dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onestore-dev.gitbook.io/dev/chi/tools/webshop/billing.md).

# Webshop 支付联动

{% hint style="info" %}
为联动 ONE webshop 支付，必须完成 PNS 接收 和 购买确认。

* [**PNS(Payment Notification Service)** ](#pns)
  * 用户完成购买后，Webshop 会通过 PNS 通知将支付结果与相关信息发送至开发者服务器 URL。
  * 请根据该信息向用户发放道具。
* [**购买确认**](#purchase)
  * 用于将道具已成功发放的信息通知至 ONE store 服务器。
  * 可根据需求使用 consumePurchase 或 acknowledgePurchase。
  * 若购买确认未在 3 天内完成，则视为发放失败，该购买将自动取消。
    {% endhint %}

## 1. PNS  <a href="#pns" id="pns"></a>

发生购买或取消时，ONE store 会向开发者服务器发送通知。

{% hint style="info" %}
PNS 规范与 In‑App Payment API V7 相同 (Reference :  [使用PNS(Push Notification Service)](/dev/chi/tools/billing/v21/pns.md))
{% endhint %}

### **1.1** Configure PNS Reception Server URL <a href="#id-07.pns-pushnotificationservice-pns-url" id="id-07.pns-pushnotificationservice-pns-url"></a>

{% hint style="warning" %}
**注意事项**\
根据 ONE webshop 的安全策略，**仅支持 HTTPS 协议及 443 端口**，使用其他规格可能无法正常运行。
{% endhint %}

可在以下位置设置 PNS 接收 URL：`Developer Center > Webshop > Integration Mgmt.`

详细设置方法请参考 [Integration Mgmt Guide](/dev/chi/webshop/integration.md)。

### 1.2 PNS 详细规格

**HTTP TYPE :** HTTPS

**URI** : 在开发者中心设置的 Payment Notification URL

**Method** : POST

**Request Parameters** : N/A

**Request Header** :&#x20;

| Name         | Type   | Value              |
| ------------ | ------ | ------------------ |
| Content-Type | String | `application/json` |

**Request Body** : JSON format

<table><thead><tr><th width="328.740478515625">Name</th><th width="155.9791259765625">Type</th><th width="303.3809814453125">Description</th></tr></thead><tbody><tr><td><code>msgVersion</code></td><td>String</td><td><p></p><p></p><p>消息版本</p><ul><li>开发(Sandbox) : 3.1.0D</li><li>商用(商用测试) : 3.1.0</li></ul></td></tr><tr><td><code>clientId</code></td><td>String</td><td>ONE webshop 标题 ID（商品 ID）</td></tr><tr><td><code>productId</code></td><td>String</td><td>ONE webshop 道具 ID（In-App 商品 ID）</td></tr><tr><td><code>messageType</code></td><td>String</td><td>SINGLE_PAYMENT_TRANSACTION 固定</td></tr><tr><td><code>purchaseId</code></td><td>String</td><td>购买ID</td></tr><tr><td><code>developerPayload</code></td><td>String</td><td>由开发公司管理以标识购买件的标识符</td></tr><tr><td><code>purchaseTimeMillis</code></td><td>Long</td><td>在ONE store支付系统中完成支付的时间(ms)</td></tr><tr><td><code>purchaseState</code></td><td>String</td><td>COMPLETED : 已支付 / CANCELED : 取消</td></tr><tr><td><code>price</code></td><td>String</td><td>支付金额</td></tr><tr><td><code>priceCurrencyCode</code></td><td>String</td><td>支付金额货币代码(KRW, USD, ...)</td></tr><tr><td><code>productName</code></td><td>String</td><td>请求购买时，如开发公司设置了customized In-App商品标题则传达</td></tr><tr><td><code>paymentTypeList[]</code></td><td>List</td><td>支付信息列表</td></tr><tr><td><code>paymentTypeList[].paymentMethod</code></td><td>String</td><td>Payment method (for details, refer to the definition of paymentMethod)</td></tr><tr><td><code>paymentTypeList[].amount</code></td><td>String</td><td>Amount per payment method</td></tr><tr><td><code>billingKey</code></td><td>String</td><td>用于扩展的付款密钥</td></tr><tr><td><code>isTestMdn</code></td><td>Boolean</td><td>是否是测试机(true : 测试机, false : 非测试机)</td></tr><tr><td><code>purchaseToken</code></td><td>String</td><td>购买token</td></tr><tr><td><code>environment</code></td><td>String</td><td><p></p><p>支付环境</p><ul><li>开发(SANDBOX) : SANDBOX</li><li>商用 :COMMERCIAL</li></ul></td></tr><tr><td><code>marketCode</code></td><td>String</td><td><p>市场分类编码 ( MKT_ONE : ONE store, MKT_GLB : Global ONE store)</p><ul><li>在 Webshop 中，该值固定为 MKT_ONE。</li></ul></td></tr><tr><td><code>signature</code></td><td>String</td><td>此消息的signature</td></tr><tr><td><code>serviceUserId</code></td><td>String</td><td>用户游戏 ID</td></tr><tr><td><code>serviceServerId</code></td><td>String</td><td>用户服务器 ID</td></tr></tbody></table>

\
**Example**

```
{
    "msgVersion" : "3.1.0"
    "clientId":"0999999999",
    "productId":"0900001234",
    "messageType":"SINGLE_PAYMENT_TRANSACTION",
    "purchaseId":"SANDBOX3000000004564",
    "developerPayload":"OS_000211234",
    "purchaseTimeMillis":24431212233,
    "purchaseState":"COMPLETED",
    "price":"10000",
    "priceCurrencyCode":"KRW"
    "productName":"GOLD100(+20)"
    "paymentTypeList":[
        {
            "paymentMethod":"ONEPAY",
            "amount":"3000"
        },
        {
            "paymentMethod":"ONESTORECASH",
            "amount":"7000"
        }
    ],
    "billingKey" : "36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5",
    "isTestMdn" : true,
    "purchaseToken" : "TOKEN",
    "environment" : "SANDBOX",
    "marketCode" : "MKT_ONE"
    "signature" : "ajkfl;askfjkladfjksl",
    "serviceUserId" : "user1234",
    "serviceServerId" : "server01"
}
```

#### paymentMethod(ONE store支付方式)定义

| paymentMethod   | 支付方式名称           | 说明                        |
| --------------- | ---------------- | ------------------------- |
| DCB             | 手机支付             | 在运营商费用账单上以"信息使用费"项目收取     |
| PHONEBILL       | 手机小额支付           | 在运营商费用账单上以"小额支付"项目收取      |
| ONEPAY          | ONE pay          | ONE store提供的便捷支付          |
| CREDITCARD      | 信用卡              | 一般信用卡支付                   |
| 11PAY           | 11Pay            | SK Plannet提供的信用卡便捷支付      |
| NAVERPAY        | N pay            | Naver提供的Naver pay支付       |
| CULTURELAND     | Culture cash     | 韩国文化振兴提供的Culture cash支付   |
| TELCOMEMBERSHIP | 通讯公司会员           | 通讯公司提供的会员支付               |
| OCB             | OK cashbag       | SK Plannet提供的OK cashbag支付 |
| POINT           | ONE store point  | ONE store point支付         |
| ONESTORECASH    | ONE store cash   | ONE store cash支付          |
| COUPON          | ONE store coupon | ONE store Coupon支付        |
| EWALLET         | e-Wallet         | e-Wallet支付                |
| BANKACCT        | 银行账户支付           | 一般银行账户支付                  |
| PAYPAL          | PAYPAL           | Paypal支付                  |
| MYCARD          | My card          | 智冠科技提供的MY CARD支付          |

&#x20;

#### Signatue验证方法 <a href="#id-shi-yong-pnspushnotificationservicesignature-yan-zheng-fang-fa" id="id-shi-yong-pnspushnotificationservicesignature-yan-zheng-fang-fa"></a>

使用下面的代码，您可以检查signature是否伪造。

* 代码中的 PublicKey 指的是 `Developer Center > Webshop > Intergration Mgmt. > Settings for Licensing`菜单中提供的许可证密钥。

{% tabs %}
{% tab title="Java" %}

```java
import java.security.PublicKey;
   
import org.apache.commons.codec.binary.Base64;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
   
   
public class SignatureVerifier {
   
    private static final String SIGN_ALGORITHM = "SHA512withRSA";
    private ObjectMapper mapper = new ObjectMapper();
   
   
    boolean verify(String rawMsg, PublicKey key) throws Exception {
        // Extract the signature from the JSON message.
        JsonNode root = mapper.readTree(rawMsg);
        String signature = root.get("signature").getValueAsText();
        ((ObjectNode)root).remove("signature");
          
        // Verify that the extracted signature is correct.
        Signature sign = Signature.getInstance(SIGN_ALGORITHM);
        sign.initVerify(key);
        sign.update(root.toString().getBytes("UTF-8"));
        return sign.verify(Base64.decodeBase64(signature));
    }
}
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php
function formatPublicKey($publicKey) {
 $BEGIN= "-----BEGIN PUBLIC KEY-----";
 $END = "-----END PUBLIC KEY-----";
  
 $pem = $BEGIN . "\n";
 $pem .= chunk_split($publicKey, 64, "\n");
 $pem .= $END . "\n";
  
 return $pem;
}
  
function formatSignature($signature) {
 return base64_decode(chunk_split($signature, 64, "\n"));
}
  
// Sample message
$sampleMessage = '{"msgVersion":"3.1.0D","purchaseId":"SANDBOX3000000004564","developerPayload":"OS_000211234","clientId":"0000000001","productId":"0900001234","messageType":"SINGLE_PAYMENT_TRANSACTION","purchaseMillis":24431212233,"purchaseState":"COMPLETED","price":20000,"productName":"한글은?GOLD100(+20)","paymentTypeList":[{"paymentMethod":"DCB","amount":3000},{"paymentMethod":"ONESTORECASH","amount":7000}],"billingKey":"36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5","isTestMdn":true,"signature":"MNxIl32ws+yYWpUr7om+jail4UQxBUXdNX5yw5PJKlqW2lurfvhiqF0p4XWa+fmyV6+Ot63w763Gnx2+7Zp2Wgl73TWru5kksBjqVJ3XqyjUHDDaF80aq0KvoQdLAHfKze34cJXKR/Qu8dPHK65PDH/Vu6MvPVRB8TvCJpkQrqg="}';
  
// Sample public key
$publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMzpWJoK1GSOrr4juma5+sREYjdCW8/xSd9+6z6PAkUH5af97wy8ecfkLtP9LK5VskryfDlcOjfu0BgmHYntAqKT7B4KWk8jWbJ8VHUpp30H95UbcnCRFDqpEtwYzNA5gNMYKtAdbL41K8Fbum0Xqxo65pPEI4UC3MAG96O7X1WQIDAQAB";
  
  
// Parse JSON message
$jsonArr = json_decode($sampleMessage, true);
  
// Extract and remove signature
$signature = $jsonArr["signature"];
unset($jsonArr["signature"]);
$originalMessage = json_encode($jsonArr, JSON_UNESCAPED_UNICODE);
  
// Veify
$formattedKey = formatPublicKey($publicKey);
$formattedSign = formatSignature($signature);
$hash_algorithm = 'sha512';
  
$success = openssl_verify($originalMessage, $formattedSign, $formattedKey, $hash_algorithm);
if ($success == 1) {
 echo "verified";
}
else {
 echo "unverified";
}
?>
```

{% endtab %}

{% tab title="Python" %}
{% code overflow="wrap" %}

```python
# -*- coding: utf-8 -*-
  
import json
from base64 import b64decode
from collections import OrderedDict
  
from Crypto.Hash import SHA512
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
  
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
  
hash = "SHA-512"
  
  
def verify(message, signature, pub_key):
    signer = PKCS1_v1_5.new(pub_key)
    digest = SHA512.new()
    digest.update(message)
    return signer.verify(digest, signature)
  
  
jsonData = json.loads(rawMsg, encoding='utf-8', object_pairs_hook=OrderedDict)
signature = jsonData['signature']
del jsonData['signature']
originalMessage = json.dumps(jsonData, ensure_ascii=False, encoding='utf-8', separators=(',', ':'))
  
RSA.importKey(publickey).publickey()
print(verify(originalMessage, b64decode(signature), RSA.importKey(publickey).publickey()))
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 1.3 **Notification传输方法**

ONE store中的PNS服务器通过HTTP(S)请求向开发公司服务器发送notification。

此时，开发公司服务器应以200响应HTTP Status Code，表示已正常接收notification。

如果由于网络延迟而丢失，或者由于开发公司服务器的异常情况，HTTP Status Code未能以200进行响应，PNS服务器将认为notification传输失败，并在3天内最多执行30次重传。

Notification的重传是在具有一定delay后执行，如下例所示，当重试次数增多时，delay会逐渐增加。

**Example**

| 次数     | delay (秒) | 重传时间                |
| ------ | --------- | ------------------- |
| 0 (首次) | 0         | 2020-05-17 13:10:00 |
| 1      | 30        | 2020-05-17 13:10:30 |
| 2      | 120       | 2020-05-17 13:12:30 |
| 3      | 270       | 2020-05-17 13:17:00 |
| 4      | 480       | 2020-05-17 13:25:00 |
| ...    | ...       | ...                 |

## 2. 购买确认 <a href="#purchase" id="purchase"></a>

为确保道具正常发放，必须执行购买确认流程。

{% hint style="warning" %}
使用 ONE store Open API 需要 OAuth 认证。

详细内容请参考 [ONE store OAuth](/dev/chi/tools/billing/v21/serverapi.md#onestoreinapp-zhi-fu-fu-wu-qi-apiapiv7onestoreoauth) 指南。
{% endhint %}

{% hint style="success" %}
**可通过 `getUnconfirmedPurchases` API 查询尚未完成购买确认的记录。**

建议使用该 API 以确保稳定的支付处理。

* 详细说明请参考 [getUnconfirmedPurchases](/dev/chi/tools/billing/v21/serverapi.md#getunconfirmedpurchases) 文档。
  {% endhint %}

### 2.1 consumePurchase

<table data-header-hidden><thead><tr><th width="129.44921875"></th><th></th></tr></thead><tbody><tr><td><strong>Path</strong></td><td>(Commercial) <br>https://iap-apis.onestore.net/v7/apps/{clientId}/purchases/inapp/products/{productId}/{purchaseToken}/consume<br><br>(Development) <br>https://sbpp.onestore.net/v7/apps/{clientId}/purchases/inapp/products/{productId}/{purchaseToken}/consume</td></tr><tr><td><strong>Description</strong></td><td><ul><li><p>将购买的管理型商品状态更改为消费状态。</p><ul><li>处理完成后，<strong>用户可以再次购买相同道具</strong>。</li></ul></li><li>Error Code: <a href="https://onestore-dev.gitbook.io/dev/chi/tools/billing/v21/serverapi#onestoreinapp-zhi-fu-fu-wu-qi-apiapiv7-biao-zhun-xiang-ying-gui-ge">Standard Response Codes</a></li></ul></td></tr></tbody></table>

* **Method :** POST
* **Request Parameter :** Path Variable格式
  * String clientId :调用 API的应用软件客户端ID (Data Size : 128)
  * String productId : 商品ID (Data Size : 150)
  * String purchaseToken : 购买token (Data Size : 20)
* **Request Header:**&#x20;

  | Parameter Name | Data Type | Required | Description                        |
  | -------------- | --------- | -------- | ---------------------------------- |
  | Authorization  | String    | true     | 通过Access Token API接收的access\_token |
  | Content-Type   | String    | true     | application/json                   |
  | x-market-code  | String    | false    | 市场分类代码                             |
* **Example**&#x20;

  ```java
  Request.setHeader("Authorization", "Bearer 680b3621-1234-1234-1234-8adfaef561b4");
  Request.setHeader("Content-Type", "application/json");
  Request.setHeader("x-market-code", "MKT_ONE");
  ```
* **Request Body** : JSON 格式

  | Element Name     | Data Type | Required | Description |
  | ---------------- | --------- | -------- | ----------- |
  | developerPayload | String    | false    | <p><br></p> |
* **Example :**

  ```json
  {
      "developerPayload": "your payload"
  }
  ```
* **Response Body :** JSON 형식

  传回以下格式的响应，以便在成功处理API时更直观地判断是否处理完成。如果API处理失败，将传回标准错误响应。

  | Element Name | Data Type | Data Size | Description |
  | ------------ | --------- | --------- | ----------- |
  | code         | String    | -         | 响应代码        |
  | message      | String    | -         | 响应信息        |
  | result       | Object    | -         | <p><br></p> |
* **Example :**&#x20;

  ```json
  HTTP/1.1 200 OK
  Content-type: application/json;charset=UTF-8
  {
      "result" : {
          "code" : "Success",
          "message" : "Request has been completed successfully."
      }
  }
  ```

### 2.2 acknowledgePurchase

<table data-header-hidden><thead><tr><th width="125.921875"></th><th></th></tr></thead><tbody><tr><td><strong>Path</strong></td><td>(Commercial) <br>https://iap-apis.onestore.net/v7/apps/{clientId}/purchases/all/products/{productId}/{purchaseToken}/acknowledge<br><br>(Development) <br>https://sbpp.onestore.net/v7/apps/{clientId}/purchases/all/products/{productId}/{purchaseToken}/acknowledge</td></tr><tr><td><strong>Description</strong></td><td><ul><li><p>将购买的管理型或包月型商品变更为购买确认状态。</p><ul><li>处理完成后，<strong>用户可以再次购买相同道具</strong>。</li></ul></li><li>Error Code: <a href="https://onestore-dev.gitbook.io/dev/chi/tools/billing/v21/serverapi#onestoreinapp-zhi-fu-fu-wu-qi-apiapiv7-biao-zhun-xiang-ying-gui-ge">Standard Response Codes</a></li></ul></td></tr></tbody></table>

* **Method :** POST

* **Request Parameter :** Path Variable格式

  * String clientId : 调用 API的应用软件客户端ID (Data Size : 128)
  * String productId : 商品ID (Data Size : 150)
  * String purchaseToken : 购买token (Data Size : 20)

* **Request Header:**&#x20;

  | Parameter Name | Data Type | Required | Description                        |
  | -------------- | --------- | -------- | ---------------------------------- |
  | Authorization  | String    | true     | 通过Access Token API接收的access\_token |
  | Content-Type   | String    | true     | application/json                   |
  | x-market-code  | String    | false    | 市场分类代码                             |

* **Example**&#x20;

  ```java
  Request.setHeader("Authorization", "Bearer 680b3621-1234-1234-1234-8adfaef561b4");
  Request.setHeader("Content-Type", "application/json");
  Request.setHeader("x-market-code", "MKT_ONE");
  ```

* **Request Body** : JSON 格式

  | Element Name     | Data Type | Required | Description |
  | ---------------- | --------- | -------- | ----------- |
  | developerPayload | String    | false    | <p><br></p> |

* **Example :**

  ```json
  {
      "developerPayload": "your payload"
  }
  ```

* **Response Body :** JSON 格式

  传回以下格式的响应，以便在成功处理API时更直观地判断是否处理完成。如果API处理失败，将传回标准错误响应。

  | Element Name | Data Type | Data Size | Description |
  | ------------ | --------- | --------- | ----------- |
  | code         | String    | -         | 响应代码        |
  | message      | String    | -         | 响应信息        |
  | result       | Object    | -         | <p><br></p> |

* **Example :**&#x20;

  ```json
  HTTP/1.1 200 OK
  Content-type: application/json;charset=UTF-8
  {
      "result" : {
          "code" : "Success",
          "message" : "Request has been completed successfully."
      }
  }
  ```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://onestore-dev.gitbook.io/dev/chi/tools/webshop/billing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
