Handling DodoPayments Webhooks with Firebase Cloud Functions

Published: (March 19, 2026 at 04:32 PM EDT)
7 min read
Source: Dev.to

Source: Dev.to

Integrating DodoPayments Webhook Events with a Firebase Cloud Function

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 server‑less 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
  • Writes the results to Firestore

Prerequisites

RequirementDetails
Firebase projectBlaze (pay‑as‑you‑go) plan
DodoPaymentsMerchant account in test mode
Firebase CLIInstalled locally (npm i -g firebase-tools)
Nodev18+ (recommended)
GitOptional, but useful for version control

1. Set Up the Firebase Function

Assumption: You are already at the root of your Firebase project and the Firebase CLI is installed.

1.1 Authenticate & Initialise Functions

# Log in to your Google account
firebase login

# Initialise the Functions directory (choose TypeScript)
firebase init functions
  • Choose an existing project or create a new one.
  • When prompted, select TypeScript – it works nicely with modern Nuxt projects.
  • Accept the default options and let the CLI install dependencies.

1.2 Create a Placeholder Function

Open functions/src/index.ts (or functions/index.ts if you didn’t use the src folder) and add a minimal HTTP endpoint. This will give you a public URL that you can register in DodoPayments.

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

export const dodoWebhook = onRequest((req, res) => {
  res.status(200).send('OK');
});

1.3 Deploy the Placeholder

cd functions
firebase deploy --only functions:dodoWebhook --project your-project-name

Copy the generated URL from the terminal output – you’ll need it in the next step.


2. Register the Endpoint in DodoPayments

  1. Log in to the DodoPayments Dashboard.

  2. Navigate to Developer → Webhook → Add Endpoint.

  3. Paste the Firebase Function URL you just copied.

  4. Select the events you want to listen for (minimum recommended set):

    • payment.succeeded
    • payment.failed
    • subscription.active
    • subscription.cancelled
  5. Save the endpoint.

  6. Copy the webhook secret key that DodoPayments generates – you’ll store it securely in Firebase next.


3. Store the Webhook Secret Securely

Never hard‑code secrets in source files. Use Firebase’s secret manager:

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

When prompted, paste the secret you copied from DodoPayments.


4. Write the Full Webhook Function

4.1 Install the DodoPayments SDK

cd functions
npm install dodopayments

4.2 Replace index.ts with the Complete Implementation

// ------------------------------------------------------------
//  functions/src/index.ts
// ------------------------------------------------------------
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';

// Initialise the Admin SDK (required for Firestore)
initializeApp();

// Load the secret we stored earlier
const dodoWebhookSecret = defineSecret('DODO_WEBHOOK_SECRET');

/* ---------------------------------------------------------
   Helper: Verify DodoPayments signature
   --------------------------------------------------------- */
async function verifyDodoSignature(
  rawBody: string,
  signature: string | string[] | undefined,
  timestamp: string | string[] | undefined,
  webhookId: string | string[] | undefined,
  secret: string
): Promise {
  try {
    // Build the signed string: ".."
    const signedContent = `${webhookId}.${timestamp}.${rawBody}`;

    // The secret is stored as "_", we need the base64 part
    const secretBytes = Buffer.from(secret.split('_')[1], 'base64');

    // Compute HMAC‑SHA256 and base64‑encode it
    const computedSignature = crypto
      .createHmac('sha256', secretBytes)
      .update(signedContent)
      .digest('base64');

    // DodoPayments may send multiple signatures separated by spaces
    const signatures = String(signature)
      .split(' ')
      .map(s => s.split(',')[1]); // each entry looks like "v1,abcdef..."

    // Return true if any of the supplied signatures matches ours
    return signatures.some(s => s === computedSignature);
  } catch {
    return false;
  }
}

/* ---------------------------------------------------------
   Main Cloud Function
   --------------------------------------------------------- */
export const dodoWebhook = onRequest(
  {
    timeoutSeconds: 60,
    memory: '512MiB',
    secrets: [dodoWebhookSecret],
  },
  async (req, res) => {
    // -------------------------------------------------------
    // 1️⃣  Validate HTTP method
    // -------------------------------------------------------
    if (req.method !== 'POST') {
      res.status(405).json({ error: 'Method not allowed' });
      return;
    }

    // -------------------------------------------------------
    // 2️⃣  Extract headers & raw body
    // -------------------------------------------------------
    const signature = req.headers['webhook-signature'];
    const timestamp = req.headers['webhook-timestamp'];
    const webhookId = req.headers['webhook-id'];
    const rawBody = JSON.stringify(req.body); // keep the exact payload

    // -------------------------------------------------------
    // 3️⃣  Verify signature
    // -------------------------------------------------------
    const isValid = await verifyDodoSignature(
      rawBody,
      signature,
      timestamp,
      webhookId,
      dodoWebhookSecret.value()
    );

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

    // -------------------------------------------------------
    // 4️⃣  Acknowledge receipt (DodoPayments expects a 2xx)
    // -------------------------------------------------------
    res.status(200).send('OK');

    // -------------------------------------------------------
    // 5️⃣  Process the event
    // -------------------------------------------------------
    const { type, data } = req.body as { type: string; data: any };
    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}`);
    }
  }
);

/* ---------------------------------------------------------
   Event Handlers
   --------------------------------------------------------- */
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(data.subscription_id).set({
    customerId,
    status: 'active',
    plan: data.plan_id,
    startedAt: new Date(data.started_at * 1000), // assuming UNIX timestamp
    updatedAt: new Date(),
  });
}

async function handleSubscriptionCancelled(
  data: any,
  db: FirebaseFirestore.Firestore
) {
  const customerId = data.customer.customer_id;
  await db.collection('subscriptions').doc(data.subscription_id).update({
    status: 'cancelled',
    cancelledAt: new Date(data.cancelled_at * 1000),
    updatedAt: new Date(),
  });
}

Note:

  • The helper extracts the base‑64 part of the secret (_).
  • All timestamps from DodoPayments are assumed to be UNIX seconds; adjust if they are ISO strings.

4.3 Deploy the Full Function

cd functions
firebase deploy --only functions:dodoWebhook --project your-project-name

After deployment, the URL will stay the same, so no further changes are required in DodoPayments.


5. Test the Integration

  1. In the DodoPayments dashboard, use the “Send test webhook” feature (if available) or trigger a real test payment.

  2. Verify that:

    • The Cloud Function logs “✅ Received …” (check via firebase functions:log).
    • Corresponding documents appear in Firestore under payments and subscriptions.
  3. If you see signature‑validation errors, double‑check that the secret stored in Firebase matches the one shown in the DodoPayments dashboard.


6. (Optional) Add Local Development Support

You can run the function locally with the Firebase emulator suite:

firebase emulators:start --only functions,firestore

Use a tool like ngrok to expose the local endpoint to DodoPayments for rapid iteration.


7. Clean‑up & Best Practices

✅ Good practice❌ What to avoid
Store secrets with functions:secrets:set.Hard‑coding secrets in source code.
Acknowledge the webhook before heavy processing.Delaying the 2xx response (causes retries).
Keep the function idempotent – DodoPayments may resend events.Assuming a single delivery.
Use Firestore security rules to protect data.Leaving the database open to the world.
Monitor logs (firebase functions:log) and set up alerts for failures.Ignoring error logs.

🎉 You’re done!

Your Firebase Cloud Function now securely receives DodoPayments webhook events, validates them, and persists the relevant data to Firestore. Feel free to extend the handlers (e.g., send emails, update user profiles, etc.) while keeping the same verification and acknowledgement pattern. Happy coding!

tomer.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 });
}

Deploying to Production

firebase deploy --only functions:dodoWebhook --project your-project-name
0 views
Back to Blog

Related posts

Read more »

Barbershop Web App

Summary A full‑featured, responsive barbershop website built with Next.js, React, TypeScript, Tailwind CSS, Firebase, and a collection of reusable UI component...