different-ai/openwork · error · DenApiError

invalid_billing_payload

invalid_billing_payload

Error message

Billing response was missing details.

What it means

DenApiError thrown by getBillingStatus in apps/app/src/app/lib/den.ts when GET /v1/workers/billing returned 2xx but getBillingSummary could not build a DenBillingSummary from the payload — the billing details (plan/subscription/invoice fields the parser requires) were missing or malformed. This prevents the UI from rendering a half-populated billing page.

Source

Thrown at apps/app/src/app/lib/den.ts:3522

    },

    async getBillingStatus(options: { includePortal?: boolean; includeInvoices?: boolean } = {}): Promise<DenBillingSummary> {
      const params = new URLSearchParams();
      if (options.includePortal === false) {
        params.set("excludePortal", "1");
      }
      if (options.includeInvoices === false) {
        params.set("excludeInvoices", "1");
      }

      const path = params.size > 0 ? `/v1/workers/billing?${params.toString()}` : "/v1/workers/billing";
      const payload = await requestJson<unknown>(baseUrls, path, {
        method: "GET",
        token,
      });
      const summary = getBillingSummary(payload);
      if (!summary) {
        throw new DenApiError(500, "invalid_billing_payload", "Billing response was missing details.");
      }
      return summary;
    },

    async updateSubscriptionCancellation(cancelAtPeriodEnd: boolean): Promise<{ subscription: DenBillingSubscription | null; billing: DenBillingSummary }> {
      const payload = await requestJson<unknown>(baseUrls, "/v1/workers/billing/subscription", {
        method: "POST",
        token,
        body: { cancelAtPeriodEnd },
      });
      const billing = getBillingSummary(payload);
      if (!billing) {
        throw new DenApiError(500, "invalid_billing_payload", "Subscription update response was missing billing details.");
      }

      return {
        subscription: isRecord(payload) ? getBillingSubscription(payload.subscription) : null,
        billing,

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Log the raw GET /v1/workers/billing body and compare against getBillingSummary's required fields
  2. Confirm the request includes a valid token and that billing is provisioned for that account on the Den server
  3. Try without the excludePortal/excludeInvoices flags to see if the reduced payload is what fails the parser
  4. Align server and client versions if the billing schema changed
  5. Rule out proxies/gateways rewriting the response to HTML

Example fix

// before
const summary = getBillingSummary(payload);
if (!summary) {
  throw new DenApiError(500, "invalid_billing_payload", "Billing response was missing details.");
}
// after
caller-side:
try {
  const billing = await client.getBillingStatus({ includePortal: true, includeInvoices: true });
} catch (err) {
  if (err instanceof DenApiError && err.code === "invalid_billing_payload") {
    // show a 'billing unavailable' state instead of crashing
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// ensure the response looks like billing data before trusting it
function looksLikeBilling(v: unknown): boolean {
  return typeof v === "object" && v !== null && Object.keys(v).length > 0;
}
// also: request full payload first time (omit excludePortal/excludeInvoices)

Type guard

function isBillingSummary(v: unknown): v is DenBillingSummary {
  return (
    typeof v === "object" && v !== null &&
    "plan" in v // narrow further to match getBillingSummary's required fields
  );
}

Try / catch

try {
  const billing = await client.getBillingStatus();
} catch (err) {
  if (err instanceof DenApiError && err.code === "invalid_billing_payload") {
    // render a billing-unavailable state; optionally retry once without exclude params
  } else { throw err; }
}

Prevention

When it happens

Trigger: GET /v1/workers/billing (with optional excludePortal/excludeInvoices params) responds 200 with an empty object, an error envelope, a non-object body, or a billing object missing the fields getBillingSummary requires (e.g. billing not provisioned for the token's account).

Common situations: Calling billing endpoints with a token that has no worker/billing context; self-hosted Den without the billing module returning an empty body; excludePortal/excludeInvoices flags trimming fields the parser needs; proxy or gateway replacing JSON with a 200 HTML page; server/client schema drift.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/823dda98361ce3aa. Report an issue: GitHub.