Problem
SendLit, MediaLit and CourseLit sell through one Dodo Payments business, each under its own Dodo brand. Dodo sends every event of the business to every webhook endpoint, so SendLit's webhook also receives the other products' payment and subscription events.
SendLit's webhook processor (apps/api/src/billing/webhooks/processor.ts) handles an event for a product it does not know differently from the platform standard:
|
SendLit today |
Platform standard (@codelitdev/billing) |
| Error |
throw new Error("billing_unknown_provider_product") |
BillingWorkflowError("operation_quarantined") |
| Retries |
Retried with backoff until the attempt budget runs out |
None: the error is final |
| Quarantine |
After the last retry |
On the first attempt, with lastError: "operation_quarantined" |
Nothing is applied to any account, but alerts.ts pages webhook_quarantined for each of these events, so every ordinary MediaLit or CourseLit payment would page SendLit's on-call. SendLit's processor predates the package's and has drifted from it.
Proposed fix
- Upgrade
@codelitdev/billing from 0.1.0-alpha.3 to the release that adds brand filtering (platform ADR 0009). That release also schedules cancellations at the end of the paid period and adds resumeCancellation (ADR 0010).
- Filter by Dodo brand: pass
brandId to createDodoBillingProvider, for example from a new DODO_BRAND_ID environment variable set to SendLit's brand ID. Events from other brands are then stored as ignored when they arrive. They are never processed, retried, quarantined or paged.
- Check product assignment: confirm in Dodo that SendLit's products are assigned to the SendLit brand and not the business's primary brand. Events carry the primary brand's ID when a product has no brand of its own.
- Use the platform error for unknown products: in
processor.ts, replace throw new Error("billing_unknown_provider_product") with throw new BillingWorkflowError("operation_quarantined") from @codelitdev/billing/core. In the processor's catch, treat operation_quarantined as final, as the package's processWebhookRecord does: set the status to quarantined with lastError: "operation_quarantined" on the first attempt, without scheduling a retry. Keep backoff for transient errors only.
- Move to the package's webhook processing (follow-up): replace the custom processor with
billing.runWebhookInboxBatch, so SendLit follows the platform rules without its own copy. First check which SendLit-specific steps in processor.ts need a place in the package's lifecycle hooks.
- Keep alerting on real problems: after step 2, a quarantined webhook means a real misconfiguration, such as an own-brand product missing from the catalog. Keep paging on it, ideally from
billing.health().quarantinedWebhooks.
Acceptance criteria
- A webhook for another brand's subscription is stored as
ignored and does not page.
- A webhook for a SendLit product still processes as before.
- A webhook for an unknown product under SendLit's own brand is quarantined on the first attempt with
operation_quarantined, is not retried, and pages once.
- No code path throws
billing_unknown_provider_product any more.
DODO_BRAND_ID is documented next to the other Dodo settings.
Timing
Steps 1 to 4 should ship before MediaLit takes its first paid checkout in production. Step 5 can follow.
Problem
SendLit, MediaLit and CourseLit sell through one Dodo Payments business, each under its own Dodo brand. Dodo sends every event of the business to every webhook endpoint, so SendLit's webhook also receives the other products' payment and subscription events.
SendLit's webhook processor (
apps/api/src/billing/webhooks/processor.ts) handles an event for a product it does not know differently from the platform standard:@codelitdev/billing)throw new Error("billing_unknown_provider_product")BillingWorkflowError("operation_quarantined")lastError: "operation_quarantined"Nothing is applied to any account, but
alerts.tspageswebhook_quarantinedfor each of these events, so every ordinary MediaLit or CourseLit payment would page SendLit's on-call. SendLit's processor predates the package's and has drifted from it.Proposed fix
@codelitdev/billingfrom0.1.0-alpha.3to the release that adds brand filtering (platform ADR 0009). That release also schedules cancellations at the end of the paid period and addsresumeCancellation(ADR 0010).brandIdtocreateDodoBillingProvider, for example from a newDODO_BRAND_IDenvironment variable set to SendLit's brand ID. Events from other brands are then stored asignoredwhen they arrive. They are never processed, retried, quarantined or paged.processor.ts, replacethrow new Error("billing_unknown_provider_product")withthrow new BillingWorkflowError("operation_quarantined")from@codelitdev/billing/core. In the processor'scatch, treatoperation_quarantinedas final, as the package'sprocessWebhookRecorddoes: set the status toquarantinedwithlastError: "operation_quarantined"on the first attempt, without scheduling a retry. Keep backoff for transient errors only.billing.runWebhookInboxBatch, so SendLit follows the platform rules without its own copy. First check which SendLit-specific steps inprocessor.tsneed a place in the package's lifecycle hooks.billing.health().quarantinedWebhooks.Acceptance criteria
ignoredand does not page.operation_quarantined, is not retried, and pages once.billing_unknown_provider_productany more.DODO_BRAND_IDis documented next to the other Dodo settings.Timing
Steps 1 to 4 should ship before MediaLit takes its first paid checkout in production. Step 5 can follow.