Node.jsでPayPalの商品購入の通知(Webhook)を受け取る


PayPalライトユーザーなので簡単な通知受け取りやってみます。

PayPalのAPIは正直分かりにくいので自分用にまとめます。APIのドキュメントページは以前よりは良くなったけど、昔から存在する分、調べていると過去の情報と混同しがちです。なので2018年2月バージョンです。適宜読み替え必須。

別途ちゃんと記事にまとめたい

## `かんたん決済ボタン`で会社の商品(Nefry BT)販売しています

PayPalには`かんたん決済ボタン`というツールがあります。たぶん通称が`PayPalボタン`で、製品名が`ウェブペイメントスタンダード`。呼び方いっぱいありますね苦笑

> https://www.paypal.com/jp/webapps/mpp/merchant/solutions/paypal-button

![](https://i.gyazo.com/cec92c97d7516a8fda123651a45b6bd5.png)

HTMLソースを貼り付けるだけで商品購入機能を追加でき、すごく簡単に作れて助かっています。

例えば[Nefry BT](https://dotstud.io/shop/nefry-bt-single/)の購入ページからたどると、こんな画面になります。

![](https://i.gyazo.com/f8b03006a8d6e31988757ffaeb0c06e4.png)

## かんたん決済ボタン(PayPalボタン)の購入通知をNode.jsで受け取りたい

この、商品が購入されたことを知りたいときに使う仕組みが`Instant Payment Notification (IPN)`と呼ぶそうです。認識間違ってるかもしれないけど僕はそう理解しておきます。

ちなみに、毎回購入してテストするのも大変なので、購入があったときに通知をシミュレーション出来るツールもあります。
`Instant Payment Notification (IPN) Simulator`らしいです。

> https://developer.paypal.com/developer/ipnSimulator/

今回は手元のMacでNode.jsのサーバーを立てて[ngrok](https://ngrok.com/)を経由してPayPalからのWebhookを受け取ります。

![](https://i.gyazo.com/b9e80c6f4b773f7e20603d9d21a26ab4.png)

Expressを使ってPOSTリクエストを受け取るので、expressとbody-parserをインストールします。
ちなみにNode.jsはv9.5.0です。

```bash
$ npm init -y
$ npm i --save express body-parser
$ touch app.js
```

```app.js
'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.urlencoded({extended: false}));
app.use(express.static(__dirname+'/'));

const PORT = process.env.PORT || 3000;

app.post('/', async (req, res) => {
    console.log(req.body); //PayPalから送られて来る情報の確認    

    const response = {
        message: 'Success!'
    };
    res.end(JSON.stringify(response));
});

app.listen(PORT);
console.log(`listening on *:${PORT}`);
```

起動

```bash
$ node app.js
listening on *:3000
```

これで3000番ポートでサーバーが起動します。

このままだと、PayPal側からのWebhookを手元のPCで立てただけのサーバーでは受け取れないのでngrokを使ってトンネリングをします。

![](https://i.gyazo.com/abd4e25e1272c13107740b16a66d11e7.png)

まずインストールです。npm経由でインストールできます。

```bash
$ npm i -g ngrok
```

先ほどNode.jsで3000番ポートでサーバーを立てたので、3000番ポートにリダイレクトするトンネリングサーバーを起動します。ターミナルの別タブなどで起動しましょう。

```bash
$ ngrok http 3000
```

起動するとこんな感じでURLが発行されます。

```
ngrok by @inconshreveable                                      (Ctrl+C to quit)

Session Status                online
Account                       n0bisuke (Plan: Free)
Version                       2.2.8
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://2c0f263e.ngrok.io -> localhost:3000
Forwarding                    https://2c0f263e.ngrok.io -> localhost:3000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              8       0       0.00    0.00    0.21    5.61
```

この場合だと`https://2c0f263e.ngrok.io`がトンネリングサーバーのURLです。
このURLはグローバル公開されているので、このURLに対してPayPalはPOSTリクエストを送ることが出来ます。

このトンネリングサーバーにPOSTリクエストがあると、手元のNode.jsで立てたサーバーにもPOSTリクエストがそのまま送られます。

ちなみにngrokのURLはngrokを再起動するたびに再発行されるので注意しましょう。

### Instant Payment Notification (IPN) Simulatorで試してみる

[こちら](https://developer.paypal.com/developer/ipnSimulator/)のURLにアクセスし、`IPN handler URL`の箇所に、ngrokで作成したトンネリングサーバーのURL（今回だと`https://2c0f263e.ngrok.io`）を設定します。`Transaction type`はあんまりよく分かってないけど`Express Checkout`にしました。(教えて下さい)

![](https://i.gyazo.com/cb095aff70a08cbab51b23f1e6f43a6f.png)

この状態でページ下部の`Send IPN`を押しましょう。

![](https://i.gyazo.com/f1c44c64ba4210be82eb850186912c8f.png)

これで購入があったときのようなPOSTリクエストがPayPalから送られてきて、手元で確認できます。

```bash
$ node app.js

listening on *:3000
{ payment_type: 'instant',
  payment_date: 'Mon Feb 12 2018 19:27:32 GMT+0900 (JST)',
  payment_status: 'Pending',
  address_status: 'confirmed',
  payer_status: 'verified',
  first_name: 'John',
  last_name: 'Smith',
  payer_email: 'buyer@paypalsandbox.com',
  payer_id: 'TESTBUYERID01',
  address_name: 'John Smith',
  address_country: 'United States',
  address_country_code: 'US',
  address_zip: '95131',
  address_state: 'CA',
  address_city: 'San Jose',
  address_street: '123 any street',
  business: 'seller@paypalsandbox.com',
  receiver_email: 'seller@paypalsandbox.com',
  receiver_id: 'seller@paypalsandbox.com',
  residence_country: 'US',
  item_name: 'something',
  item_number: 'AK-1234',
  quantity: '1',
  shipping: '3.04',
  tax: '2.02',
  mc_currency: 'USD',
  mc_fee: '0.44',
  mc_gross: '12.34',
  mc_gross_1: '9.34',
  txn_type: 'web_accept',
  txn_id: '507675401',
  notify_version: '2.1',
  custom: 'xyz123',
  invoice: 'abc1234',
  test_ipn: '1',
  verify_sign: 'AV5g.RRvzcNMaeOO73JJ-F3fudM2ApaAE0NwEVMxteL8ElyK913tPoLn' }
```

ターミナルにこんな感じのログが表示されればOKです。

### 実際の`かんたん決済ボタン(PayPalボタン)`で試してみる

ボタンの設定画面で`ステップ 3: 高度な機能をカスタマイズする(オプション)`を選択し、その中の`高度な変数を追加する`の項目に`notify_url=(Webhookを受け取るサーバーのURL)`という形式で記述します。

今回の場合は`notify_url=https://2c0f263e.ngrok.io`を記述しました。

![](https://i.gyazo.com/366b69244b8f90c8eab62f5a6b7f233e.png)

この状態で実際に購入してみると手元のNode.jsのサーバーに情報が送られてきます。

```bash
{ mc_gross: '1',
  protection_eligibility: 'Eligible',
  address_status: 'confirmed',
  payer_id: 'YKQ5X3VAHDSR8',
  address_street: '����2-17-11\r\n�N���G�C�^�[�Y�V�F�A�n�E�X �g���u��',
  payment_date: '12:19:52 Feb 17, 2018 PST',
  payment_status: 'Completed',
  charset: 'Shift_JIS',
  address_zip: '111-0025',
  first_name: '�ɉ�',
  mc_fee: '1',
  address_country_code: 'JP',
  address_name: '���� �ɉ�',
  notify_version: '3.9',
  custom: '',
  payer_status: 'verified',
  business: 'info@dotstud.io',
  address_country: 'Japan',
  address_city: '�䓌��',
  quantity: '1',
  verify_sign: 'AXK39wE-yxGXy8w.5f-Q92oLUMrXAgw1pM3bgPNM5AJYP8TnyyiCUzHc',
  payer_email: 'sugawara@ryousuke.org',
  txn_id: '8TW04018NT010421T',
  payment_type: 'instant',
  last_name: '����',
  address_state: '�����s',
  receiver_email: 'info@dotstud.io',
  payment_fee: '',
  receiver_id: 'R9B4DK8ZQGJ7U',
  txn_type: 'web_accept',
  item_name: '[�w��]�T���v���{�^��',
  mc_currency: 'JPY',
  item_number: '',
  residence_country: 'JP',
  transaction_subject: '',
  payment_gross: '',
  ipn_track_id: '4a2e2f679688c' }
```

送られて来たけど文字化けしてますね...

### 文字化け対応

送られてきた情報をの`charset: 'Shift_JIS'`が怪しいです。調べるとUTF8に設定できるらしい。

> [RailsでPaypalのIPNを処理](https://qiita.com/Arahabica/items/cc6098d0d7fc299fbc44)

[設定画面](https://www.paypal.com/businessprofile/mytools)から`PayPalボタンの言語コード化`を選択します。

![](https://i.gyazo.com/c49348ad9b7f658e5c73894237422d4f.png)

[言語のエンコード](https://www.paypal.com/cgi-bin/customerprofileweb?cmd=_profile-language-encoding)画面になります。

![](https://i.gyazo.com/b0a034c50fe84a6e3c7f2c753495b31f.png)

`詳細オプション`を選択します。`エンコード方式`が確認出来ますが、確かに`Shif_JIS`になってますねぇ。。。

![](https://i.gyazo.com/824e8f12443be6fcba752b15b3884baf.png)

`UTF-8`に変更しましょう。

![](https://i.gyazo.com/9fb06027bc530effc6e478d22c84d273.png)

これでやっととれました。PayPalに登録している住所やメールアドレス、どの商品を買ったかなどの情報がNode.jsで文字化けせずに受け取れました。プログラム側でエンコードしてもいいですけど、UTF-8にしておいた方が何かと便利です。

```bash
{ mc_gross: '2',
  protection_eligibility: 'Eligible',
  address_status: 'confirmed',
  payer_id: 'YKQ5X3VAHDSR8',
  address_street: 'xxxx2-17-11\r\nXXXXXXシェアハウス XXXXX',
  payment_date: '14:31:07 Feb 17, 2018 PST',
  payment_status: 'Completed',
  charset: 'UTF-8',
  address_zip: '111-xxxx',
  first_name: '遼介',
  mc_fee: '2',
  address_country_code: 'JP',
  address_name: '菅原 遼介',
  notify_version: '3.9',
  custom: '',
  payer_status: 'verified',
  business: 'xxxx@xxxx.io',
  address_country: 'Japan',
  address_city: '台東区',
  quantity: '1',
  verify_sign: 'XXXXXXXXXX59viv6mMh95wQVtgejAmdhHhoJEWKLm9oesHOJkWqMK-8G',
  payer_email: 'xxxxx@xxxxx.org',
  txn_id: '9XX216775X972960X',
  payment_type: 'instant',
  last_name: '菅原',
  address_state: '東京都',
  receiver_email: 'xxxx@xxxx.io',
  payment_fee: '',
  receiver_id: 'XXXXDK8ZQGJ7U',
  txn_type: 'web_accept',
  item_name: '[購入]サンプルボタン',
  mc_currency: 'JPY',
  item_number: '44',
  residence_country: 'JP',
  transaction_subject: '',
  payment_gross: '',
  ipn_track_id: 'xxxxf023bdc2a' }
```

## 所感

昔よりは分かりやすいけどまだまだ分かりにくい印象なので今回のメモもいつまで使えるのかなぁという感じです苦笑

PayPalボタンと同様のIPNシミュレータの使い方を知りたいですけど、よくわからなかったので実際試すときは1円とか2円のPayPalボタンを作って購入テストしてみました。笑

![](https://i.gyazo.com/c063b62be1e69b740ad0b9cf8de798b7.png)

もっとスマートなデバッグしたい気持ち。