DEV Community

Cover image for Handling DodoPayments Webhooks with Firebase Cloud Functions
Shadai Scott
Shadai Scott

Posted on

Handling DodoPayments Webhooks with Firebase Cloud Functions

This guide walks through integrating DodoPayments webhook events into a Firebase Cloud Function. Firebase is a natural choice for handling webhooks because it provides a serverless environment that scales automatically and minimizes infrastructure management. By the end of this guide you'll have a deployed Cloud Function that receives DodoPayments events, verifies their authenticity, and writes the results to Firestore.

Prerequisites

  • Firebase project set up with Blaze plan
  • DodoPayments merchant account in test mode
  • Firebase CLI installed

Setting up Firebase Function

This guide assumes your Firebase CLI is already installed and you are at the root of your project.

  • Authenticate and initalize functions
firebase login
firebase init functions
Enter fullscreen mode Exit fullscreen mode
  • Select an existing project or create a new one. Choose TypeScript when prompted. It's recommended for modern Nuxt projects. Opt in to install dependencies when prompted.

  • In your index.ts file write a placeholder function to get your public endpoint URL:

import { onRequest } from 'firebase-functions/v2/https';

export const dodoWebhook = onRequest((req, res) => {
  res.status(200).send("OK");
});
Enter fullscreen mode Exit fullscreen mode
  • Deploy it
cd functions
firebase deploy --only functions:dodoWebhook --project your-project-name
Enter fullscreen mode Exit fullscreen mode
  • Copy the URL from the terminal output. You'll need it in the next step.

Registering the Endpoint in DodoPayments

  • In your DodoPayments Dashboard go to Developer -> Webhook -> Add Endpoint

  • Paste your Firebase Function URL.

  • Select the events you want to listen for. At minimum: payment.succeeded, payment.failed, subscription.active, subscription.cancelled

  • Save and copy the webhook secret key that is generated.

Storing your Webhook Secret

Never hardcode secrets in your function. Store it using Firebase's secret manager:

firebase functions:secrets:set DODO_WEBHOOK_SECRET --project your-project-name

Paste your webhook secret when prompted.

Writing the Webhook Function

Install the DodoPayments SDK inside your functions folder:

cd functions
npm install dodopayments
Enter fullscreen mode Exit fullscreen mode

Replace the contents of index.ts with the following:

import { onRequest } from 'firebase-functions/v2/https';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { defineSecret } from 'firebase-functions/params';
import crypto from 'crypto';

initializeApp();

const dodoWebhookSecret = defineSecret('DODO_WEBHOOK_SECRET');
Enter fullscreen mode Exit fullscreen mode

Next is to add the signature verification helper. DodoPayments follows the Standard Webhooks spec. Verification works by concatenating the webhook-id, webhook-timestamp, and raw payload separated by periods, then computing an HMAC SHA256 of that string using your secret key and comparing it against the webhook-signature header.

async function verifyDodoSignature(
  rawBody: string,
  signature: string | string[] | undefined,
  timestamp: string | string[] | undefined,
  webhookId: string | string[] | undefined,
  secret: string
): Promise<boolean> {
  try {
    const signedContent = `${webhookId}.${timestamp}.${rawBody}`;
    const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
    const computedSignature = crypto
      .createHmac('sha256', secretBytes)
      .update(signedContent)
      .digest('base64');

    const signatures = String(signature)
      .split(' ')
      .map(s => s.split(',')[1]);

    return signatures.some(s => s === computedSignature);
  } catch {
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we move on to write the firebase function. In this function verification is done first to ensure security after which the processes are carried out. It is good to write handler functions as it makes it easier to make any changes if needed.

export const dodoWebhook = onRequest(
  {
    timeoutSeconds: 60,
    memory: '512MiB',
    secrets: [dodoWebhookSecret],
  },
  async (req, res) => {
    if (req.method !== 'POST') {
      res.status(405).json({ error: 'Method not allowed' });
      return;
    }

    const signature = req.headers['webhook-signature'];
    const timestamp = req.headers['webhook-timestamp'];
    const webhookId = req.headers['webhook-id'];
    const rawBody = JSON.stringify(req.body);

    const isValid = await verifyDodoSignature(
      rawBody,
      signature,
      timestamp,
      webhookId,
      dodoWebhookSecret.value()
    );

    if (!isValid) {
      console.error('Invalid webhook signature');
      res.status(401).send('Invalid signature');
      return;
    }

    // Acknowledge immediately. DodoPayments expects a 2xx response 
    // before you process. Failure to do so triggers a retry.
    res.status(200).send('OK');

    const { type, data } = req.body;
    const db = getFirestore();

    switch (type) {
      case 'payment.succeeded':
        await handlePaymentSucceeded(data, db);
        break;
      case 'payment.failed':
        await handlePaymentFailed(data, db);
        break;
      case 'subscription.active':
        await handleSubscriptionActive(data, db);
        break;
      case 'subscription.cancelled':
        await handleSubscriptionCancelled(data, db);
        break;
      default:
        console.log(`Unhandled event type: ${type}`);
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

Add your event handlers. The customerId comes from data.customer.customer_id in the DodoPayments payload:

async function handlePaymentSucceeded(data: any, db: FirebaseFirestore.Firestore) {
  const customerId = data.customer.customer_id;
  await db.collection('payments').doc(data.payment_id).set({
    customerId,
    status: 'succeeded',
    amount: data.total_amount,
    currency: data.currency,
    updatedAt: new Date(),
  });
}

async function handlePaymentFailed(data: any, db: FirebaseFirestore.Firestore) {
  const customerId = data.customer.customer_id;
  await db.collection('payments').doc(data.payment_id).set({
    customerId,
    status: 'failed',
    updatedAt: new Date(),
  });
}

async function handleSubscriptionActive(data: any, db: FirebaseFirestore.Firestore) {
  const customerId = data.customer.customer_id;
  await db.collection('subscriptions').doc(customerId).set({
    status: 'active',
    subscriptionId: data.subscription_id,
    updatedAt: new Date(),
  }, { merge: true });
}

async function handleSubscriptionCancelled(data: any, db: FirebaseFirestore.Firestore) {
  const customerId = data.customer.customer_id;
  await db.collection('subscriptions').doc(customerId).set({
    status: 'cancelled',
    updatedAt: new Date(),
  }, { merge: true });
}
Enter fullscreen mode Exit fullscreen mode

Deploying to Production
firebase deploy --only functions:dodoWebhook --project your-project-name

Top comments (0)