# 使用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;
