Handling DodoPayments Webhooks with Firebase Cloud Functions
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
| Requirement | Details |
|---|---|
| Firebase project | Blaze (pay‑as‑you‑go) plan |
| DodoPayments | Merchant account in test mode |
| Firebase CLI | Installed locally (npm i -g firebase-tools) |
| Node | v18+ (recommended) |
| Git | Optional, 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
-
Log in to the DodoPayments Dashboard.
-
Navigate to Developer → Webhook → Add Endpoint.
-
Paste the Firebase Function URL you just copied.
-
Select the events you want to listen for (minimum recommended set):
payment.succeededpayment.failedsubscription.activesubscription.cancelled
-
Save the endpoint.
-
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
-
In the DodoPayments dashboard, use the “Send test webhook” feature (if available) or trigger a real test payment.
-
Verify that:
- The Cloud Function logs “✅ Received …” (check via
firebase functions:log). - Corresponding documents appear in Firestore under
paymentsandsubscriptions.
- The Cloud Function logs “✅ Received …” (check via
-
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