<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Michael Tedder</title>
    <description>The latest articles on Forem by Michael Tedder (@falken42).</description>
    <link>https://forem.com/falken42</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F571956%2F6bcaa3e9-48b2-40f9-b347-eadfba523ec5.jpeg</url>
      <title>Forem: Michael Tedder</title>
      <link>https://forem.com/falken42</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/falken42"/>
    <language>en</language>
    <item>
      <title>First Time with Stripe: Fully Serverless Ticket Sales</title>
      <dc:creator>Michael Tedder</dc:creator>
      <pubDate>Sun, 30 Jan 2022 05:08:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/first-time-with-stripe-fully-serverless-ticket-sales-329a</link>
      <guid>https://forem.com/aws-builders/first-time-with-stripe-fully-serverless-ticket-sales-329a</guid>
      <description>&lt;p&gt;[ 本ブログの&lt;a href="https://dev.to/aws-builders/chu-metenostripe-wan-quan-nisabaresunotiketutofan-mai-2780"&gt;日本語版はこちら&lt;/a&gt;です。 ]&lt;/p&gt;

&lt;p&gt;This blog entry was a part of the &lt;a href="https://qiita.com/advent-calendar/2021/stripe" rel="noopener noreferrer"&gt;Stripe Advent Calendar 2021&lt;/a&gt;, originally posted in Japanese on December 3rd, 2021.&lt;/p&gt;




&lt;p&gt;Hello everyone!  My name is Michael Tedder and I'm one of the main organizers for Tokyo Demo Fest.  I've been using AWS for over 9 years, and help run the JAWS-UG Sapporo AWS User Group in Japan, as well as assist with organizing larger AWS community events such as &lt;a href="https://jawsdays2021.jaws-ug.jp/" rel="noopener noreferrer"&gt;JAWS DAYS 2021&lt;/a&gt; and &lt;a href="https://jawspankration2021.jaws-ug.jp/" rel="noopener noreferrer"&gt;JAWS PANKRATION 2021&lt;/a&gt;.  I've also been an AWS Community Builder since 2020.&lt;/p&gt;

&lt;p&gt;In this post, I'll be explaining how we implemented online ticket sales using Stripe on AWS for this year's Tokyo Demo Fest 2021.  While this post does focus on the technical side of things, I'll try to cover everything necessary as if this is the first time you're using Stripe.  All the sample code presented here is in Node.js 14.x.&lt;/p&gt;

&lt;h2&gt;
  
  
  About Tokyo Demo Fest
&lt;/h2&gt;

&lt;p&gt;Tokyo Demo Fest (also called "TDF") is currently the only active demoparty in Japan.  At a demoparty, people who are interested in computer programming and art gather together -- not only from Japan but also from all over the world -- and hold competitions and seminars about demo productions.&lt;/p&gt;

&lt;p&gt;For all past TDFs we had previously been using PayPal for our ticket sales, but this year we finally made the switch over to Stripe.  By using Stripe Checkout, we were able to get an implementation working in as little as a few hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Design
&lt;/h2&gt;

&lt;p&gt;Below is a diagram showing the overall design of the system.  Note that as this is post is only about Stripe and ticket sales, other parts of the TDF system (such as live streaming) have been omitted for clarity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdi5bdgrkerfx41kzi69h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdi5bdgrkerfx41kzi69h.png" alt="Serverless Stripe Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visitors first access the &lt;a href="https://tokyodemofest.jp/" rel="noopener noreferrer"&gt;TDF Website&lt;/a&gt;, published through Amplify.  After choosing which of the two types of tickets to purchase, the visitor is then forwarded to Stripe Checkout.  Once payment has been completed, Stripe calls out to our webhook, and we then send a votekey by email to the address which was entered during checkout.  Visitors then use the votekey to access and login to the &lt;a href="https://github.com/Gargaj/wuhu" rel="noopener noreferrer"&gt;Wuhu Party System&lt;/a&gt; which is running on ECS/Fargate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Products in Stripe
&lt;/h2&gt;

&lt;p&gt;This year's TDF has two different types of tickets available for purchase.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visitor Ticket (1,000 yen)&lt;/li&gt;
&lt;li&gt;Bronze Supporter (10,000 yen, includes T-shirt + shipping)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As the Visitor Ticket requires nothing special, it is simply added directly as a Product in the Stripe dashboard.&lt;/p&gt;

&lt;p&gt;The Bronze Supporter ticket has a different price than the Visitor Ticket, and so requires registering a different Product in Stripe.  However, as the Bronze Supporter ticket includes a T-shirt (with a selectable size of S/M/L/XL), we also need to add separate Products for each T-shirt size, resulting in a total of four more Products, and all with a price of zero (0 yen) as the T-shirt is included in the ticket price.  Since the T-shirt is a physical item and needs to be shipped, we will also require the purchaser to enter their address during Checkout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx84n72dkdyacy92hpz7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx84n72dkdyacy92hpz7l.png" alt="TDF Stripe Products"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to require the address fields to be shown on the Checkout page, a Shipping rate is required.  Since shipping is included in our Bronze Supporter ticket price, the amount for this is set to zero (0 yen) here as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fve3m61fk3cimm5851dhu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fve3m61fk3cimm5851dhu.png" alt="TDF Stripe Shipping"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe API Key Security
&lt;/h2&gt;

&lt;p&gt;Your Stripe API key needs to be kept secret, and it's important to ensure you never use it in your source code or accidentally commit it to source control.  I typically use the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="noopener noreferrer"&gt;Parameter Store&lt;/a&gt; functionality of AWS Systems Manager to store secrets when using them with Lambda functions, and I'll be doing the same here.&lt;/p&gt;

&lt;p&gt;Stripe has various keys and values -- such as your API key, Webhook secret, and Price IDs -- that all need to be kept secret.  You could store each of these into individual SecureStrings, but as the Standard parameter can store up to 4KB of data, it's much easier to encode all of the necessary keys and values into a block of JSON, and store the JSON as a single SecureString in Parameter Store.&lt;/p&gt;

&lt;p&gt;I've hidden the values of our keys below, but this is the JSON we use to store our data for Stripe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"stripe_api_secret_key":"sk_test_51JU2XXXXXXXXXXXX",
"webhook_signing_secret":"whsec_TqW4TXXXXXXXXXXXX",
"product_visitor_ticket":"price_1JX1zXXXXXXXXXXXX",
"product_bronze_ticket":"price_1JrKaXXXXXXXXXXXX",
"product_tshirt_s":"price_1JrKlXXXXXXXXXXXX",
"product_tshirt_m":"price_1JrKmXXXXXXXXXXXX",
"product_tshirt_l":"price_1JrKnXXXXXXXXXXXX",
"product_tshirt_xl":"price_1JrKoXXXXXXXXXXXX",
"success_url":"https://tokyodemofest.jp/success.html",
"cancel_url":"https://tokyodemofest.jp#registration",
"shipping_rate":"shr_1JrKNXXXXXXXXXXXX",
"shipping_countries":"US,JP,IE,GB,NO,SE,FI,RU,PT,ES,FR,DE,CH,IT,PL,CZ,AT,HU,BA,BY,UA,RO,BG,GR,AU,NZ,KR,TW,IS"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to read the JSON from Parameter Store, we'll be using the &lt;code&gt;aws-sdk&lt;/code&gt; package included with Node.js Lambda function runtime.  Once we have the JSON configuration read, we'll pass the Stripe API key to the Stripe API initialization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const loadConfig = async function() {
  const aws = require('aws-sdk');
  const ssm = new aws.SSM();
  const res = await ssm.getParameter( { Name: '/tdf/config-stripe', WithDecryption: true } ).promise();
  return JSON.parse(res.Parameter.Value);
}

exports.handler = async (event) =&amp;gt; {
  const config = await loadConfig();
  const stripe = require('stripe')(config.stripe_api_secret_key);
  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding the HTML Purchase Buttons
&lt;/h2&gt;

&lt;p&gt;Below are the ticket purchase buttons we've used for this year's TDF.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijk8naj9ixj4uoxo2yxh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijk8naj9ixj4uoxo2yxh.png" alt="TDF Visitor Ticket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6pgpv0vlypkn1q8pif4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6pgpv0vlypkn1q8pif4.png" alt="TDF Bronze Supporter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that even though there are two different ticket types, both of them POST through the same endpoint, as you can see in the HTML below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz90o2ltnbje9dcyddxtt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz90o2ltnbje9dcyddxtt.png" alt="TDF Ticket HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The way the Lambda function determines the difference between the ticket types is through the &lt;code&gt;&amp;lt;input type="hidden" name="type" value="bronze"&amp;gt;&lt;/code&gt; tag.  When &lt;code&gt;type=bronze&lt;/code&gt; is set, the T-shirt size is also available through the &lt;code&gt;tshirt&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;I'll explain more on how these values direct to the proper items within Stripe Checkout below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating a Stripe Checkout URL
&lt;/h2&gt;

&lt;p&gt;Once a visitor clicks on a ticket purchase button from the TDF website, we direct them to Stripe Checkout using a specifically generated URL.  This generated URL contains all of the information needed to perform the checkout, including what products to purchase, and any other required information in order to make the purchase (such as whether a physical address is needed or not).  The Stripe SDK automatically handles the generation of this URL, and we forward the browser to it using &lt;code&gt;HTTP 303&lt;/code&gt; (See Other).&lt;/p&gt;

&lt;p&gt;In order to generate a Checkout session URL and forward the browser to it, we use the following code in our Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exports.handler = async (event) =&amp;gt; {
  const config = await loadConfig();
  const stripe = require('stripe')(config.stripe_api_secret_key);

  const session = await stripe.checkout.sessions.create( {
    line_items: /* TODO */,
    payment_method_types: [ 'card' ],
    mode: 'payment',
    success_url: config.success_url,
    cancel_url: config.cancel_url
  } );

  const response = {
    statusCode: 303,
    headers: {
      'Location': session.url
    }
  };

  return response;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;line_items&lt;/code&gt; in the session data specifies which products are to be purchased.  We look at the data POSTed from the browser to the Lambda, and change what product should be added to the &lt;code&gt;line_items&lt;/code&gt; value.  Note that as payloads from the browser can be Base64 encoded, we check for this and decode as necessary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  if (event.body) {
    let payload = event.body;
    if (event.isBase64Encoded)
      payload = Buffer.from(event.body, 'base64').toString();

    const querystring = require('querystring');
    const res = querystring.parse(payload);
    if ((res.type) &amp;amp;&amp;amp; (res.type == 'bronze')) {
      // ...
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this is a Visitor Ticket, we simply add it's product to the item array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  let items = [ {
    price: config.product_visitor_ticket,
    quantity: 1
  } ];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting Stripe Checkout page when purchasing a Visitor Ticket looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4po7ld6thtf25au7br7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4po7ld6thtf25au7br7h.png" alt="Stripe Checkout Visitor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the Bronze Supporter ticket, we match up the selected T-shirt size with the Price ID, and add two Products (both the ticket and the T-shirt) into the &lt;code&gt;line_items&lt;/code&gt; array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  let tshirt_type = config.product_tshirt_s;

  if (res.tshirt) {
    if (res.tshirt == 's') tshirt_type = config.product_tshirt_s;
    if (res.tshirt == 'm') tshirt_type = config.product_tshirt_m;
    if (res.tshirt == 'l') tshirt_type = config.product_tshirt_l;
    if (res.tshirt == 'xl') tshirt_type = config.product_tshirt_xl;
  }

  items = [ {
    price: config.product_bronze_ticket,
    quantity: 1
  }, {
    price: tshirt_type,
    quantity: 1
  } ];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we also need to require the entry of a physical address since the Bronze Supporter ticket includes the T-shirt.  In the Checkout session data, this is specified with both &lt;code&gt;shipping_rates&lt;/code&gt; and the supported countries (which countries you want to allow shipping to) via the &lt;code&gt;allowed_countries&lt;/code&gt; in the &lt;code&gt;shipping_address_collection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Putting it all together, the resulting code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  const session = await stripe.checkout.sessions.create( {
    line_items: [ {
      price: config.product_bronze_ticket,
      quantity: 1
    }, {
      price: tshirt_type,
      quantity: 1
    } ],
    payment_method_types: [ 'card' ],
    mode: 'payment',
    success_url: config.success_url,
    cancel_url: config.cancel_url,
    shipping_rates: [ config.shipping_rate ],
    shipping_address_collection: {
      allowed_countries: config.shipping_countries.split(',')
    }
  } );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the shipping fields added to the session data, you can now see that the physical address entry fields are now visible on the Stripe Checkout form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxl82a4iy6uu47hot3lml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxl82a4iy6uu47hot3lml.png" alt="Stripe Checkout Bronze"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just by implementing these few lines of code, you're now able to accept payments through Stripe Checkout.  In the next session I'll cover how to configure a Webhook through Stripe, which can be used to send email or perform other processing in response to events from Stripe, such as once a payment has successfully completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing a Stripe Webhook
&lt;/h2&gt;

&lt;p&gt;As mentioned above, once a ticket purchase has successfully completed, TDF needs to email the ticket information (containing the login information to the Wuhu Party System) to the purchaser.  This is accomplished by using a separate Lambda function (with access via API Gateway) as a Stripe Webhook.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fte3z7rdltkaagb64lqk2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fte3z7rdltkaagb64lqk2.png" alt="TDF Stripe Webhook"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since implementing a webhook will be different depending on the whatever functionality you will need, I'll introduce up to what's required to properly decode the POST data from Stripe.&lt;/p&gt;

&lt;p&gt;The first important thing to notice is that as your webhook URL is public, anyone can potentially access it.  Stripe will always call your webhook with a signature included in the HTTP header, and you can use the Webhook secret key to verify that the payload data is correct and valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exports.handler = async (event) =&amp;gt; {
  // require Stripe signature in header
  if (!event.headers['stripe-signature']) {
    console.log('no Stripe signature received in header, returning 400 Bad Request');
    return {
      statusCode: 400
    };
  }

  const sig = event.headers['stripe-signature'];

  // require an event body
  if (!event.body) {
    console.log('no event body received in POST, returning 400 Bad Request');
    return {
      statusCode: 400
    };
  }

  // decode payload
  let payload = event.body;
  if (event.isBase64Encoded)
    payload = Buffer.from(event.body, 'base64').toString();

  // construct a Stripe Webhook event
  const config = await loadConfig();
  const stripe = require('stripe')(config.stripe_api_secret_key);

  try {
    let ev = stripe.webhooks.constructEvent(payload, sig, config.webhook_signing_secret);
  } catch (err) {
    console.log('error creating Stripe Webhook event');
    console.log(err);
    return {
      statusCode: 400
    };
  }

  // ...TODO...

  return {
    statusCode: 200
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you've successfully constructed the Stripe Webhook event from the payload, signature, and signing secret, you can examine the event type to determine how the status changed.  For simple payments with Checkout, handling the following three events is typical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;checkout.session.completed&lt;/code&gt; : A purchase through Stripe Checkout has been completed.  Depending on the payment method, the actual transaction may not have completed yet.  For credit cards, the &lt;code&gt;payment_status&lt;/code&gt; is typically set to &lt;code&gt;paid&lt;/code&gt;, which means the transaction has finished.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkout.session.async_payment_succeeded&lt;/code&gt; : An incomplete purchase that was notified through a previous &lt;code&gt;completed&lt;/code&gt; event has succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkout.session.async_payment_failed&lt;/code&gt; : An incomplete purchase that was notified through a previous &lt;code&gt;completed&lt;/code&gt; event has failed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In order to implement the functionality for these three events, we mostly follow the same code as shown in the &lt;a href="https://stripe.com/docs/payments/checkout/fulfill-orders" rel="noopener noreferrer"&gt;Stripe Sample Code&lt;/a&gt; documentation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const createOrder = async function(session) {
  // we (TDF) don't need to do anything here
}

const fulfillOrder = async function(session) {
  // send ticket info to customer by email
  console.log('customer email is: ' + session.customer_details.email);
}

const emailCustomerAboutFailedPayment = async function(session) {
  // send email about failed payment
}

exports.handler = async (event) =&amp;gt; {
  // ...
  const session = ev.data.object;
  switch (ev.type) {
    case 'checkout.session.completed':
      // save an order in your database, marked as 'awaiting payment'
      await createOrder(session);

      // check if the order is paid (e.g., from a card payment)
      // a delayed notification payment will have an `unpaid` status
      if (session.payment_status === 'paid') {
        await fulfillOrder(session);
      }
      break;

    case 'checkout.session.async_payment_succeeded':
      // fulfill the purchase...
      await fulfillOrder(session);
      break;

    case 'checkout.session.async_payment_failed':
      // send an email to the customer asking them to retry their order
      await emailCustomerAboutFailedPayment(session);
      break;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After implementing the three functions &lt;code&gt;createOrder()&lt;/code&gt;, &lt;code&gt;fulfillOrder()&lt;/code&gt;, and &lt;code&gt;emailCustomerAboutFailedPayment()&lt;/code&gt;, your Webhook is complete.&lt;/p&gt;

&lt;p&gt;In the event of an error or a non &lt;code&gt;HTTP 2xx&lt;/code&gt; response in your Webhook, Stripe will automatically wait and handle retries as necessary.  For more details, please see the &lt;a href="https://stripe.com/docs/webhooks/best-practices" rel="noopener noreferrer"&gt;Stripe Webhook Best Practices&lt;/a&gt; documentation.&lt;/p&gt;

&lt;p&gt;...and that's it!  You've just finished everything necessary to process payments with Stripe Checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging Multiple Endpoints with API Gateway Custom Domains
&lt;/h2&gt;

&lt;p&gt;With the implementation presented here, there are two endpoints: the Stripe Checkout URL generation, and the Stripe Webhook.  It's completely fine to use the generated endpoint URL that you're given by API Gateway ( &lt;code&gt;https://7q6f1e5os2.execute-api.ap-northeast-1...&lt;/code&gt; ), but configuring this to be something like &lt;code&gt;stripe.tokyodemofest.jp&lt;/code&gt; as a subdomain within your domain looks better to the end user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpvoy2tl8f32h3ipbkm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpvoy2tl8f32h3ipbkm9.png" alt="API Gateway Custom Domain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the above settings, we've configured both the &lt;code&gt;checkout&lt;/code&gt; and &lt;code&gt;fulfill&lt;/code&gt; Lambda functions and API Gateways into a single Custom Domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts on Using Stripe for the First Time for TDF
&lt;/h2&gt;

&lt;p&gt;To be honest, implementing Stripe Checkout on serverless was incredibly easy.  The amount of code needed is tiny, and allows anyone to accept payments from their website in just a few lines of code.&lt;/p&gt;

&lt;p&gt;Additionally, the Stripe Dashboard shows full HTTP request and response logs along with helpful and detailed error messages that make debugging your Checkout or Webhook implementation easy.&lt;/p&gt;

&lt;p&gt;Up until this point we've been fighting with keeping PayPal active for many years that my only regret is not switching over to using Stripe sooner.  :)&lt;/p&gt;

&lt;p&gt;Thank you for reading this post, hopefully it was helpful to someone.  If you should have any questions, feel free to ask in the comment section below!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>stripe</category>
      <category>aws</category>
      <category>demoscene</category>
    </item>
    <item>
      <title>初めてのStripe: 完全にサーバーレスのチケット販売</title>
      <dc:creator>Michael Tedder</dc:creator>
      <pubDate>Thu, 02 Dec 2021 15:19:16 +0000</pubDate>
      <link>https://forem.com/aws-builders/chu-metenostripe-wan-quan-nisabaresunotiketutofan-mai-2780</link>
      <guid>https://forem.com/aws-builders/chu-metenostripe-wan-quan-nisabaresunotiketutofan-mai-2780</guid>
      <description>&lt;p&gt;[ This post is available &lt;a href="https://dev.to/aws-builders/first-time-with-stripe-fully-serverless-ticket-sales-329a"&gt;in English here&lt;/a&gt;. ]&lt;/p&gt;

&lt;p&gt;本ブログは2021年&lt;a href="https://qiita.com/advent-calendar/2021/stripe" rel="noopener noreferrer"&gt;Stripe Advent Calendar 2021&lt;/a&gt;の12月3日分のエントリーです。&lt;/p&gt;




&lt;p&gt;皆さん、こんにちは！Tokyo Demo Fest実行委員のテッダー　マイケルです。AWSとの9年間の開発経験を活かしながら、JAWS-UG札幌支部とJAWS-UGのコミュニティイベント（&lt;a href="https://jft2019.jaws-ug.jp/" rel="noopener noreferrer"&gt;FESTA 2019&lt;/a&gt;、&lt;a href="https://jawsdays2021.jaws-ug.jp/" rel="noopener noreferrer"&gt;DAYS 2021&lt;/a&gt;、&lt;a href="https://jawspankration2021.jaws-ug.jp/" rel="noopener noreferrer"&gt;PANKRATION 2021&lt;/a&gt;）の運営を手伝いしています。2020年にAWSコミュニティビルダーに認定されました。&lt;/p&gt;

&lt;p&gt;今回はTokyo Demo Fest 2021のチケット販売のため、AWS上で初めてStripeを実装した話を詳しくご紹介します。Stripeをまだ触っていない方でもわかりやすく伝えたいと思います。サンプルコードの言語はNode.js 14.xになります。&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokyo Demo Festについて
&lt;/h2&gt;

&lt;p&gt;Tokyo Demo Fest (略: TDF)は日本で唯一のデモパーティです。デモパーティは、コンピュータを用いたプログラミングとアートに興味のある人々が日本のみならず、世界中から一堂に会し、デモ作品のコンペティション(コンポ)やセミナーなどを行います。また、イベント開催中は集まった様々な人たちとの交流が深められます。&lt;/p&gt;

&lt;p&gt;デモについての解説は&lt;a href="https://tokyodemofest.jp/2016/?lang=ja#aboutdemo" rel="noopener noreferrer"&gt;こちらをご参照ください&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;過去のTDFのチケット販売はPayPalで行いましたが、今年からはようやくStripeに移行することができました。今回は実装がとても簡単なStripe Checkoutを利用し、実際のコーディングは数時間程度で対応できました。&lt;/p&gt;

&lt;h2&gt;
  
  
  システム設計
&lt;/h2&gt;

&lt;p&gt;こちらが全体図です。今回はStripeに限る話だけなので、ライブ配信周りなどは描かれてません。Stripeとその関係するシステムのみになります。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdi5bdgrkerfx41kzi69h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdi5bdgrkerfx41kzi69h.png" alt="Serverless Stripe Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visitorは最初にAmplifyで公開されている&lt;a href="https://tokyodemofest.jp/index_ja.html" rel="noopener noreferrer"&gt;TDFのWebサイト&lt;/a&gt;から入ります。2種類のチケットがあるのでどちらかを選択し、Stripe Checkoutへ移行します。決済完了時はStripeからのWebhookが呼び出され、チケット情報とVotekeyがStripeでの購入時に入力されたメールアドレスに送信されます。Visitorはメールで配布されたVotekeyを使い、&lt;a href="https://github.com/Gargaj/wuhu" rel="noopener noreferrer"&gt;Wuhuパーティシステム&lt;/a&gt; (ECS/Fargate)にログインできるようになります。&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe商品の作成
&lt;/h2&gt;

&lt;p&gt;今回のTDFではチケットを2種類販売しています。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visitor Ticket（1,000円）&lt;/li&gt;
&lt;li&gt;Bronze Supporter (10,000円、Tシャツ無料配送込み)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Visitor TicketはStripeでは1つの「商品」になるので、簡単ですね。&lt;/p&gt;

&lt;p&gt;Bronze SupporterはVisitor Ticketと金額が違うため別の商品が必要ですが、Tシャツのサイズ（S/M/L/XL）の選択もあるので、サイズ別の4つの商品を作成しました。Tシャツはチケット代に含まれているので、それぞれのサイズの金額は「ゼロ円」に設定しています。そしてTシャツは配送になるため、購入時にVisitorの住所入力が必要です。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu418fcq355zfmtxtnxqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu418fcq355zfmtxtnxqp.png" alt="TDF Stripe Products"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stripeで配送情報を入力させるためには「配送料金」を作成する必要があります。今回はTシャツの配送料もチケット代に含まれているため、こちらの金額も「ゼロ円」に設定しています。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvj9r6cp4jwpvlqd7jwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvj9r6cp4jwpvlqd7jwo.png" alt="TDF Stripe Shipping"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe APIキーのセキュリティ
&lt;/h2&gt;

&lt;p&gt;Stripe APIキーは秘密情報なので、ソースコードに書き込んだりすると情報漏えいの要因になります。私のいつもの実装パターンですが、Lambdaを使ってる時はAWS Systems Managerの&lt;a href="https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="noopener noreferrer"&gt;Parameter Store&lt;/a&gt;にAPIキー等を保存することにしています。&lt;/p&gt;

&lt;p&gt;APIキー、Webhookシークレット、商品のPriceID、配送料金IDなどは1つずつ別々のSecureStringに保存が可能ですが、実はStandardでも4KBまで保存が可能なので、すべての必要な情報をJSON化にし、1つの文字列として保存するのがとても楽です。&lt;/p&gt;

&lt;p&gt;以下、キーの内容は隠してますが、実際保存してるデータがこんな感じです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"stripe_api_secret_key":"sk_test_51JU2XXXXXXXXXXXX",
"webhook_signing_secret":"whsec_TqW4TXXXXXXXXXXXX",
"product_visitor_ticket":"price_1JX1zXXXXXXXXXXXX",
"product_bronze_ticket":"price_1JrKaXXXXXXXXXXXX",
"product_tshirt_s":"price_1JrKlXXXXXXXXXXXX",
"product_tshirt_m":"price_1JrKmXXXXXXXXXXXX",
"product_tshirt_l":"price_1JrKnXXXXXXXXXXXX",
"product_tshirt_xl":"price_1JrKoXXXXXXXXXXXX",
"success_url":"https://tokyodemofest.jp/success.html",
"cancel_url":"https://tokyodemofest.jp#registration",
"shipping_rate":"shr_1JrKNXXXXXXXXXXXX",
"shipping_countries":"US,JP,IE,GB,NO,SE,FI,RU,PT,ES,FR,DE,CH,IT,PL,CZ,AT,HU,BA,BY,UA,RO,BG,GR,AU,NZ,KR,TW,IS"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambdaが実行されてる際、このJSONをパラメータストアから読み込むためには Lambdaに内蔵されてる &lt;code&gt;aws-sdk&lt;/code&gt; を使います。Stripeの初期化にはAPIキーを渡すのが必要なので、SSMからconfigを引っ張ってから初期化を行います。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const loadConfig = async function() {
  const aws = require('aws-sdk');
  const ssm = new aws.SSM();
  const res = await ssm.getParameter( { Name: '/tdf/config-stripe', WithDecryption: true } ).promise();
  return JSON.parse(res.Parameter.Value);
}

exports.handler = async (event) =&amp;gt; {
  const config = await loadConfig();
  const stripe = require('stripe')(config.stripe_api_secret_key);
  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  HTML側の購入ボタン作成
&lt;/h2&gt;

&lt;p&gt;参考のため、今回のTDFのHTML側の購入ボタンを軽く紹介します。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg5o6ttt7wdnu9bnjuhdq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg5o6ttt7wdnu9bnjuhdq.png" alt="TDF Visitor Ticket"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08ukha0sn2r182dqwq24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08ukha0sn2r182dqwq24.png" alt="TDF Bronze Supporter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;チケットの種類が2つなのですが、実はPOSTするエンドポイントが同じです。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz90o2ltnbje9dcyddxtt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz90o2ltnbje9dcyddxtt.png" alt="TDF Ticket HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lambda側でチケットの違いをわかるためには &lt;code&gt;&amp;lt;input type="hidden" name="type" value="bronze"&amp;gt;&lt;/code&gt; で判断しています。そして &lt;code&gt;type=bronze&lt;/code&gt; の場合はTシャツサイズの &lt;code&gt;tshirt&lt;/code&gt; 指定もわかります。&lt;/p&gt;

&lt;p&gt;Stripe CheckoutにBronze SupporterチケットとTシャツの商品指定は次のセクションで紹介します。&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe CheckoutへのURL生成
&lt;/h2&gt;

&lt;p&gt;次はWebサイトでVisitorがチケット購入ボタンを押したら、Stripe Checkoutに移行させます。このURLには、どの商品を購入するとか、購入時に必要な情報（住所の入力が必要かどうか）が含まれています。URL生成はStripe SDKが行うので、URLが作成されたら、ブラウザに &lt;code&gt;HTTP 303&lt;/code&gt; (See Other) で転送されると、Stripe Checkoutのページが表示されます。&lt;/p&gt;

&lt;p&gt;LambdaでCheckoutセッションのURLを生成し転送させるにはこんな感じです。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exports.handler = async (event) =&amp;gt; {
  const config = await loadConfig();
  const stripe = require('stripe')(config.stripe_api_secret_key);

  const session = await stripe.checkout.sessions.create( {
    line_items: /* TODO */,
    payment_method_types: [ 'card' ],
    mode: 'payment',
    success_url: config.success_url,
    cancel_url: config.cancel_url
  } );

  const response = {
    statusCode: 303,
    headers: {
      'Location': session.url
    }
  };

  return response;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;セッションデータの &lt;code&gt;line_items&lt;/code&gt; は商品を指定します。ブラウザからPOSTで送信されたデータがあるかどうかを確認し、 &lt;code&gt;line_items&lt;/code&gt; に入れる商品データを変えます。なお、LambdaではペイロードがBase64エンコードされてることがあるので、デコードを行う必要があります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  if (event.body) {
    let payload = event.body;
    if (event.isBase64Encoded)
      payload = Buffer.from(event.body, 'base64').toString();

    const querystring = require('querystring');
    const res = querystring.parse(payload);
    if ((res.type) &amp;amp;&amp;amp; (res.type == 'bronze')) {
      // ...
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;まずVisitorチケットの場合は単純に1つの商品になります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  let items = [ {
    price: config.product_visitor_ticket,
    quantity: 1
  } ];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visitorチケットの1つの商品でStripe Checkoutに転送するとこんな感じで表示されます。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4po7ld6thtf25au7br7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4po7ld6thtf25au7br7h.png" alt="Stripe Checkout Visitor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;では、次にBronze Supporterチケットの場合は選択されたTシャツサイズを商品のPriceIDとマッチングし、2つの商品（チケットの商品とTシャツの商品）を &lt;code&gt;line_items&lt;/code&gt; に入れます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  let tshirt_type = config.product_tshirt_s;

  if (res.tshirt) {
    if (res.tshirt == 's') tshirt_type = config.product_tshirt_s;
    if (res.tshirt == 'm') tshirt_type = config.product_tshirt_m;
    if (res.tshirt == 'l') tshirt_type = config.product_tshirt_l;
    if (res.tshirt == 'xl') tshirt_type = config.product_tshirt_xl;
  }

  items = [ {
    price: config.product_bronze_ticket,
    quantity: 1
  }, {
    price: tshirt_type,
    quantity: 1
  } ];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;あとは、Bronze Supporterチケットの場合はTシャツの配送に住所を入力してもらう必要があります。Checkoutセッションデータに配送料金の &lt;code&gt;shipping_rates&lt;/code&gt; と配送対象国（どの国への配送が可能）を &lt;code&gt;shipping_address_collection&lt;/code&gt; の &lt;code&gt;allowed_countries&lt;/code&gt; で指定します。&lt;/p&gt;

&lt;p&gt;まとめるとこんな感じになります。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  const session = await stripe.checkout.sessions.create( {
    line_items: [ {
      price: config.product_bronze_ticket,
      quantity: 1
    }, {
      price: tshirt_type,
      quantity: 1
    } ],
    payment_method_types: [ 'card' ],
    mode: 'payment',
    success_url: config.success_url,
    cancel_url: config.cancel_url,
    shipping_rates: [ config.shipping_rate ],
    shipping_address_collection: {
      allowed_countries: config.shipping_countries.split(',')
    }
  } );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;配送情報がセッションデータに含まれるとStripe Checkoutで住所入力フィールドが一緒にフォームに表示されます。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxl82a4iy6uu47hot3lml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxl82a4iy6uu47hot3lml.png" alt="Stripe Checkout Bronze"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;一応、この数十行のコードだけでStripe Checkoutでの決済は可能になりました。購入決済が完了されたら、StripeからWebhookを呼び出し、メール送信などの他の処理が可能なので、次のセクションで紹介します。&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe Webhookの実装
&lt;/h2&gt;

&lt;p&gt;先ほど紹介しましたが、TDFでは、チケット購入の決済完了となった時はVisitorへのチケット情報をメールで送信します。こちらの対応は別のLambda関数を作成し、API GatewayのURLをStripe Webhookに設定します。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsu51bm074bf6j57u7t16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsu51bm074bf6j57u7t16.png" alt="TDF Stripe Webhook"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Webhookの処理は各自それぞれ違う対応になりますので、StripeからのPOSTデータのデコードまで紹介します。&lt;/p&gt;

&lt;p&gt;まずは、WebhookのURLが公開されているため、誰でもアクセスができてしまいます。Stripeからのアクセスの際はHTTPヘッダーに署名が含まれ、Webhookシークレットのキーでペイロードデータが正常なのかを確認します。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exports.handler = async (event) =&amp;gt; {
  // require Stripe signature in header
  if (!event.headers['stripe-signature']) {
    console.log('no Stripe signature received in header, returning 400 Bad Request');
    return {
      statusCode: 400
    };
  }

  const sig = event.headers['stripe-signature'];

  // require an event body
  if (!event.body) {
    console.log('no event body received in POST, returning 400 Bad Request');
    return {
      statusCode: 400
    };
  }

  // decode payload
  let payload = event.body;
  if (event.isBase64Encoded)
    payload = Buffer.from(event.body, 'base64').toString();

  // construct a Stripe Webhook event
  const config = await loadConfig();
  const stripe = require('stripe')(config.stripe_api_secret_key);

  try {
    let ev = stripe.webhooks.constructEvent(payload, sig, config.webhook_signing_secret);
  } catch (err) {
    console.log('error creating Stripe Webhook event');
    console.log(err);
    return {
      statusCode: 400
    };
  }

  // ...TODO...

  return {
    statusCode: 200
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stripe Webhookのイベントまで正常に作成したら、次はCheckoutセッションのステータス変化を確認します。決済に関して以下の3つのイベントを対応するのが一般的です。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;checkout.session.completed&lt;/code&gt; : Stripe Checkoutで決済が行われました。支払い方法によって、決済が完了になってない可能性があります。クレジットカードの場合は基本的に &lt;code&gt;payment_status&lt;/code&gt; が &lt;code&gt;paid&lt;/code&gt; になるので、決済完了ということになります。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkout.session.async_payment_succeeded&lt;/code&gt; : &lt;code&gt;completed&lt;/code&gt; のイベントで決済が未完了だったのが、決済完了となりました。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkout.session.async_payment_failed&lt;/code&gt; : &lt;code&gt;completed&lt;/code&gt; のイベントで決済が未完了だったのが、決済失敗となりました。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;この3つのイベントを対応するには&lt;a href="https://stripe.com/docs/payments/checkout/fulfill-orders" rel="noopener noreferrer"&gt;Stripeのサンプルコード&lt;/a&gt;とほぼ同様に行ってます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const createOrder = async function(session) {
  // we (TDF) don't need to do anything here
}

const fulfillOrder = async function(session) {
  // send ticket info to customer by email
  console.log('customer email is: ' + session.customer_details.email);
}

const emailCustomerAboutFailedPayment = async function(session) {
  // send email about failed payment
}

exports.handler = async (event) =&amp;gt; {
  // ...
  const session = ev.data.object;
  switch (ev.type) {
    case 'checkout.session.completed':
      // save an order in your database, marked as 'awaiting payment'
      await createOrder(session);

      // check if the order is paid (e.g., from a card payment)
      // a delayed notification payment will have an `unpaid` status
      if (session.payment_status === 'paid') {
        await fulfillOrder(session);
      }
      break;

    case 'checkout.session.async_payment_succeeded':
      // fulfill the purchase...
      await fulfillOrder(session);
      break;

    case 'checkout.session.async_payment_failed':
      // send an email to the customer asking them to retry their order
      await emailCustomerAboutFailedPayment(session);
      break;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createOrder()&lt;/code&gt; 、&lt;code&gt;fulfillOrder()&lt;/code&gt; 、そして &lt;code&gt;emailCustomerAboutFailedPayment()&lt;/code&gt; の3つの関数を実装することでWebhookの対応は完了になります。&lt;/p&gt;

&lt;p&gt;もしWebhookがエラーで &lt;code&gt;HTTP 2xx&lt;/code&gt; 以外のレスポンスを返された場合、Stripe側では時間を置いてから自動的にリトライされます。詳しくは&lt;a href="https://stripe.com/docs/webhooks/best-practices" rel="noopener noreferrer"&gt;Stripe Webhook Best Practices&lt;/a&gt;を確認してください。&lt;/p&gt;

&lt;p&gt;ここまで実装ができてれば、Stripe Checkoutの対応は完了になります。おめでとうございます！&lt;/p&gt;

&lt;h2&gt;
  
  
  API GatewayのCustom Domainで複数エンドポイントをひとつに統合
&lt;/h2&gt;

&lt;p&gt;今回の実装では、Stripe CheckoutのURL生成とStripe Webhookの2つのエンドポイントがあります。もちろんAPI Gatewayで払い出されたURL ( &lt;code&gt;https://7q6f1e5os2.execute-api.ap-northeast-1...&lt;/code&gt; )をそのまま使えますが、 &lt;code&gt;stripe.tokyodemofest.jp&lt;/code&gt; などの名前を付けられたサブドメインに統合するとURLの見た目が良くなります。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpvoy2tl8f32h3ipbkm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpvoy2tl8f32h3ipbkm9.png" alt="API Gateway Custom Domain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;こんな感じで &lt;code&gt;checkout&lt;/code&gt; と &lt;code&gt;fulfill&lt;/code&gt; の2つのLambdaとAPI GatewayがCustom Domainの1つにまとめています。&lt;/p&gt;

&lt;h2&gt;
  
  
  TDFで初めてStripeを実装しての感想
&lt;/h2&gt;

&lt;p&gt;正直、Stripe Checkoutをサーバーレスで実装するのはとても簡単でした。コードを書く量が少ないので、本当に数十行だけで自分のWebサイトから決済ができるようになります。&lt;/p&gt;

&lt;p&gt;しかも、CheckoutやWebhookを実装してる際はStripe UIでAPIのHTTPリクエストとレスポンスとログ情報まで細かく見れて、Dashboardでグラフがとてもわかりやすいです。&lt;/p&gt;

&lt;p&gt;ひとつ欲を言えば、テスト環境で作成した商品を簡単に本番モードに持って行きたいです。Webhookは「テストエンドポイントをインポート」する機能がありますが、&lt;del&gt;商品ではできないのがちょっとだけ残念です。テスト環境で作成した商品をもう一度すべて本番モードで作り直す必要があります。&lt;/del&gt;&lt;/p&gt;

&lt;p&gt;【※: 投稿してから知りましたが、実は商品詳細ページには「本番環境にコピー」というボタンがあります。テスト環境で作成したすべての商品を一気に本番環境までインポートするのはできないようですが、1つずつ商品をコピーするのは可能です。】&lt;/p&gt;

&lt;p&gt;長年PayPalと戦っていたので、もっと早く乗り換えれば良かったと後悔しています（笑）&lt;/p&gt;

&lt;p&gt;最後まで読んでくれてありがとうございました。何か質問やコメントがあれば、ぜひどうぞよろしくお願いいたします！&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>stripe</category>
      <category>aws</category>
      <category>demoscene</category>
    </item>
  </channel>
</rss>
