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)
| Collection | Operations | Purpose |
|---|---|---|
_store.invoices | Upsert, Update | Mirror Stripe invoices with payment attempt tracking |
_store.subscriptions | Upsert, Update, Query | Subscription state synchronization |
_store.orders | Update, Query | Order status and fulfillment linkage |
_store.products | Upsert | Product catalog synchronization |
_store.prices | Upsert, Update | Price tier synchronization |
_store.coupons | Upsert, Delete | Coupon state synchronization |
_store.promo.codes | Upsert, Delete | Promo code synchronization |
_store.disputes | Create, Update | Dispute and chargeback tracking |
_store.payout | Create, Update | Payout event tracking |
_store.cart | Delete | Clear cart items on coupon/promo deletion |
Supporting Collections (Read/Update)
| Collection | Operations | Purpose |
|---|---|---|
_accounts | Update | Set has_orders, became_customer_on, downgrade.plan |
queues | Create, Update | Spawn fulfillment, downgrade, cleanup jobs |
projects-tasks | Create, Update, Delete | Task lifecycle management |
projects-pulses | Update, Delete | Pulse completion and cleanup |
communications | Create | Activity audit trail |
lightning.domains | Update | Software subscription domain cancellation |
store-subscription-feedback | Delete | Clear feedback on upgrades |
crm.contacts | Query | Person assignment for new subscriptions |
_users | Query | Owner 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
findOneAndUpdatewithupsert: trueto handle duplicate webhook deliveries - Payment Attempts: Tracks all failure timestamps in
payment_attemptsarray 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
pricesarray - Transaction Safety: Price creation and product update happen atomically
- Metadata Extraction:
additional_infoextracted 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 Event | Handler | Primary Actions |
|---|---|---|
invoice.created | updateInvoice() | Sync metadata, flag sub-account charges |
invoice.updated | updateInvoice() | Update invoice data |
invoice.finalized | updateInvoice() | Mark invoice finalized |
invoice.paid | updateInvoice() | Mark invoice paid |
invoice.payment_failed | updateInvoice() | Track attempt, create activity |
invoice.deleted | deleteInvoice() | Remove invoice record |
invoice.voided | deleteInvoice() | Remove voided invoice |
Subscription Events
| Stripe Event | Handler | Primary Actions |
|---|---|---|
customer.subscription.created | updateSubscription() | Create subscription, assign person/typeform |
customer.subscription.updated | updateSubscription() | Sync status, complete pulses, update order |
customer.subscription.paused | updateSubscription() | Update status |
customer.subscription.resumed | updateSubscription() | Update status, complete past_due tasks |
customer.subscription.deleted | cancelSubscription() | Queue cleanup, cancel domains, remove tasks |
Product/Price Events
| Stripe Event | Handler | Primary Actions |
|---|---|---|
product.created | updateProduct() | Create product mirror |
product.updated | updateProduct() | Update product data |
product.deleted | updateProduct() | Mark product deleted |
price.created | updatePrice() | Create price, link to product |
price.updated | updatePrice() | Update price data |
price.deleted | updatePrice() | Mark price deleted |
Discount Events
| Stripe Event | Handler | Primary Actions |
|---|---|---|
coupon.created | updateCoupon() | Create coupon mirror |
coupon.updated | updateCoupon() | Update coupon data |
coupon.deleted | deleteCoupon() | Remove coupon, clear carts, delete promo codes |
promotion_code.created | updatePromo() | Create promo code mirror |
promotion_code.updated | updatePromo() | Update promo code data |
Financial Events
| Stripe Event | Handler | Primary Actions |
|---|---|---|
charge.dispute.created | newDispute() | Create dispute record, link customer |
charge.dispute.updated | updateDispute() | Update dispute status |
charge.dispute.closed | updateDispute() | Mark dispute closed |
payout.created | newPayout() | Create payout record with details |
payout.updated | updatePayout() | Update payout status |
payout.paid | updatePayout() | Mark payout paid |
payout.failed | updatePayout() | 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
| Event | Queue Type | Purpose |
|---|---|---|
| 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
| Event | Activity Type | Event Type |
|---|---|---|
| Invoice Payment Failed | subscription_status | subscription_past_due |
| Subscription Canceled | subscription_status | subscription_canceled |
Pulses Managed
| Event | Pulse Type | Action |
|---|---|---|
| Subscription Active/Canceled | subscription_cancellation | Complete pulse |
| Subscription Active | payment_failed | Complete pulse |
| Subscription Active | subscription_past_due | Complete pulse |
| Subscription Canceled | schedule_onboarding | Delete pulse |
| Subscription Canceled | review_request | Delete pulse |
| Subscription Canceled | quarterly_checkin | Delete pulse |
Tasks Updated
| Event | Task Type | Action |
|---|---|---|
| Subscription Active | subscription_past_due | Mark completed |
| Subscription Canceled | Any pending | Mark removed |
⚠️ Important Notes
Critical Business Rules
- Stripe as Source of Truth: All mutations happen in Stripe first; webhooks sync to MongoDB
- Idempotent Processing: Handlers must safely handle duplicate webhook deliveries
- Platform Validation: Webhooks validated against local price/product catalog
- Payment Retry Tracking:
payment_attemptsarray tracks all failure timestamps - Sub-Account Billing: Separate charge logic for sub-accounts billed through parent
- Software Upgrades: Automatically clear downgrade queue when tier increases
- Pulse Completion: Completed pulses create communication records for audit trail
- has_orders Calculation: Recalculated on every subscription cancellation
- became_customer_on: Set only once when account gets first subscription
Error Handling
- Duplicate Key Errors: Set
errno: 200to 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
updateManyfor pulse/task cleanup - Parallel Queries: Use
Promise.allfor independent operations - Lean Queries: Use
.lean()for read-only operations - Selective Population: Only populate required fields
🔗 Related Documentation
- Subscription Management - Subscription lifecycle operations
- Order Management - Order tracking and fulfillment
- Product Management - Product catalog management
- Cart Management (link removed - file does not exist) - Checkout and cart operations
Last Updated: October 8, 2025
Status: Production-Ready ✅