• Documentation
  • /
  • Remix Firebase
  • /
  • Stripe Webhooks

Stripe Webhooks

Learn how MakerKit handles Stripe Webhooks in your application

Makerkit handles five webhooks from Stripe and stores "just enough" data into the organization's entity to function correctly.

The Stripe's webhooks topics are defined here at src/core/stripe/stripe-webhooks.enum.ts:

export enum StripeWebhooks {
  AsyncPaymentSuccess = 'checkout.session.async_payment_succeeded',
  Completed = 'checkout.session.completed',
  AsyncPaymentFailed = 'checkout.session.async_payment_failed',
  SubscriptionDeleted = 'customer.subscription.deleted',
  SubscriptionUpdated = 'customer.subscription.updated',
}

To handle webhooks, we created an API route at pages/api/stripe/webhook.ts.

This route is responsible for:

  1. Validating that the webhook is coming from Stripe
  2. Routing the webhook to its handler

Validating Stripe's Webhooks

To validate the webhook, we do the following:

  1. get the header 'stripe-signature'
  2. create a Stripe instance
  3. get the raw body
  4. call constructEvent to validate and build an event object sent by Stripe. When this fails, it will throw an error
const signature = req.headers['stripe-signature'];

// verify signature header is not missing
if (!signature) {
  return throwBadRequestException();
}

const rawBody = await getRawBody(req);
const stripe = await getStripeInstance();

const event = stripe.webhooks.constructEvent(
  rawBody,
  signature,
  webhookSecretKey
);

Handling Stripe's Webhooks

After validating the webhook, we can now handle the webhook type relative to its topic:

switch (event.type) {
  case StripeWebhooks.Completed: {
    // handle completed
  }

  case StripeWebhooks.AsyncPaymentSuccess: {
     // handle async payment success
  }
}

As described above, we handle 5 events:

  1. Completed: the checkout session was completed (but payment may not have arrived yet if it's asynchronous)
  2. AsyncPaymentSuccess: an asynchronous payment arrived
  3. AsyncPaymentFailed: an asynchronous payment failed
  4. SubscriptionDeleted: the subscription was deleted by the customer
  5. SubscriptionUpdated: the subscription was updated by the customer

Checkout Completed

When a user completes the checkout session, we create the subscription object on the currently connected user's organization.

The payment may not have yet succeeded when it's "asynchronous", so the payment's status may be "AwaitingPayment" rather than "Paid". You may want to consider the subscription active only when the subscription's status is "Paid".

The subscription's object has the following interface:

export enum OrganizationPlanStatus {
  AwaitingPayment = 'awaitingPayment',
  Paid = 'paid',
}

export interface OrganizationSubscription {
  id: string;
  priceId: string;

  status: OrganizationPlanStatus;
  currency: string | null;

  interval: string | null;
  intervalCount: number | null;

  createdAt: UnixTimestamp;
  periodStartsAt: UnixTimestamp;
  periodEndsAt: UnixTimestamp;
  trialStartsAt: UnixTimestamp | null;
  trialEndsAt: UnixTimestamp | null;
}

This interface is added to the Organization object.

Async Payment Success

When a payment is asynchronous, we need to wait for another webhook for confirmation. When this webhook is received, we turn the status property from AwaitingPayment to Paid.

Async Payment Failed

When an asynchronous payment fails, we send an email to the customer to warn them.

Subscription Updated

When a subscription is updated, we rebuild the subscription object and update it in the organization's Firestore entity.

Subscription Deleted

When a subscription is deleted, we simply remove it from the organization entity.


Stay informed with our latest resources for building a SaaS

Subscribe to our newsletter to receive updatesor