Repository navigation
Description
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/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). - Filter by Dodo brand: pass
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. - 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, 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. - 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 inprocessor.tsneed 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
ignoredand 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_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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-]Ignore other brands' Dodo webhooks instead of quarantining and paging[/-][+]Ignore other brands' Dodo webhooks and quarantine unknown products with operation_quarantined[/+]s3rot0n1n commentedon Oct 27, 2026
Closing: the fix is in place in code and on prod and QA.
@codelitdev/billingis on 0.4.2 and SendLit uses the package's webhook inbox (runWebhookInboxBatch). The customprocessor.tsis gone, andbilling_unknown_provider_productis no longer thrown anywhere. Unknown products now go through the package'soperation_quarantinedpath.DODO_BRAND_IDis passed tocreateDodoBillingProvider, documented in both.env.examplefiles, and set in the infra env templates for prod and QA.billing webhook-inspect) that SendLit's events carry the configured brand ID and areprocessed. The API restarted cleanly with the ID set.Not yet seen live: a foreign brand's event showing
ignored. It will appear on MediaLit's first paid checkout.Remaining gap, to be tracked separately:
alerts.tsstill countsquarantinedrows straight frombilling_webhook_eventsand pages on every check. It should readbilling.health().quarantinedWebhooksand page once per new quarantine. SendLit also has no test of its own for a foreign-brand event being stored asignored.