koala73/worldmonitor · error · ConvexError

DODO_PORTAL_ERROR

DODO_PORTAL_ERROR

Error message

DODO_PORTAL_ERROR

What it means

Thrown in `createCustomerPortalUrlForUser` when the Dodo SDK call `client.customers.customerPortal.create(...)` throws — either a Dodo APIError (non-2xx response) or a transport failure. The underlying cause is logged server-side (`console.error` with the customer id and message), and the call is re-thrown as an object-typed ConvexError (`{ kind: "DODO_PORTAL_ERROR" }`) so the client tags it for Sentry while the user falls back to the generic Dodo portal.

Source

Thrown at convex/payments/billing.ts:470

  } catch (err) {
    // The Dodo REST SDK throws a plain Error (APIError on a non-2xx Dodo
    // response, or a transport failure) when the portal-session create
    // fails. Convex's action runtime then masks any NON-ConvexError throw
    // as an opaque `[Request ID: X] Server Error`, dropping the real cause
    // from the wire — the exact opacity WORLDMONITOR-R5 fought for the
    // missing-customer path above (this was the last unwrapped throw site).
    // Re-throw as a structured ConvexError so the client receives
    // `err.data.kind === 'DODO_PORTAL_ERROR'` for proper Sentry
    // classification (browser → `extractBillingErrorKind` → tag
    // `billing_error_kind`; the user still falls back to the generic Dodo
    // portal), and log the underlying cause here so it survives in the
    // Convex function logs for server-side triage. WORLDMONITOR-ST.
    const cause = err instanceof Error ? err.message : String(err);
    console.error(
      `[billing] Dodo customer-portal create failed for customer ${dodoCustomerId}:`,
      cause,
    );
    throw new ConvexError({ kind: "DODO_PORTAL_ERROR" });
  }

  return { portal_url: session.link };
}

function getSubscriptionStatusPriority(status: string): number {
  switch (status) {
    case "active":
      return 0;
    case "on_hold":
      return 1;
    case "cancelled":
      return 2;
    default:
      return 3;
  }
}

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Retry the portal open after a short delay (transient Dodo 5xx/network often clears).
  2. Check the Convex function logs for the logged underlying cause (`[billing] Dodo customer-portal create failed for customer ...`).
  3. If persistent, verify the customer id is still valid in the Dodo dashboard and that the Dodo account is in good standing.
  4. Surface a fallback 'open Dodo portal directly' link to the user while investigating.

Example fix

// before
const { portal_url } = await getCustomerPortalUrl({});

// after (client-side retry with fallback)
try {
  const { portal_url } = await getCustomerPortalUrl({});
  location.href = portal_url;
} catch (e) {
  if (e.data?.kind === "DODO_PORTAL_ERROR") {
    location.href = "https://pay.dodopayments.com"; // fallback
  } else throw e;
}
Defensive patterns

Strategy: retry

Try / catch

async function openPortal() {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      const { portal_url } = await getCustomerPortalUrl({});
      location.href = portal_url;
      return;
    } catch (e: any) {
      if (e?.data?.kind === "DODO_PORTAL_ERROR" && attempt < 2) {
        await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
        continue;
      }
      location.href = "https://pay.dodopayments.com"; // fallback
      return;
    }
  }
}

Prevention

When it happens

Trigger: Dodo returns a non-2xx for the portal-session create (rate limit, 5xx, bad customer id, account suspended), or the outbound HTTPS request to Dodo fails (timeout, DNS, network). Distinct from NO_CUSTOMER: here a customer id exists but the create call itself failed.

Common situations: Dodo temporary outage or 5xx; transient network blip between Convex and Dodo; customer id is stale/revoked on Dodo's side; rate-limited by Dodo during a burst of portal opens.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/65caa439081b49bc. Report an issue: GitHub.