koala73/worldmonitor · error · ConvexError
CUSTOMER_PROVENANCE_REQUIRED
CUSTOMER_PROVENANCE_REQUIRED
Error message
CUSTOMER_PROVENANCE_REQUIRED
What it means
When deleting/retaining a subscription, the code resolves the Dodo customer ID from the subscription or raw webhook data and requires a non-empty value: the current user-customer mapping does not prove which customer a subscription belongs to (rows get reassigned by later webhooks). Without an attributable customerId, the deletion cannot preserve provenance, so it throws and the subscription is kept until the customer can be repaired from provider/audit evidence.
Solutions
- Repair the subscription's dodoCustomerId from provider (Dodo API) or audit records, then retry the deletion.
- If processing a webhook, verify the payload includes the customer id before calling this path.
- Check why sub.dodoCustomerId was never written — fix the subscription-creation/webhook handler that should persist it.
Example fix
// before (webhook handler)
await handleSubscriptionDeleted({ id: body.data.id }); // customer id dropped
// after
await handleSubscriptionDeleted({ id: body.data.id, dodoCustomerId: body.data.customer_id }); Defensive patterns
Strategy: validation
Validate before calling
const customerId = sub.dodoCustomerId ?? body?.data?.customer_id;
if (typeof customerId !== "string" || !customerId.trim()) {
throw new Error("webhook payload missing customer id; repair before deleting subscription");
} Try / catch
try {
await deleteSubscription(args);
} catch (e) {
if (isConvexError(e) && e.data?.kind === "CUSTOMER_PROVENANCE_REQUIRED") {
await repairCustomerFromProvider(e.data.subscriptionId); // Dodo API / audit, then retry
}
} Prevention
- Validate Dodo webhook payloads include customer_id before enqueueing handlers.
- Persist dodoCustomerId at subscription creation and on every webhook update.
- Alert on subscriptions with missing dodoCustomerId before deletion is attempted.
When it happens
Trigger: Processing a deletion (webhook or mutation) where sub.dodoCustomerId is missing and rawCustomerId is absent, empty, or not a string, leaving customerId as "" after trim.
Common situations: Dodo webhook payload malformed or missing customer field; subscription created before customer-ID propagation was added; test fixtures omitting dodoCustomerId; webhook replay with a stripped payload.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- DNS ${recordType} lookup failed: HTTP ${response.status}
- DNS ${recordType} lookup failed: status ${data.Status}
- callbackUrl is not a valid URL
- callbackUrl must use https
- callbackUrl hostname is a blocked metadata endpoint
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/da639127a62c1011.
Report an issue: GitHub.
Appendix: source
Thrown at convex/payments/billing.ts:4255
q.eq("dodoSubscriptionId", args.dodoSubscriptionId),
)
.unique();
if (!sub) {
throw new Error(
`[billing] deleteSubscriptionByDodoId: no subscription found with dodoSubscriptionId="${args.dodoSubscriptionId}"`,
);
}
const userId = sub.userId;
const rawCustomerId = (sub.rawPayload as { customer?: { customer_id?: unknown } } | null)
?.customer?.customer_id;
const customerId = sub.dodoCustomerId ||
(typeof rawCustomerId === "string" ? rawCustomerId : "");
// A user's current customer mapping does not prove this subscription's
// customer: shared customer rows are reassigned by later webhooks. Preserve
// the subscription until its customer can be repaired from provider/audit evidence.
if (!customerId.trim()) {
throw new ConvexError({ kind: "CUSTOMER_PROVENANCE_REQUIRED" });
}
if (customerId) {
const retainedOwner = await ctx.db.query("deletedSubscriptionCustomers")
.withIndex("by_customer_user", (q) => q.eq("dodoCustomerId", customerId).eq("userId", userId))
.first();
if (!retainedOwner) {
await ctx.db.insert("deletedSubscriptionCustomers", { userId, dodoCustomerId: customerId });
}
}
// Index prefix — deliberately unfiltered by cohort so deleting a
// subscription reaps BOTH its day-0 and retro presentation rows.
const presentations = await ctx.db
.query("proActivationPresentations")
.withIndex("by_subscription_cohort", (q) => q.eq("subscriptionId", sub._id))
.collect();
for (const presentation of presentations) {
await ctx.db.delete(presentation._id);
}View on GitHub (pinned to 7d06c8633d)