> 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."
      }
  }
  ```
