# 使用PNS(Push Notification Service)

## **概要**  <a href="#id-shi-yong-pnspushnotificationservice-gai-yao" id="id-shi-yong-pnspushnotificationservice-gai-yao"></a>

ONE store为开发者提供两种Push Notification Service。 &#x20;

* 第一种是PNS(Payment Notification Service，支付通知服务)，当发生In-App商品支付或取消支付时，ONE store向开发公司服务器发送通知。 &#x20;
* 第二种是SNS（Subscription Notifacation Service，订阅通知服务），在订阅状态发生变化时向开发公司服务器发送通知。

{% hint style="warning" %}

* 由于notification可能因发送/接收服务器的状态而延迟或丢失，因此不建议以接收notification为准提供App商品(服务)。
* 如果您想以服务器到服务器方式确认是否正常付款，我们建议您使用相关服务器API进行查询，而不是使用PNS notification。
* ONE store可能会进行付款测试以便验证和监控，在付款/取消付款时，这些测试也会收到同样的notification。ONE store的付款测试历史记录会定期由ONE store自行取消。
  {% endhint %}

## **设置PNS接收服务器URL** <a href="#id-shi-yong-pnspushnotificationservice-she-zhi-pns-jie-shou-fu-wu-qi-url" id="id-shi-yong-pnspushnotificationservice-she-zhi-pns-jie-shou-fu-wu-qi-url"></a>

您可以通过点击"开发者中心>Apps>App商品选择>In-App信息"菜单中的"管理PNS"按钮来设置接收PNS的开发公司服务器的URL。

URL可以分别设置Sandbox(开发用)支付环境和商用(包括商用测试)支付环境，如果开发用/商用服务器相同，则输入相同的URL即可。

## **PNS详细信息** <a href="#id-shi-yong-pnspushnotificationservicepns-xiang-xi-xin-xi" id="id-shi-yong-pnspushnotificationservicepns-xiang-xi-xin-xi"></a>

{% hint style="info" %}
由于2025年3月20日开发者中心的改版，PNS 3.1.0版本已新增。\
`packageName`参数已更改为`clientId`，对于3.0.0及以下版本，没有任何更改。
{% endhint %}

### Payment Notification消息发送规格(ONE store→开发公司服务器)  <a href="#id-shi-yong-pnspushnotificationservicepaymentnotification-xiao-xi-fa-song-gui-ge-onestore-kai-fa-gon" id="id-shi-yong-pnspushnotificationservicepaymentnotification-xiao-xi-fa-song-gui-ge-onestore-kai-fa-gon"></a>

* **URI** : 在开发者中心设置的 Payment Notification URL
* **Method** : POST
* **Request Parameters** : N/A&#x20;
* **Request Header**

  | **Parameter Name** | **Data Type** | **Description**  |
  | ------------------ | ------------- | ---------------- |
  | Content-Type       | String        | application/json |
* **Request Body** : JSON格式

  <table data-header-hidden><thead><tr><th></th><th></th><th width="247"></th></tr></thead><tbody><tr><td><strong>Element Name</strong></td><td><strong>Data Type</strong></td><td><strong>Description</strong></td></tr><tr><td>msgVersion</td><td>String</td><td><p>消息版本</p><ul><li>开发(Sandbox) : 3.1.0D</li><li>商用(商用测试) : 3.1.0</li></ul></td></tr><tr><td>clientId</td><td>String</td><td>应用软件的Client ID</td></tr><tr><td>productId</td><td>String</td><td>In-App商品的商品ID</td></tr><tr><td>messageType</td><td>String</td><td>SINGLE_PAYMENT_TRANSACTION 固定</td></tr><tr><td>purchaseId</td><td>String</td><td>购买ID</td></tr><tr><td>developerPayload</td><td>String</td><td>由开发公司管理以标识购买件的标识符</td></tr><tr><td>purchaseTimeMillis</td><td>Long</td><td>在ONE store支付系统中完成支付的时间(ms)</td></tr><tr><td>purchaseState</td><td>String</td><td>COMPLETED : 已支付 / CANCELED : 取消</td></tr><tr><td>price</td><td>String</td><td>支付金额</td></tr><tr><td>priceCurrencyCode</td><td>String</td><td>支付金额货币代码(KRW, USD, ...)</td></tr><tr><td>productName</td><td>String</td><td>请求购买时，如开发公司设置了customized In-App商品标题则传达</td></tr><tr><td>paymentTypeList</td><td>List</td><td>支付信息列表</td></tr><tr><td><br></td><td>paymentMethod</td><td>String</td></tr><tr><td><br></td><td>amount</td><td>String</td></tr><tr><td>billingKey</td><td>String</td><td>用于扩展的付款密钥</td></tr><tr><td>isTestMdn</td><td>Boolean</td><td>是否是测试机(true : 测试机, false : 非测试机)</td></tr><tr><td>purchaseToken</td><td>String</td><td>购买token</td></tr><tr><td>environment</td><td>String</td><td><p>支付环境</p><ul><li>开发(SANDBOX) :  SANDBOX</li><li>商用 :COMMERCIAL</li></ul></td></tr><tr><td>marketCode</td><td>String</td><td>市场分类编码 ( MKT_ONE : ONE store, MKT_GLB : Global ONE store)</td></tr><tr><td>signature</td><td>String</td><td>此消息的signature</td></tr></tbody></table>
* **Example**

```json
{
	"msgVersion" : "3.1.0"
	"clientId":"0000000001",
	"productId":"0900001234",
	"messageType":"SINGLE_PAYMENT_TRANSACTION",
	"purchaseId":"SANDBOX3000000004564",
	"developerPayload":"OS_000211234",
	"purchaseTimeMillis":24431212233,
	"purchaseState":"COMPLETED",
	"price":"10000",
	"priceCurrencyCode":"KRW"
	"productName":"GOLD100(+20)"
	"paymentTypeList":[
		{
			"paymentMethod":"DCB",
			"amount":"3000"
		},
		{
			"paymentMethod":"ONESTORECASH",
			"amount":"7000"
		}
	],
	"billingKey" : "36FED4C6E4AC9E29ADAF356057DB98B5CB92126B1D52E8757701E3A261AF49CCFBFC49F5FEF6E277A7A10E9076B523D839E9D84CE9225498155C5065529E22F5",
	"isTestMdn" : true,
	"purchaseToken" : "TOKEN...",
	"environment" : "SANDBOX",
	"marketCode" : "MKT_ONE"
	"signature" "SIGNATURE..."
}

```

#### paymentMethod(ONE store支付方式)定义 <a href="#id-shi-yong-pnspushnotificationservicepaymentmethodonestore-zhi-fu-fang-shi-ding-yi" id="id-shi-yong-pnspushnotificationservicepaymentmethodonestore-zhi-fu-fang-shi-ding-yi"></a>

| **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支付          |

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

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

* 代码中的 PublicKey 指的是“许可证管理”菜单中提供的许可证密钥。

{% 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 {
        // JSON 메시지에서 signature를 추출한다.
        JsonNode root = mapper.readTree(rawMsg);
        String signature = root.get("signature").getValueAsText();
        ((ObjectNode)root).remove("signature");
          
        // 추출한 signature가 올바른 값인지 검증한다.
        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 %}

### Subscription Notification消息发送规格(ONE store → 开发公司服务器)

* **URI** : 在开发者中心设置的Subscription Notification URL
* **Method** : POST
* **Request Parameters** : N/A&#x20;
* **Request Header**&#x20;

  | **Parameter Name** | **Data Type** | **Description**  |
  | ------------------ | ------------- | ---------------- |
  | Content-Type       | String        | application/json |
* **Request Body** : JSON格式

  | **Element Name**         | **Data Type** | **Description**                                                            |
  | ------------------------ | ------------- | -------------------------------------------------------------------------- |
  | msgVersion               | String        | <p>消息版本</p><ul><li>开发(Sandbox) : 3.1.0D</li><li>商用(商用测试) : 3.1.0</li></ul> |
  | clientId                 | String        | 应用软件的Client ID                                                             |
  | eventTimeMillis          | Long          | event发生时间                                                                  |
  | subscriptionNotification | Object        | 支付信息目录                                                                     |
  | version                  | String        | 订阅提醒消息版本                                                                   |
  | notificationType         | Integer       | 订阅状态                                                                       |
  | <p><br>purchaseToken</p> | String        | 购买Token                                                                    |
  | <p><br>productId</p>     | String        | In-App商品的商品ID                                                              |
  | environment              | String        | <p>支付环境<br>· 开发(Sandbox) : SANDBOX<br>· 商用 :COMMERCIAL</p>                 |
  | marketCode               | String        | 市场分类代码 ( MKT\_ONE : ONE store, MKT\_GLB : ONE store Global )               |
* **Example**&#x20;

```json
{
    "msgVersion":"3.1.0",
    "clientId":"0000000001",
    "eventTimeMillis":24431212233000,
    "subscriptionNotification": {
        "version": "1",
        "notificationType" : 1,
        "purchaseToken":"TOKEN",
        "productId": "com.product.id"
    },
    "environmenmt": "COMMERCIAL",
    "marketCode": "MKT_ONE"
}
```

#### 订阅状态定义 <a href="#id-shi-yong-pnspushnotificationservice-ding-yue-zhuang-tai-ding-yi" id="id-shi-yong-pnspushnotificationservice-ding-yue-zhuang-tai-ding-yi"></a>

| **订阅状态** | **订阅代码**                               | **说明**             |
| -------- | -------------------------------------- | ------------------ |
| 1        | SUBSCRIPTION\_RECOVERED                | 定期支付已从保留状态恢复。      |
| 2        | SUBSCRIPTION\_RENEWED                  | 已更新定期支付。           |
| 3        | SUBSCRIPTION\_CANCELED                 | 客户要求解除定期支付。        |
| 4        | SUBSCRIPTION\_PURCHASED                | 您购买了新的定期支付商品。      |
| 5        | SUBSCRIPTION\_ON\_HOLD                 | 由于支付失败，定期支付处于保留状态。 |
| 6        | SUBSCRIPTION\_IN\_GRACE\_PERIOD        | 由于支付失败，定期支付处于延期状态。 |
| 7        | SUBSCRIPTION\_RESTARTED                | 客户取消了解除定期支付的要求。    |
| 8        | SUBSCRIPTION\_PRICE\_CHANGE\_CONFIRMED | 用户同意变更定期支付的价格。     |
| 9        | SUBSCRIPTION\_DEFERRED                 | 已延长定期支付的使用期限。      |
| 10       | SUBSCRIPTION\_PAUSED                   | 已暂停定期支付。           |
| 11       | SUBSCRIPTION\_PAUSE\_SCHEDULE\_CHANGED | 已变更暂停定期支付的日程。      |
| 12       | SUBSCRIPTION\_REVOKED                  | 已立即解除定期支付。         |
| 13       | SUBSCRIPTION\_EXPIRED                  | 定期支付已到期。           |

#### Notification传输方法 <a href="#id-shi-yong-pnspushnotificationservicenotification-chuan-shu-fang-fa" id="id-shi-yong-pnspushnotificationservicenotification-chuan-shu-fang-fa"></a>

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

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

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

Notification的重传是在具有一定delay后执行，如下例所示，当重试次数增多时，delay会逐渐增加。\
&#x20;\
**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 |
| ...    | ...           | ...                 |

&#x20;


---

# Agent Instructions: 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:

```
GET https://onestore-dev.gitbook.io/dev/chi/tools/billing/v21/pns.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
