# 使用PNS(Push Notification Service)

ONE store為開發者提供兩種Push Notification Service。 &#x20;

* 第一種是PNS(Payment Notification Service，支付通知服務)，當發生In-App商品支付或取消支付時，ONE store向開發公司伺服器發送通知。 &#x20;
* 第二種是SNS（Subscription Notifacation Service，訂閱通知服務），在訂閱狀態發生變化時向開發公司伺服器發送通知。<br>

{% 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       | 訂閱狀態                                                                        |
  | purchaseToken            | String        | 购买Token                                                                     |
  | productId                | String        | In-App商品的商品ID                                                               |
  | environment              | String        | <p>支付環境<br>· 開發(Sandbox) : SANDBOX<br>· 商用 :COMMERCIAL</p>                  |
  | marketCode               | String        | 市場分類代碼 ( MKT\_ONE )                                                         |

* **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;
