Skip to main content

Webhook Handler

Overview

The Webhook Handler provides critical infrastructure for maintaining data consistency between Stripe (source of truth) and the local MongoDB database. It processes incoming Stripe webhook events and synchronizes state changes across subscriptions, invoices, products, prices, coupons, disputes, and payouts.

Source File: internal/api/v1/store/Controllers/webhook.js (2071 lines)

Key Capabilities

  • Real-Time State Sync: Mirrors Stripe events to MongoDB collections
  • Invoice Processing: Handles payment success, failure, and retry logic
  • Subscription Lifecycle: Tracks creation, updates, cancellations
  • Product Catalog Sync: Keeps product and price data synchronized
  • Discount Management: Syncs coupon and promo code changes
  • Dispute Handling: Tracks chargeback and dispute events
  • Payout Tracking: Monitors connected account payout events
  • Queue Orchestration: Triggers fulfillment and cleanup jobs
  • Activity Logging: Creates audit trail for subscription events
  • Pulse Management: Completes pulses based on subscription state

🗄️ MongoDB Collections

Primary Collections (Write Operations)

CollectionOperationsPurpose
_store.invoicesUpsert, UpdateMirror Stripe invoices with payment attempt tracking
_store.subscriptionsUpsert, Update, QuerySubscription state synchronization
_store.ordersUpdate, QueryOrder status and fulfillment linkage
_store.productsUpsertProduct catalog synchronization
_store.pricesUpsert, UpdatePrice tier synchronization
_store.couponsUpsert, DeleteCoupon state synchronization
_store.promo.codesUpsert, DeletePromo code synchronization
_store.disputesCreate, UpdateDispute and chargeback tracking
_store.payoutCreate, UpdatePayout event tracking
_store.cartDeleteClear cart items on coupon/promo deletion

Supporting Collections (Read/Update)

CollectionOperationsPurpose
_accountsUpdateSet has_orders, became_customer_on, downgrade.plan
queuesCreate, UpdateSpawn fulfillment, downgrade, cleanup jobs
projects-tasksCreate, Update, DeleteTask lifecycle management
projects-pulsesUpdate, DeletePulse completion and cleanup
communicationsCreateActivity audit trail
lightning.domainsUpdateSoftware subscription domain cancellation
store-subscription-feedbackDeleteClear feedback on upgrades
crm.contactsQueryPerson assignment for new subscriptions
_usersQueryOwner lookup for queue jobs

🎯 Core Webhook Handlers

1. updateInvoice() - Invoice Events

Handles: invoice.created, invoice.payment_failed, invoice.updated, invoice.finalized, invoice.paid

Purpose: Synchronize invoice state and handle payment failures with retry logic.

Business Logic

flowchart TD
A[Webhook Event] --> B{Validate Line Items}
B -->|Invalid| C[Return Error: WEBHOOK NOT FOR CURRENT PLATFORM]
B -->|Valid| D[Determine Connected Account]
D --> E{Has Subscription?}
E -->|Yes| F[Load Subscription with Price/Product]
E -->|No| G[Create Invoice Record]
F --> H{Event Type?}
H -->|invoice.created| I[Sync Metadata to Stripe]
H -->|invoice.payment_failed| J[Track Payment Attempt]
I --> K{Sub-account Charge Needed?}
K -->|Yes| L[Set pending_sub_account_charge Flag]
K -->|No| M[Upsert Invoice]
J --> N[Append Timestamp to payment_attempts Array]
N --> O{Subscription past_due?}
O -->|Yes| P[Create subscription_past_due Activity]
O -->|No| M
L --> M
I --> M
P --> M
M --> Q[Return Success]
G --> M

Key Operations

1. Line Item Validation
for (const line of req.body.data.object.lines.data) {
if (line.price.product === process.env.SETUP_PRODUCT) continue;

let pCheck = await StorePrice.findOne({
stripe_id: line.plan?.id || line.price?.id,
}).populate({ path: 'product' });

if (!pCheck) {
throw new Error('WEBHOOK NOT FOR CURRENT PLATFORM');
}
}
2. Payment Failure Tracking
if (req.body.type === 'invoice.payment_failed') {
invoice['payment_attempts'] = [...(dbInvoiceData?.payment_attempts || []), Date.now()];
}
3. Past Due Activity Creation
if (sub?._doc?.status === 'past_due') {
const latestOrder = await StoreOrder.findOne({
subscription: subscriptionId,
})
.sort({ _id: -1 })
.lean();

await createActivity({
refId: latestOrder._id,
accountId: sub?.metadata?.account_id,
activityType: 'subscription_status',
eventType: 'subscription_past_due',
metadata: {
total_attempts: invoice?.attempts,
amount_remaining: invoice?.amount_remaining,
previous_attempts: invoice?.payment_attempts,
},
});
}
4. Sub-Account Charge Logic
// For sub-accounts billed through parent Stripe credentials
const metadata = sub._doc.metadata;
const account_id = metadata.account_id;
const main_acc_id = metadata.main_account_id;

if (account_id.toString() !== main_acc_id?.toString()) {
if (!sub._doc.charge_id) {
pending_sub_account_charge = true; // Flag for billing queue
}
}

Important Notes

  • Idempotency: Uses findOneAndUpdate with upsert: true to handle duplicate webhook deliveries
  • Payment Attempts: Tracks all failure timestamps in payment_attempts array for retry rate limiting
  • Metadata Sync: Invoice metadata inherits from subscription metadata for consistency
  • Sub-Account Billing: Flags invoices requiring separate charge to sub-account Stripe customer

2. deleteInvoice() - Invoice Deletion

Handles: invoice.deleted, invoice.voided

Purpose: Remove invoice records when deleted in Stripe.

await StoreInvoice.deleteOne({
stripe_id: req.body.data.object.id,
connected_account,
});

3. updateSubscription() - Subscription Lifecycle

Handles: customer.subscription.created, customer.subscription.updated, customer.subscription.paused, customer.subscription.resumed

Purpose: Synchronize subscription state changes and trigger downstream actions.

Business Logic

flowchart TD
A[Subscription Event] --> B{Subscription Exists?}
B -->|No| C[Create New Subscription]
C --> D[Load Person Contact]
D --> E[Load Typeform from Price/Product]
E --> F[Save New Subscription]
B -->|Yes| G[Load Existing Subscription]
F --> H[Load Active Price]
G --> H
H --> I{Status Change?}
I -->|Active & Not Canceled| J[Complete Cancellation Pulses]
I -->|Canceled| K[Complete Cancellation Pulses]
I -->|No Change| L[Update Subscription Data]
J --> M{Software Upgrade?}
K --> N[Remove Pending Tasks]
M -->|Yes| O[Clear Feedback & Downgrade Queue]
M -->|No| L
O --> L
N --> L
L --> P[Update Order Metadata]
P --> Q{Status Active?}
Q -->|Yes| R[Complete Past Due Tasks/Pulses]
Q -->|Canceled| S[Delete Past Due Tasks/Pulses]
R --> T[Update Invoice Link]
S --> T
T --> U[Return Success]

Key Operations

1. New Subscription Creation

if (!currentSub) {
let personContact;
let typeform;

// Find person contact for onboarding email
const account = await Account.findById(req.body.data.object.metadata?.account_id).lean();

if (account) {
personContact = await Contact.findOne({
businesses: new mongoose.Types.ObjectId(account.business),
});
}

// Load typeform from price or product
const price = await StorePrice.findOne({
stripe_id: plan.id,
}).lean();

if (price?.metadata?.typeform) {
typeform = price?.metadata?.typeform;
} else {
const product = await StoreProduct.findOne({
stripe_id: plan.product,
}).lean();
typeform = product?.metadata?.typeform;
}

const newSub = await new StoreSubscription({
stripe_id: req.body.data.object.id,
account: new mongoose.Types.ObjectId(metadata.main_account_id),
...(personContact && { person: personContact._id }),
...(typeform && { typeform }),
...req.body.data.object,
}).save();
}

2. Pulse Completion on Active/Canceled

if (
(req.body.data.object.status == 'active' && !req.body.data.object.canceled_at) ||
req.body.data.object.status == 'canceled'
) {
const pulse = await ProjectsPulse.updateMany(
{
type: 'subscription_cancellation',
subscription_id: currentSub.id,
status: 'pending',
},
{
$set: {
status: 'completed',
completed_at: Date.now(),
},
},
);

const updatedPulses = await ProjectsPulse.find({
type: 'subscription_cancellation',
subscription_id: currentSub.id,
status: 'completed',
}).lean();

if (updatedPulses.length > 0) {
await createCommunicationRecord(updatedPulses);
}
}

3. Software Upgrade Logic

const plan = req.body.data.object.plan;

// If software tier increased, clear downgrade queue and feedback
if (plan.metadata.software === 'true' && currentSub.plan.metadata.tier < plan.metadata.tier) {
// Delete subscription feedback
await SubscriptionFeedback.deleteOne({
subscription: new mongoose.Types.ObjectId(currentSub.id),
account: new mongoose.Types.ObjectId(currentSub.metadata.account_id),
});

// Delete downgrade queue
await Queue.deleteOne({
account_id: currentSub.metadata.account_id,
status: 'pending',
in_progress: false,
});

// Clear downgrade.plan field
await Account.findByIdAndUpdate(
{ _id: currentSub.metadata.account_id },
{ $unset: { 'downgrade.plan': 1 } },
);
}

4. Order Metadata Sync

const order = await StoreOrder.findOne({
subscription: sub.id,
}).lean();

if (order) {
const price = await StorePrice.findOne({
stripe_id: sub.plan.id,
}).populate({ path: 'product' });

const product = price.product;

let metadata = {
...(order?.metadata || {}),
product_name: product._doc?.name,
price_name: price.nickname,
unit_amount: price.unit_amount,
currency: price.currency,
images: product._doc?.images,
product_type: product._doc?.metadata?.product_type,
interval: price.recurring.interval_count,
current_period_end: sub?.current_period_end,
};

await StoreOrder.updateOne({ _id: order._id }, { $set: flattenObject({ metadata }) });
}

5. Task Cleanup on Status Change

// Active: Complete past_due tasks
if (sub.status == 'active') {
await ProjectsTasks.updateMany(
{ order_id: order._id, type: 'subscription_past_due' },
{ $set: { status: 'completed' } },
);

await ProjectsPulse.updateMany(
{ order_id: order._id, type: 'payment_failed' },
{ $set: { status: 'completed', completed_at: Date.now() } },
);
}

// Canceled: Delete past_due tasks and mark pending tasks removed
if (sub.status == 'canceled') {
await ProjectsTasks.deleteMany({
order_id: order._id,
type: 'subscription_past_due',
});

await ProjectsPulse.deleteMany({
order_id: order._id,
type: 'payment_failed',
});

await ProjectsTasks.updateMany({ order_id: order._id, status: 'pending' }, { removed: true });
}

Important Notes

  • Person Assignment: New subscriptions automatically link to business contact for onboarding
  • Typeform Inheritance: Typeform config loaded from price first, fallback to product
  • Pulse Completion: Creates communication records for completed pulses
  • Software Upgrades: Automatically clear downgrade queue when tier increases
  • Task Management: Status changes trigger task completion or removal

4. cancelSubscription() - Subscription Cancellation

Handles: customer.subscription.deleted

Purpose: Handle subscription cancellation with resource cleanup and fulfillment queuing.

Business Logic

flowchart TD
A[Cancellation Event] --> B[Load Subscription with Relations]
B --> C{Product Type?}
C -->|listings| D[Queue Listings Cleanup]
C -->|phone_number| E[Queue Number Release]
C -->|site| F[Queue Site Deactivation]
C -->|Other| G[Update Subscription Status]
D --> G
E --> G
F --> G
G --> H{Software Subscription?}
H -->|Yes| I[Mark Domains Canceled]
H -->|No| J[Mark Order Inactive]
I --> K[Create Cancellation Activity]
J --> L[Remove Pending Tasks]
L --> K
K --> M[Update has_orders Flag]
M --> N[Delete Pending Pulses]
N --> O[Return Success]

Key Operations

1. Product-Specific Cleanup Queuing

const type = sCheck.price?.product?._doc?.metadata?.product_type;

let user = await User.findOne({
account: new mongoose.Types.ObjectId(sCheck.metadata.account_id._id),
is_owner: true,
});

switch (type) {
case 'listings':
await new Queue({
account_id: sCheck.metadata.account_id._id,
parent_account: sCheck.account._id,
additional_data: { type: 'listings' },
user_id: user?._id,
client_id: user?._id,
source: 'subscription-cancel',
}).save();
break;

case 'phone_number':
await new Queue({
account_id: sCheck.metadata.account_id._id,
parent_account: sCheck.account._id,
additional_data: {
type: 'numbers',
number: sCheck._doc.metadata.action_value,
},
user_id: user?._id,
client_id: user?._id,
source: 'subscription-cancel',
}).save();
break;

case 'site':
await new Queue({
account_id: sCheck.metadata.account_id._id,
parent_account: sCheck.account._id,
additional_data: {
type: 'sites',
sub_type: sCheck._doc.metadata.action_type,
id: sCheck._doc.metadata.internal_product_id,
},
user_id: user?._id,
client_id: user?._id,
source: 'subscription-cancel',
}).save();
break;
}

2. Software Domain Cancellation

if (req.body.data?.object.plan.metadata.software === 'true') {
await LightningDomain.updateMany(
{
parent_account: new mongoose.Types.ObjectId(sub.metadata.account_id),
},
{ cancel: true },
);
}

3. Order Status Update

if (req.body.data?.object.plan.metadata.software !== 'true') {
const order = await StoreOrder.findOneAndUpdate(
{
subscription: sCheck._id,
seller_account: sCheck.account,
buyer_account: sCheck.metadata?.account_id,
},
{ status: 'inactive' },
{ new: true },
);

if (order) {
// Remove all pending tasks
await ProjectsTasks.updateMany({ order_id: order._id, status: 'pending' }, { removed: true });

// Delete pending pulses
await ProjectsPulse.deleteMany({
order_id: order._id,
status: 'pending',
});
}
}

4. has_orders Flag Management

const latestOrder = await StoreOrder.findOne({
subscription: subscriptionId,
})
.sort({ _id: -1 })
.lean();

// Query active orders for sub account
const subAccountOrders = await StoreOrder.find({
buyer_account: latestOrder.buyer_account,
status: 'active',
}).lean();

// Query active orders for main account (excluding current sub account)
const mainAccountOrders = await StoreOrder.find({
seller_account: latestOrder.seller_account,
buyer_account: { $ne: latestOrder.buyer_account },
status: 'active',
}).lean();

const updates = [];

// Update sub account has_orders
if (latestOrder.buyer_account) {
const hasOrders = subAccountOrders.length > 0;
updates.push(
Account.findOneAndUpdate(
{
_id: latestOrder.buyer_account,
has_orders: { $exists: true },
},
{ has_orders: hasOrders },
),
);

// Set became_customer_on if first order
if (hasOrders) {
setBecameCustomerOn(latestOrder.buyer_account);
}
}

// Update main account has_orders
if (latestOrder.seller_account) {
const hasActiveOrders = mainAccountOrders.length > 0 || subAccountOrders.length > 0;

updates.push(
Account.findOneAndUpdate({ _id: latestOrder.seller_account }, { has_orders: hasActiveOrders }),
);

if (hasActiveOrders) {
setBecameCustomerOn(latestOrder.seller_account);
}
}

await Promise.all(updates);

5. Pulse Cleanup

// Delete onboarding, review, and checkin pulses
await ProjectsPulse.deleteMany({
account_id: latestOrder.seller_account,
type: 'schedule_onboarding',
});

await ProjectsPulse.deleteMany({
account_id: latestOrder.seller_account,
type: 'review_request',
});

await ProjectsPulse.deleteMany({
account_id: latestOrder.seller_account,
type: 'quarterly_checkin',
});

// Delete specific subscription cancellation pulse
if (sub.status === 'canceled') {
await ProjectsPulse.findOneAndDelete({
type: 'subscription_cancellation',
subscription_id: sub.id,
status: 'pending',
});
}

Important Notes

  • Cleanup Queues: Product type determines cleanup action (listings, phone numbers, sites)
  • Owner Lookup: Queue jobs require owner user for proper assignment
  • Software Handling: Software subscriptions cancel domains, non-software deactivates orders
  • has_orders Flag: Recalculated based on remaining active orders
  • became_customer_on: Set when account first gets active orders
  • Pulse Cleanup: Removes future scheduled pulses (onboarding, reviews, checkins)

5. updateProduct() - Product Sync

Handles: product.created, product.updated, product.deleted

Purpose: Mirror Stripe product changes to MongoDB.

let conditions = {
stripe_id: req.body.data.object.id,
};

if (connected_account != 'platform') {
conditions.connected_account = connected_account;
}

let currentProd = await StoreProduct.findOne(conditions);

let prod = {
...(currentProd || {}),
...req.body.data.object,
};

delete prod.id;

if (typeof req.account !== 'string') {
prod.account = req.account.id;
}

if (connected_account == 'platform') {
prod.platform_type = 'dashclicks';
}

await StoreProduct.findOneAndUpdate(conditions, { ...prod }, { new: true, upsert: true });

6. updatePrice() - Price Sync

Handles: price.created, price.updated, price.deleted

Purpose: Mirror Stripe price changes to MongoDB with product linkage.

Business Logic

flowchart TD
A[Price Event] --> B{Price Exists?}
B -->|Yes| C[Load Existing Price]
B -->|No| D[Find Related Product]
D --> E{Product Found?}
E -->|No| F[Return 404 Error]
E -->|Yes| G[Create New Price]
C --> H[Update Price Data]
G --> I[Add Price to Product.prices Array]
I --> J[Return Success]
H --> J

Key Operations

1. New Price Creation

if (!currentPrice) {
// Find related product
const product = await StoreProduct.findOne({
stripe_id: req.body.data.object.product,
});

if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}

const { id: stripe_id, ...rest } = req.body.data.object;

if (connected_account == 'platform') {
rest.platform_type = 'dashclicks';
rest.connected_account = 'platform';
}

if (rest.metadata?.additional_info) {
rest.additional_info = rest.metadata.additional_info;
}

// Create price within transaction
const savedPrice = await withTransactionValue(async session => {
const savedPrice = await new StorePrice(
dotNotationToNestedObject({
...rest,
stripe_id,
product: product._id,
}),
).save({ session });

// Add to product's prices array
await StoreProduct.updateOne(
{ stripe_id: req.body.data.object.product },
{ $addToSet: { prices: savedPrice._id } },
{ session },
);

return savedPrice;
});
}

2. Additional Info Extraction

if (req.body.data.object.metadata?.additional_info) {
req.body.data.object.additional_info = req.body.data.object.metadata.additional_info;
}

Important Notes

  • Product Linkage: New prices automatically added to product's prices array
  • Transaction Safety: Price creation and product update happen atomically
  • Metadata Extraction: additional_info extracted from metadata for easier querying
  • Platform Type: Platform prices auto-tagged as 'dashclicks'

7. updateCoupon() / deleteCoupon() - Coupon Sync

Handles: coupon.created, coupon.updated, coupon.deleted

Purpose: Synchronize coupon state and cleanup cart items on deletion.

// Update Coupon
exports.updateCoupon = async (req, res, next) => {
let updatedCoupon = await stripe.coupons.retrieve(req.body.data.object.id, {
expand: ['applies_to'],
});

updatedCoupon.stripe_id = updatedCoupon.id;
delete updatedCoupon.id;
updatedCoupon.connected_account = 'platform';
updatedCoupon.platform_type = 'dashclicks';

await storeCoupon.updateOne(
{ stripe_id: req.body.data.object.id },
{ $set: updatedCoupon },
{ upsert: true, new: true },
);
};

// Delete Coupon
exports.deleteCoupon = async (req, res, next) => {
let coupon = req.body.data.object;

// Delete coupon record
await storeCoupon.deleteOne({ stripe_id: coupon.id });

// Clear from carts
await storeCart.deleteMany({
coupon_id: coupon.id,
type: 'promocode',
});

// Delete related promo codes
await storePromoCode.deleteMany({
'coupon.id': coupon.id,
});
};

8. updatePromo() - Promo Code Sync

Handles: promotion_code.created, promotion_code.updated

Purpose: Synchronize promotional code state with expanded coupon data.

let updatedPromo = await stripe.promotionCodes.retrieve(req.body.data.object.id, {
expand: ['coupon.applies_to'],
});

updatedPromo.stripe_id = updatedPromo.id;
delete updatedPromo.id;
delete updatedPromo.account;
updatedPromo.connected_account = 'platform';
updatedPromo.platform_type = 'dashclicks';

await storePromoCode.updateOne(
{ stripe_id: req.body.data.object.id },
{ $set: updatedPromo },
{ upsert: true, new: true },
);

9. newDispute() / updateDispute() - Dispute Handling

Handles: charge.dispute.created, charge.dispute.updated, charge.dispute.closed

Purpose: Track chargebacks and disputes with customer linkage.

// New Dispute
exports.newDispute = async (req, res, next) => {
let connected_account;
if (typeof req.account === 'string') connected_account = 'platform';
else connected_account = req.account.stripe_connected_account;

let dispute = {
stripe_id: req.body.data.object.id,
connected_account,
...req.body.data.object,
};

// Expand charge to get customer
let expandedDispute;
if (connected_account == 'platform') {
expandedDispute = await stripe.disputes.retrieve(dispute.stripe_id, { expand: ['charge'] });
} else {
expandedDispute = await stripe.disputes.retrieve(
dispute.stripe_id,
{ expand: ['charge'] },
{ stripeAccount: connected_account },
);
}

// Find disputing customer
let customer;
if (expandedDispute) {
if (connected_account != 'platform') {
customer = await Account.findOne({
parent_account: req.account.id,
stripe_customer: expandedDispute.charge.customer,
});
customer = customer?._id;
} else {
customer = req.account.id;
}
}

await new StoreDispute({
disputing_account: customer,
...dispute,
}).save();
};

// Update Dispute
exports.updateDispute = async (req, res, next) => {
await StoreDispute.findOneAndUpdate(
{
stripe_id: dispute.stripe_id,
connected_account,
},
{ ...dispute },
{ new: true, upsert: true },
);
};

10. newPayout() / updatePayout() - Payout Tracking

Handles: payout.created, payout.updated, payout.paid, payout.failed

Purpose: Track connected account payouts with destination and balance transaction data.

// New Payout
exports.newPayout = async (req, res, next) => {
let payout = {
stripe_id: req.body.data.object.id,
connected_account,
...req.body.data.object,
};

// Expand destination and balance transaction
let expandedPayout;
if (connected_account == 'platform') {
expandedPayout = await stripe.payouts.retrieve(payout.stripe_id, {
expand: ['destination', 'balance_transaction'],
});
} else {
expandedPayout = await stripe.payouts.retrieve(
payout.stripe_id,
{ expand: ['destination', 'balance_transaction'] },
{ stripeAccount: connected_account },
);
}

payout = {
...payout,
...expandedPayout,
};

await new StorePayout(payout).save();
};

// Update Payout
exports.updatePayout = async (req, res, next) => {
let expandedPayout = await stripe.payouts.retrieve(
payout.stripe_id,
{ expand: ['destination', 'balance_transaction'] },
connected_account == 'platform' ? {} : { stripeAccount: connected_account },
);

await StorePayout.findOneAndUpdate(
{ stripe_id: payout.stripe_id, connected_account },
{ ...payout, ...expandedPayout },
{ new: true, upsert: true },
);
};

🔧 Helper Functions

setBecameCustomerOn(accountId)

Purpose: Set the became_customer_on timestamp when an account gets their first subscription.

const setBecameCustomerOn = async accountId => {
// Check if already set
const account = await Account.findById(accountId, {
became_customer_on: 1,
}).lean();

if (!account?.became_customer_on) {
// Find oldest subscription
const oldestSubscription = await StoreSubscription.findOne({
$or: [
{ 'metadata.account_id': accountId },
{ 'metadata.account_id': new mongoose.Types.ObjectId(accountId) },
],
})
.sort({ created: 1 })
.select('created')
.lean();

if (oldestSubscription) {
const becameCustomerDate =
oldestSubscription.created instanceof Date
? oldestSubscription.created
: new Date(oldestSubscription.created * 1000);

await Account.updateOne(
{ _id: accountId },
{ $set: { became_customer_on: becameCustomerDate } },
);
}
}
};

createCommunicationRecord(updatedPulses)

Purpose: Create communication records for completed pulses.

const createCommunicationRecord = async updatedPulses => {
await Promise.all(
updatedPulses.map(async pulse => {
const { _id: communicationId } = await Communications.create({
origin: 'projects',
task_id: pulse._id,
sent_by: pulse.userId,
use_credit: false,
account_id: pulse.parent_account,
module: 'pulses',
message_type: 'text',
type: 'status_change',
body: `This pulse was completed on {{TIMESTAMP}}`,
status: 'completed',
});

await ProjectsPulse.findByIdAndUpdate(pulse._id, {
$push: { 'metadata.communications': communicationId },
});
}),
);
};

🎯 Webhook Event Mapping

Invoice Events

Stripe EventHandlerPrimary Actions
invoice.createdupdateInvoice()Sync metadata, flag sub-account charges
invoice.updatedupdateInvoice()Update invoice data
invoice.finalizedupdateInvoice()Mark invoice finalized
invoice.paidupdateInvoice()Mark invoice paid
invoice.payment_failedupdateInvoice()Track attempt, create activity
invoice.deleteddeleteInvoice()Remove invoice record
invoice.voideddeleteInvoice()Remove voided invoice

Subscription Events

Stripe EventHandlerPrimary Actions
customer.subscription.createdupdateSubscription()Create subscription, assign person/typeform
customer.subscription.updatedupdateSubscription()Sync status, complete pulses, update order
customer.subscription.pausedupdateSubscription()Update status
customer.subscription.resumedupdateSubscription()Update status, complete past_due tasks
customer.subscription.deletedcancelSubscription()Queue cleanup, cancel domains, remove tasks

Product/Price Events

Stripe EventHandlerPrimary Actions
product.createdupdateProduct()Create product mirror
product.updatedupdateProduct()Update product data
product.deletedupdateProduct()Mark product deleted
price.createdupdatePrice()Create price, link to product
price.updatedupdatePrice()Update price data
price.deletedupdatePrice()Mark price deleted

Discount Events

Stripe EventHandlerPrimary Actions
coupon.createdupdateCoupon()Create coupon mirror
coupon.updatedupdateCoupon()Update coupon data
coupon.deleteddeleteCoupon()Remove coupon, clear carts, delete promo codes
promotion_code.createdupdatePromo()Create promo code mirror
promotion_code.updatedupdatePromo()Update promo code data

Financial Events

Stripe EventHandlerPrimary Actions
charge.dispute.creatednewDispute()Create dispute record, link customer
charge.dispute.updatedupdateDispute()Update dispute status
charge.dispute.closedupdateDispute()Mark dispute closed
payout.creatednewPayout()Create payout record with details
payout.updatedupdatePayout()Update payout status
payout.paidupdatePayout()Mark payout paid
payout.failedupdatePayout()Mark payout failed

🔐 Security & Validation

Webhook Signature Verification

All webhooks must pass Stripe signature verification before processing (handled by route middleware).

Platform Validation

Line Item Check: Ensures webhook events belong to current platform by validating price/product existence.

let pCheck = await StorePrice.findOne({
stripe_id: line.plan?.id || line.price?.id,
}).populate({ path: 'product' });

if (!pCheck) {
throw new Error('WEBHOOK NOT FOR CURRENT PLATFORM');
}

Connected Account Handling

Account Type Detection:

let connected_account;
if (typeof req.account === 'string') {
connected_account = 'platform';
} else {
connected_account = req.account.stripe_connected_account;
}

Idempotency

All webhook handlers use findOneAndUpdate with upsert: true to handle duplicate webhook deliveries safely.


📊 Downstream Effects

Queue Jobs Spawned

EventQueue TypePurpose
Subscription Canceled (listings)source: 'subscription-cancel'Clean up listing integrations
Subscription Canceled (phone_number)source: 'subscription-cancel'Release Twilio phone number
Subscription Canceled (site)source: 'subscription-cancel'Deactivate website

Activities Created

EventActivity TypeEvent Type
Invoice Payment Failedsubscription_statussubscription_past_due
Subscription Canceledsubscription_statussubscription_canceled

Pulses Managed

EventPulse TypeAction
Subscription Active/Canceledsubscription_cancellationComplete pulse
Subscription Activepayment_failedComplete pulse
Subscription Activesubscription_past_dueComplete pulse
Subscription Canceledschedule_onboardingDelete pulse
Subscription Canceledreview_requestDelete pulse
Subscription Canceledquarterly_checkinDelete pulse

Tasks Updated

EventTask TypeAction
Subscription Activesubscription_past_dueMark completed
Subscription CanceledAny pendingMark removed

⚠️ Important Notes

Critical Business Rules

  1. Stripe as Source of Truth: All mutations happen in Stripe first; webhooks sync to MongoDB
  2. Idempotent Processing: Handlers must safely handle duplicate webhook deliveries
  3. Platform Validation: Webhooks validated against local price/product catalog
  4. Payment Retry Tracking: payment_attempts array tracks all failure timestamps
  5. Sub-Account Billing: Separate charge logic for sub-accounts billed through parent
  6. Software Upgrades: Automatically clear downgrade queue when tier increases
  7. Pulse Completion: Completed pulses create communication records for audit trail
  8. has_orders Calculation: Recalculated on every subscription cancellation
  9. became_customer_on: Set only once when account gets first subscription

Error Handling

  • Duplicate Key Errors: Set errno: 200 to prevent duplicate webhook processing errors
  • Platform Mismatch: Return HTTP 200 with error message to prevent Stripe retries
  • Missing Data: Log errors but continue processing to avoid blocking webhook queue

Performance Considerations

  • Bulk Operations: Use updateMany for pulse/task cleanup
  • Parallel Queries: Use Promise.all for independent operations
  • Lean Queries: Use .lean() for read-only operations
  • Selective Population: Only populate required fields


Last Updated: October 8, 2025
Status: Production-Ready ✅

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM