ブログ

AWS で Stripe Webhook を受信してみる

最近、仕事で Stripe の Webhook 受信環境を作成したいと思い、試しに Amazon API Gateway + AWS Lambda で作成してみました。

Stripe はクレジットカード全般の処理を SaaS 的に準備してくれているサービスで、何かしらWebサービスにクレジット決済の機能を組み込もうと思った時に便利です。

今回はその中でも、Webhook を利用したイベント連携の機能を試してみました。
例えば、「顧客が解約したタイミングで何か連携したい」といった特定イベントから Webhook で自社のサービスに連携することが可能な機能です。

AWS側の設定 | AWS で Stripe Webhook を受信してみる

AWS Cloud Development Kit (AWS CDK) で作成しています。サンプルソースは下記です。
カスタムドメイン名で受信するようにしています。

bin 配下

import { StripeEventsStack } from '../lib/StripeEvents';

new StripeEventsStack(app, 'StripeEventsStack', {
  env: { region: 'ap-northeast-1', account: 'xxxxxx' },
  environment: environment,
  domainNameHost: `stripe-${environment}`,
  domainNameBase: `xxxxx.com`,
  hostedZone: 'Z1Qxxxxxxxx',
});

lib 配下

import * as path from 'path';
import { Construct } from 'constructs';
import {
  Stack,
  StackProps,
  aws_lambda as lambda,
  aws_lambda_nodejs as lambdaNodejs,
  aws_apigateway as apigateway,
  aws_certificatemanager as acm,
  aws_route53 as route53,
  aws_route53_targets as route53Targets,
  Duration,
} from 'aws-cdk-lib';

type Props = {
  environment: string;
  domainNameHost: string;
  domainNameBase: string;
  hostedZone: string;
} & StackProps;

export class StripeEventsStack extends Stack {
  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props);
    const { environment, domainNameHost, domainNameBase, hostedZone } = props;

    const lambdaStripeEvents = new lambdaNodejs.NodejsFunction(this, 'lambda', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: path.join(__dirname, '../lambda/stripeEvents/handler.ts'),
      handler: 'handler',
      environment: {},
      timeout: Duration.seconds(300),
    });

    const stripeEventApi = new apigateway.LambdaRestApi(
      this,
      'stripeEventApi',
      {
        handler: lambdaStripeEvents,
        proxy: false,
        deployOptions: {
          loggingLevel: apigateway.MethodLoggingLevel.INFO,
        },
        defaultMethodOptions: {
          authorizationType: apigateway.AuthorizationType.NONE,
        },
        domainName: {
          domainName: `${domainNameHost}.${domainNameBase}`,
          certificate: new acm.Certificate(this, 'Certificate', {
            domainName: `*.${domainNameBase}`,
            validation: acm.CertificateValidation.fromDns(),
          }),
        },
      }
    );
    const stripeWebhook = stripeEventApi.root.addResource('webhook');
    stripeWebhook.addMethod('POST');

    const zone = route53.HostedZone.fromLookup(this, 'baseZone', {
      domainName: domainNameBase,
    });

    new route53.ARecord(this, 'ARecod', {
      zone: zone,
      recordName: domainNameHost,
      target: route53.RecordTarget.fromAlias(
        new route53Targets.ApiGateway(stripeEventApi)
      ),
    });
  }
}

Lambda ソース

export const handler = async (event: any) => {
  console.log('event:', event);

  return {
    isBase64Encoded: false,
    statusCode: 200,
    headers: { 'test-header-res-key': 'test-header-res-Value' },
    body: '...',
  };
};

※ Stripe のベストプラクティスを見ますと、Stripe 側の署名を検証した方が良いので、あくまでサンプル出力とお考えください。

Stripe 上で設定 | AWS で Stripe Webhook を受信してみる

Stripe でエンドポイントを登録し、受信イベントをAPIレベルで選択します。
今回は Stripe のテスト環境で試してみました。

まずは、上記 AWS で作成したエンドポイントのURLを作成します。
「 xxxxx.com/webhook 」のようになります。

次に連携するイベントを選択します。

例えば Stripe 上で、サブスクの新しい購入者が発生したことを表す
「customer.subscription.created」を選択してみます。

上記を設定して、テストで適当な customer にサブスクリプションを紐づけてみましょう。
クレジットカードは適当なテスト用の番号で大丈夫です。

参考) https://stripe.com/docs/testing

結果確認 | AWS で Stripe Webhook を受信してみる

実行すると、Stripe の 開発者 > Webhook の画面から、成功/失敗が確認できます。

下記のような形で、Lambda 側でも
event の body で Stripe の subscription が下記の形で取得できてることを確認できました。

{
    "id": "evt_xxxxxx",
    "object": "event",
    "api_version": "2020-08-27",
    "created": 1643362950,
    "data": {
        "object": {
            "id": "sub_xxxxxx",
            "object": "subscription",
            "application_fee_percent": null,
            "automatic_tax": {
                "enabled": false
            },
            "billing_cycle_anchor": 1643362948,
            "billing_thresholds": null,
            "cancel_at": null,
            "cancel_at_period_end": false,
            "canceled_at": null,
            "collection_method": "charge_automatically",
            "created": 1643362948,
            "current_period_end": 1646041348,
            "current_period_start": 1643362948,
            "customer": "cus_xxxxxx",
            "days_until_due": null,
            "default_payment_method": null,
            "default_source": null,
            "default_tax_rates": [],
            "discount": null,
            "ended_at": null,
            "items": {
                "object": "list",
                "data": [
                    {
                        "id": "si_xxxxxx",
                        "object": "subscription_item",
                        "billing_thresholds": null,
                        "created": 1643362948,
                        "metadata": {},
                        "plan": {
                            "id": "price_xxxxxx",
                            "object": "plan",
                            "active": true,
                            "aggregate_usage": null,
                            "amount": 2000,
                            "amount_decimal": "2000",
                            "billing_scheme": "per_unit",
                            "created": 1642496340,
                            "currency": "jpy",
                            "interval": "month",
                            "interval_count": 1,
                            "livemode": false,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_xxxxxx",
                            "tiers_mode": null,
                            "transform_usage": null,
                            "trial_period_days": null,
                            "usage_type": "licensed"
                        },
                        "price": {
                            "id": "price_xxxxxx",
                            "object": "price",
                            "active": true,
                            "billing_scheme": "per_unit",
                            "created": 1642496340,
                            "currency": "jpy",
                            "livemode": false,
                            "lookup_key": null,
                            "metadata": {},
                            "nickname": null,
                            "product": "prod_xxxxxx",
                            "recurring": {
                                "aggregate_usage": null,
                                "interval": "month",
                                "interval_count": 1,
                                "trial_period_days": null,
                                "usage_type": "licensed"
                            },
                            "tax_behavior": "unspecified",
                            "tiers_mode": null,
                            "transform_quantity": null,
                            "type": "recurring",
                            "unit_amount": 2000,
                            "unit_amount_decimal": "2000"
                        },
                        "quantity": 1,
                        "subscription": "sub_xxxxxxxxxxxxxxxxxx",
                        "tax_rates": []
                    }
                ],
                "has_more": false,
                "total_count": 1,
                "url": "/v1/subscription_items?subscription=sub_xxxxxx"
            },
            "latest_invoice": "in_xxxxxx",
            "livemode": false,
            "metadata": {},
            "next_pending_invoice_item_invoice": null,
            "pause_collection": null,
            "payment_settings": {
                "payment_method_options": null,
                "payment_method_types": null
            },
            "pending_invoice_item_interval": null,
            "pending_setup_intent": null,
            "pending_update": null,
            "plan": {
                "id": "xxxxxx",
                "object": "plan",
                "active": true,
                "aggregate_usage": null,
                "amount": 2000,
                "amount_decimal": "2000",
                "billing_scheme": "per_unit",
                "created": 1642496340,
                "currency": "jpy",
                "interval": "month",
                "interval_count": 1,
                "livemode": false,
                "metadata": {},
                "nickname": null,
                "product": "xxxxxx",
                "tiers_mode": null,
                "transform_usage": null,
                "trial_period_days": null,
                "usage_type": "licensed"
            },
            "quantity": 1,
            "schedule": null,
            "start_date": 1643362948,
            "status": "active",
            "transfer_data": null,
            "trial_end": null,
            "trial_start": null
        }
    },
    "livemode": false,
    "pending_webhooks": 1,
    "request": {
        "id": "xxxxxx",
        "idempotency_key": "xxxxxx"
    },
    "type": "customer.subscription.created"
}

最後に ─

いかがでしたでしょうか。

Stripe は便利で多機能なので、色々と使えそうです。
最後までお読みいただきありがとうございました。

元記事発行日: 2022年06月14日、最終更新日: 2022年06月28日