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


---

# 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/cht/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.
