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
- Log the raw GET /v1/workers/billing body and compare against getBillingSummary's required fields
- Confirm the request includes a valid token and that billing is provisioned for that account on the Den server
- Try without the excludePortal/excludeInvoices flags to see if the reduced payload is what fails the parser
- Align server and client versions if the billing schema changed
- 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
- Call getBillingStatus with default options (include portal and invoices) before using exclusion flags
- Confirm billing is provisioned on your Den deployment before relying on billing UI
- Check the token has access to the billing/worker context
- Log raw /v1/workers/billing responses in dev to detect schema drift
- Handle DenApiError code invalid_billing_payload with a graceful empty-billing state
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
- invalid_mcp_connection_payload
- invalid_marketplace_payload
- invalid_plugin_payload
- Billing response was incomplete.
- Seat billing checkout response did not include a URL.
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/823dda98361ce3aa.
Report an issue: GitHub.