<- Back

what is Stripe Webhooks

When interacting with Stripe API, certain events happen. e.g. When session checkout is completed, session-checkout-completed event is created. Most of the times, we want to do some processing after the event happens. e.g. when session checkout is completed, we want to update the order details in database.

How to setup webhook

Webhook is nothing but an api endpoint that gets called by Stripe when certain event happens. When you create a webhook, stripe will ask you for below details.

  • API endpoint e.g. https://www.xyz.com/api/stripe-events You will have to write a logic for this api endpoint on your api server.
  • Next thing you will need to provide is events when above endpoint will be called. So there are several events that occur and you can select specific events. Whenver selected event occurs, stripe will call above endpoint.

How webhook code looks like?

Here is an example showing how typical webhook endpoint logic looks like. Please note that for security purpose, you will need to use signing secret (get this from developer dashboard settings of stripe) to verify the signature sent by Stripe. Also you will need to use raw request body when constructing event. Otherwise you will get error saying "Webhook Error: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe"

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
import { MongoClient } from "mongodb";
import getRawBody from 'raw-body';

require('dotenv').config();
let mongodbServer = process.env.MONGODB_URI;
const stripe = require("stripe")(process.env.stripe_sk);
const endpointSecret = process.env.STRIPE_EVENT_SECRET;

export default async function handler(
  request: NextApiRequest,
  response: NextApiResponse
) {

  console.log("WEBHOOK CALLED BY STRIPE");
  console.log("headers ", JSON.stringify(request.headers));

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

  let event;
  console.log("body ", JSON.stringify(request.body));

  try {
    const rawBody = await getRawBody(request);
    event = stripe.webhooks.constructEvent(rawBody, sig, endpointSecret);
  } catch (err) {
    console.log("Error occured ", err)
    response.status(200).send(`Webhook Error: ${err.message}`);
    return;
  }
  console.log("event ", JSON.stringify(event));

  // Handle the event
  switch (event.type) {

     case 'checkout.session.completed':
      const sessionCheckoutCompleted = event.data.object;
      // Then define and call a function to handle the event payment_intent.requires_action
      break;
    case 'payment_intent.succeeded':
      const paymentIntentSucceeded = event.data.object;
      // Then define and call a function to handle the event payment_intent.succeeded
      break;
      // ... handle other event types
    default:
      console.log(`Unhandled event type ${event.type}`);
  }
  console.log("Event handled");

  // Return a 200 response to acknowledge receipt of the event
  response.status(200).send(`Event ${event.type} handled successfully ${JSON.stringify(event)}`);

}

export const config = {
  api: {
    bodyParser: false,
  },
};

Stripe sends the event object when calling webhook. Typical event object for "checkout.session.completed" event will look like below.

{
  "id": "evt_1Msl7xLboumfgfger3344GuPMmogknXWbz",
  "object": "event",
  "api_version": "2020-08-27",
  "created": 1680519645,
  "data": {
    "object": {
      "id": "cs_live_a1wAww9lOgdfewr3354gQRJMQcVQggvFCehZBP8JraVu9Xdm7RmduTNMCMoGtZC9upW",
      "object": "checkout.session",
      "after_expiration": null,
      "allow_promotion_codes": null,
      "amount_subtotal": 1000,
      "amount_total": 1000,
      "automatic_tax": {
        "enabled": false,
        "status": null
      },
      "billing_address_collection": null,
      "cancel_url": "https://www.softpost.org/online-training",
      "client_reference_id": null,
      "consent": null,
      "consent_collection": null,
      "created": 168053419590,
      "currency": "usd",
      "currency_conversion": null,
      "custom_fields": [
      ],
      "custom_text": {
        "shipping_address": null,
        "submit": null
      },
      "customer": "cus_Ne3Edee32eTo7qyBp8y",
      "customer_creation": "always",
      "customer_details": {
        "address": {
          "city": null,
          "country": "AU",
          "line1": null,
          "line2": null,
          "postal_code": null,
          "state": null
        },
        "email": "[email protected]",
        "name": "Sagar Salunke",
        "phone": null,
        "tax_exempt": "none",
        "tax_ids": [
        ]
      },
      "customer_email": null,
      "expires_at": 168032605989,
      "invoice": null,
      "invoice_creation": {
        "enabled": false,
        "invoice_data": {
          "account_tax_ids": null,
          "custom_fields": null,
          "description": null,
          "footer": null,
          "metadata": {
          },
          "rendering_options": null
        }
      },
      "livemode": true,
      "locale": null,
      "metadata": {
      },
      "mode": "payment",
      "payment_intent": "pi_3Msldsdwew74LboumGuPMm0nfRWKoU",
      "payment_link": null,
      "payment_method_collection": "always",
      "payment_method_options": {
      },
      "payment_method_types": [
        "card"
      ],
      "payment_status": "paid",
      "phone_number_collection": {
        "enabled": false
      },
      "recovered_from": null,
      "setup_intent": null,
      "shipping": null,
      "shipping_address_collection": null,
      "shipping_options": [
      ],
      "shipping_rate": null,
      "status": "complete",
      "submit_type": null,
      "subscription": null,
      "success_url": "https://www.softpost.org/check/checkout-success",
      "total_details": {
        "amount_discount": 0,
        "amount_shipping": 0,
        "amount_tax": 0
      },
      "url": null
    }
  },
  "livemode": true,
  "pending_webhooks": 1,
  "request": {
    "id": null,
    "idempotency_key": null
  },
  "type": "checkout.session.completed"
}

You can check "status" and "payment_status" values to update database for given transaction.

Real life example

Let us say you want to sell online courses. Here are the steps you will need to take

  • Customer signs up on your site using auth service like auth0 or next-auth or custom code.
  • Customer hits enroll/buy now button
  • API (e.g. /api/enroll) gets called and customer is created in local database if not existing. Also create customer in Stripe if not created.
  • Customer is forwarded to stripe checkout url. At this point, customer info (created above) can be sent to stripe as well.
  • Customer makes payment
  • Stripe invokes the "webhook" (e.g. /api/stripe-events) when checkout.session.completed event is invoked.
  • In webhook, we know for which customer webhook was called. So we can update db accordingly.

Web development and Automation testing

solutions delivered!!