Webhook を受け取る

イベントの通知を受け取るには、以下の手順に従います。

  1. モニターするイベントや解析するイベントペイロードを決定する
  2. ローカルサーバーで Webhook エンドポイントを HTTP エンドポイント (URL) として作成する
  3. 各イベントオブジェクトを解析し、レスポンスステータスコード 200 を返し、Smartpay からのリクエストを処理する
  4. Webhook エンドポイント作成 API エンドポイント使って公開 HTTPS URL を Smartpay に登録する
  5. Webhook のセキュリティーを確保する (推奨)
  6. Webhook エンドポイントを公開 HTTPS URL としてデプロイする
  7. Webhook エンドポイントが正しく動作しているかテストする

1. モニターするイベントを決定する

サポートされているイベントの種類を確認し、登録するイベントを決定します。イベントオブジェクトイベントのサンプルコードを参照し、解析する必要があるオブジェクトの構造を確認します。

2. Webhook エンドポイントを作成する

Webhook エンドポイントの作成は、Web サイトでのページの作成と同様であり、URL を持つサーバー上の HTTPS エンドポイントです。ローカルマシンでエンドポイントを作成している場合は、HTTP でも構いませんが、本番環境では、HTTPS である必要があります。1つのエンドポイントで、複数の異なる種類のイベントを一度に処理することも、イベントごとに別々のエンドポイントを設定することも可能です。

3. Smartpay からのリクエストを処理する

受信するイベント通知の種類に応じてイベントオブジェクトを読み取るようにエンドポイントを設定する必要があります。Smartpay は、イベントを JSON ペイロードを持つ POST リクエストの一部として加盟店様の Webhook エンドポイントに送信します。

イベントオブジェクトを確認する

各イベントは、idtype、および関連する Smartpay リソースが data の下にネストされたイベントオブジェクトとして構成されています (データの構造は、イベントが関連するオブジェクトであるため、イベントの種類によって異なります)。詳細については、イベントオブジェクトを参照してください。エンドポイントは、イベントの種類を確認し、各イベントのペイロードを解析する必要があります。

{
  "id": "evt_test_yETRfprzCxFsSJaJZPIENt",
  "object": "event",
  "createdAt": 1664522079584,
  "test": true,
  "eventData": {
    "type": "order.authorized",
    "version": "2022-02-18",
    "data": {...}
  }
}

レスポンスコード 200 を返す

エンドポイントは、タイムアウトを引き起こす可能性がある複雑なロジックより前に、すばやく成功ステータスコード ( 200) を返す必要があります。

以下のサンプルコードでは、Webhook の署名を確認し (手順 5を参照)、HTTP ステータスコード 200 を返します。これらのサンプルコードは GitHub でも参照していただけます。

package main

import (
	"bytes"
	"encoding/json"
	"log"
	"io/ioutil"
	"net/http"
	"github.com/smartpay-co/sdk-go"
)

func webhook(w http.ResponseWriter, r *http.Request) {
	signature := r.Header.Get("Smartpay-Signature")
	calculatedSignature := r.Header.Get("Calculated-Signature")

	if signature != calculatedSignature {
		log.Println("Signature verification failed.", signature, calculatedSignature)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	var bodyBytes []byte
	var err error

	if r.Body != nil {
		bodyBytes, err = ioutil.ReadAll(r.Body)
		if err != nil {
			log.Printf("Body reading error: %v", err)
			return
		}
		defer r.Body.Close()
	}

	log.Printf("Headers: %+v\n", r.Header)

	if len(bodyBytes) > 0 {
		var prettyJSON bytes.Buffer
		if err = json.Indent(&prettyJSON, bodyBytes, "", "\t"); err != nil {
			log.Printf("JSON parse error: %v", err)
			return
		}
		log.Println(string(prettyJSON.Bytes()))
	} else {
		log.Printf("Body: No Body Supplied\n")
	}

	w.Write([]byte("OK"))
}

func main() {
	mux := http.NewServeMux()

	webhookHandler := http.HandlerFunc(webhook)
	mux.Handle("/", smartpay.CalculateWebhookSignatureMiddleware("YOUR_SIGNING_KEY", webhookHandler))

	log.Print("Listening on :3000...")
	err := http.ListenAndServe("0.0.0.0:3000", mux)
	log.Fatal(err)
}
const crypto = require("crypto");
const express = require("express");
const createError = require("http-errors");

const Smartpay = require("@smartpay/sdk-node").default; // The Nodejs SDK

const app = express();

const secret = "YOUR_SIGNING_KEY";

const log = new Map();

app.use(
  express.json({
    verify: Smartpay.expressWebhookMiddleware(secret),
  })
);

app.post("/webhooks", async (req, res, next) => {
  const event = req.body;
  const signature = req.headers["smartpay-signature"];
  const calculatedSignature = req.headers["calculated-signature"];

  console.log(req.headers);
  console.log(event);
  console.log(calculatedSignature);

  if (signature === calculatedSignature) {
    const key = req.headers["smartpay-event-id"];
    const existing = log.get(key) || {
      count: 0,
    };

    console.log(`key: ${key} count: ${existing.count}`);

    if (existing && existing.count >= 2) {
      log.delete(key);

      res.send("");

      return;
    }

    log.set(key, { count: existing.count + 1 });
  }

  next(createError.BadRequest());
});

app.listen(3000, "0.0.0.0", () =>
  console.log("Node server listening on port 3000!")
);
<?php

require __DIR__ . '/./vendor/autoload.php';

use Tuupola\Base62;

$base62 = new Base62(["characters" => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789']);
$secret = 'YOUR_SIGNING_KEY';

$app = new \Slim\App;

$app->map(['get', 'post'], '/', function ($request, $response, $args) use (&$base62, &$secret) {
    $fileName = trim($request->getUri()->getBasePath(), '/');

    if ($fileName == 'webhooks') {

        $headers = $request->getHeaders();
        $smartpaySignature = $request->getHeader('Smartpay-Signature')[0];
        $smartpaySignatureTimestamp = $request->getHeader('Smartpay-Signature-Timestamp')[0];

        if ($smartpaySignature && $smartpaySignatureTimestamp) {
            $body = $request->getBody();
            $parsedBody = $request->getParsedBody();

            $calculatedSignature = hash_hmac('sha256', $smartpaySignatureTimestamp . "." . $body, $base62->decode($secret));

            print_r($calculatedSignature);
            print_r($headers);
            print_r($parsedBody);

            if ($smartpaySignature == $calculatedSignature) {
                return $response;
            }
        }

        return $response->withStatus(400);
    } else {
        return $response->withStatus(404);
    }
});

$app->run();
import os

from flask import Flask, request
from smartpay import Smartpay

SIGNING_SECRET = 'YOUR_SIGNING_KEY'
SECRET_KEY = os.environ.get('SECRET_KEY', '<YOUR_SECRET_KEY>')
PUBLIC_KEY = os.environ.get('PUBLIC_KEY', '<YOUR_PUBLIC_KEY>')

smartpay = Smartpay(SECRET_KEY, public_key=PUBLIC_KEY)

app = Flask(__name__, static_url_path='')

root = '../client/build'


@app.route("/webhooks", methods=['POST'])
def webhooks():
    signature = request.headers['smartpay-signature']

    verified = smartpay.verify_webhook_signature(data=request.get_data(), secret=SIGNING_SECRET, signature=signature)

    if verified:
        print("processing webhook event")
        return ''

    return '', 400
require 'sinatra'
require 'base_x'

set :port, 3000

secret = 'YOUR_SIGNING_KEY'

helpers do
  def request_headers
    env.inject({}){|acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc}
  end
end

post '/webhooks' do
  content_type :json

  signature = request_headers["smartpay_signature"]
  signature_timestamp = request_headers["smartpay_signature_timestamp"]

  raw_body = request.body.read
  body = JSON.parse raw_body

  p BaseX::Base62ULD.decode(secret).bytes

  hmac = OpenSSL::HMAC.new(BaseX::Base62ULD.decode(secret), OpenSSL::Digest::SHA256.new)
  hmac.update signature_timestamp
  hmac.update '.'
  hmac.update raw_body
  calculated_signature = hmac.hexdigest

  p request_headers
  p body
  p calculated_signature

  if signature == calculated_signature
    return '', 200
  end

  return '', 400
rescue => err
  if err.respond_to?(:response)
    err.response.body
  else
    raise err
  end
end

組み込みの再試行

Smartpay の Webhook には、3xx、4xx、または 5xx のレスポンスステータスコードの再試行があらかじめ組み込まれています。Webhook リクエストに 200 以外で 応答した場合、Smartpay はエクスポネンシャルバックオフ方式でリクエストの再試行を続けます。

4. Webhook エンドポイントを登録する

新しく作成した Webhook エンドポイントを Smartpay に登録し、登録するイベントを受信します。これは、Webhook エンドポイント作成 API エンドポイントを利用して行います。

5. Webhook のセキュリティーを確保する (推奨)

Webhook 署名を使用して、Webhook リクエストを作成したのは Smartpay であり、 Smartpay を模倣したサーバーからのものではないことを確認します。

Smartpay は、各イベントのSmartpay-Signature ヘッダーに署名を含めることにより、加盟店様のエンドポイントに送信する Webhook イベントへの署名を行います。これにより、サードパーティではなく Smartpay が送信したイベントであることを確認していただけます。署名は、弊社の SDK を使って、または貴社ソリューションを使って手動で確認していただくことも可能です。

署名を確認する前に、エンドポイントのsigningSecret を取得する必要があります。Smartpay は、各エンドポイントに一意の signingSecret を生成し、Webhook エンドポイントを登録する際 (手順 4 を参照) や、以前登録した Webhook エンドポイントを取得する際に返します。