Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-clubs-float.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `getOrganizationBillingSubscription` to BillingApi.
Original file line numberDiff line numberDiff line change
Expand Up@@ -229,6 +229,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"backend/commerce-subscription-item-webhook-event-json.mdx",
"backend/commerce-subscription-item.mdx",
"backend/commerce-subscription-webhook-event-json.mdx",
"backend/commerce-subscription.mdx",
"backend/email-address.mdx",
"backend/external-account.mdx",
"backend/get-auth-fn.mdx",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,13 @@ import type { ClerkPaginationRequest } from '@clerk/types';

import { joinPaths } from '../../util/path';
import type { CommercePlan } from '../resources/CommercePlan';
import type { CommerceSubscription } from '../resources/CommerceSubscription';
import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import { AbstractAPI } from './AbstractApi';

const basePath = '/commerce';
const organizationBasePath = '/organizations';

type GetOrganizationListParams = ClerkPaginationRequest<{
payerType: 'org' | 'user';
Expand DownExpand Up@@ -45,4 +47,16 @@ export class BillingAPI extends AbstractAPI {
queryParams: params,
});
}

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
public async getOrganizationBillingSubscription(organizationId: string) {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}
Comment on lines +51 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add explicit return type and complete JSDoc for the public API

Per guidelines, public APIs should declare explicit return types and comprehensive JSDoc.

Apply this diff within the method block:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId - The target organization ID (e.g. "org_...").+ * @returns The organization's billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(organizationId: string): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

I can add an integration test stub that mocks AbstractAPI.request to verify the shape and deserialization path—want me to push that?


🏁 Script executed:

#!/bin/bash# Verify that the new method compiles to a Promise<CommerceSubscription> (textual check)
rg -nP "public\s+async\s+getOrganizationBillingSubscription\([^)]*\)\s*:\s*Promise<CommerceSubscription>" packages/backend/src/api/endpoints/BillingApi.ts -n
# Ensure the endpoint path matches server route conventions elsewhere in the repo
rg -n "billing.*subscription" -C2 packages | sed -n '1,200p'

Length of output: 11395


Add explicit return type and complete JSDoc for getOrganizationBillingSubscription
Public SDK methods must declare explicit return types and comprehensive JSDoc for parameters, return values, and thrown errors per our API guidelines.

– In packages/backend/src/api/endpoints/BillingApi.ts, update the method signature:

 /**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
+ * @param organizationId – The target organization’s ID (e.g. "org_…").+ * @returns The organization’s billing subscription resource.+ * @throws ClerkAPIResponseError If the organization does not exist or billing is not enabled.
*/
- public async getOrganizationBillingSubscription(organizationId: string) {+ public async getOrganizationBillingSubscription(+ organizationId: string+ ): Promise<CommerceSubscription> {
this.requireId(organizationId);
return this.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath, organizationId, 'billing', 'subscription'),
});
}

• Please also verify that the billing/subscription endpoint path aligns exactly with our server’s API specification—​I wasn’t able to locate a matching route definition in the repo.
• Let me know if you’d like me to add an integration‐test stub that mocks AbstractAPI.request to validate the response shape and deserialization path.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
*/
publicasyncgetOrganizationBillingSubscription(organizationId: string){
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,andtheAPI is subjecttochange.
*ItisadvisedtopintheSDKversiontoavoidbreakingchanges.
* @paramorganizationIdThetargetorganization’sID(e.g."org_…").
* @returnsTheorganization’sbillingsubscriptionresource.
* @throwsClerkAPIResponseErrorIftheorganizationdoesnotexistorbillingisnotenabled.
*/
publicasyncgetOrganizationBillingSubscription(
organizationId: string
): Promise<CommerceSubscription>{
this.requireId(organizationId);
returnthis.request<CommerceSubscription>({
method: 'GET',
path: joinPaths(organizationBasePath,organizationId,'billing','subscription'),
});
}
🤖 Prompt for AI Agents
In packages/backend/src/api/endpoints/BillingApi.ts around lines 51-61, the
public method getOrganizationBillingSubscription lacks an explicit return type
and complete JSDoc; update the method signature to explicitly return
Promise<CommerceSubscription> and add a JSDoc block that documents the
organizationId parameter, the Promise<CommerceSubscription> return value, and
any errors that can be thrown (e.g., validation errors from requireId and
network/request errors from AbstractAPI.request). While changing the
signature/JSDoc, verify the endpoint path "billing/subscription" against the
server route definitions (search the repo for matching routes) and correct the
joinPaths call if the server uses a different path (e.g.,
"billing/subscriptions" or a nested route); if you change the path, mirror that
in the JSDoc. Finally, run unit/integration tests or add a small test stub that
mocks AbstractAPI.request to assert the response deserializes to
CommerceSubscription.

}
79 changes: 79 additions & 0 deletions packages/backend/src/api/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { type CommerceMoneyAmount } from './CommercePlan';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import type { CommerceSubscriptionJSON } from './JSON';

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version to avoid breaking changes.
*/
export class CommerceSubscription {
constructor(
/**
* The unique identifier for the commerce subscription.
*/
readonly id: string,
/**
* The current status of the subscription.
*/
readonly status: CommerceSubscriptionJSON['status'],
/**
* The ID of the payer for this subscription.
*/
readonly payerId: string,
/**
* Unix timestamp (milliseconds) of creation.
*/
readonly createdAt: number,
/**
* Unix timestamp (milliseconds) of last update.
*/
readonly updatedAt: number,
/**
* Unix timestamp (milliseconds) when the subscription became active.
*/
readonly activeAt: number | null,
/**
* Unix timestamp (milliseconds) when the subscription became past due.
*/
readonly pastDueAt: number | null,
/**
* Array of subscription items in this subscription.
*/
readonly subscriptionItems: CommerceSubscriptionItem[],
/**
* Information about the next scheduled payment.
*/
readonly nextPayment: { date: number; amount: CommerceMoneyAmount } | null,
/**
* Whether the payer is eligible for a free trial.
*/
readonly eligibleForFreeTrial: boolean,
) {}

static fromJSON(data: CommerceSubscriptionJSON): CommerceSubscription {
const nextPayment = data.next_payment
? {
date: data.next_payment.date,
amount: {
amount: data.next_payment.amount.amount,
amountFormatted: data.next_payment.amount.amount_formatted,
currency: data.next_payment.amount.currency,
currencySymbol: data.next_payment.amount.currency_symbol,
},
}
: null;

return new CommerceSubscription(
data.id,
data.status,
data.payer_id,
data.created_at,
data.updated_at,
data.active_at ?? null,
data.past_due_at ?? null,
data.subscription_items.map(item => CommerceSubscriptionItem.fromJSON(item)),
nextPayment,
data.eligible_for_free_trial ?? false,
);
}
}
34 changes: 28 additions & 6 deletions packages/backend/src/api/resources/CommerceSubscriptionItem.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,28 +44,45 @@ export class CommerceSubscriptionItem {
* The plan ID.
*/
readonly planId: string,
/**
* The date and time the subscription item was created.
*/
readonly createdAt: number,
/**
* The date and time the subscription item was last updated.
*/
readonly updatedAt: number,
/**
* The end of the current period.
*/
readonly periodEnd?: number,
readonly periodEnd: number | null,
/**
* When the subscription item was canceled.
*/
readonly canceledAt?: number,
readonly canceledAt: number | null,
/**
* When the subscription item became past due.
*/
readonly pastDueAt?: number,
readonly pastDueAt: number | null,
/**
* When the subscription item ended.
*/
readonly endedAt: number | null,
/**
* The payer ID.
*/
readonly payerId: string,
/**
* Whether this subscription item is currently in a free trial period.
*/
readonly isFreeTrial?: boolean,
/**
* The lifetime amount paid for this subscription item.
*/
readonly lifetimePaid?: CommerceMoneyAmount | null,
) {}

static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem {
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined;
function formatAmountJSON(
amount: CommerceMoneyAmountJSON | null | undefined,
): CommerceMoneyAmount | null | undefined {
Expand All@@ -90,9 +107,14 @@ export class CommerceSubscriptionItem {
formatAmountJSON(data.amount),
CommercePlan.fromJSON(data.plan),
data.plan_id,
data.created_at,
data.updated_at,
data.period_end,
data.canceled_at,
data.past_due_at,
data.ended_at,
data.payer_id,
data.is_free_trial,
formatAmountJSON(data.lifetime_paid),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import {
} from '.';
import { AccountlessApplication } from './AccountlessApplication';
import { CommercePlan } from './CommercePlan';
import { CommerceSubscription } from './CommerceSubscription';
import { CommerceSubscriptionItem } from './CommerceSubscriptionItem';
import { Feature } from './Feature';
import type { PaginatedResponseJSON } from './JSON';
Expand DownExpand Up@@ -184,6 +185,8 @@ function jsonToObject(item: any): any {
return WaitlistEntry.fromJSON(item);
case ObjectType.CommercePlan:
return CommercePlan.fromJSON(item);
case ObjectType.CommerceSubscription:
return CommerceSubscription.fromJSON(item);
case ObjectType.CommerceSubscriptionItem:
return CommerceSubscriptionItem.fromJSON(item);
case ObjectType.Feature:
Expand Down
27 changes: 24 additions & 3 deletions packages/backend/src/api/resources/JSON.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -861,10 +861,15 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscriptionItem;
status: CommerceSubscriptionItemStatus;
plan_period: 'month' | 'annual';
payer_id: string;
period_start: number;
period_end?: number;
canceled_at?: number;
past_due_at?: number;
period_end: number | null;
is_free_trial?: boolean;
ended_at: number | null;
created_at: number;
updated_at: number;
canceled_at: number | null;
past_due_at: number | null;
lifetime_paid: CommerceMoneyAmountJSON;
next_payment: {
amount: number;
Expand DownExpand Up@@ -972,6 +977,22 @@ export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON
items: CommerceSubscriptionItemWebhookEventJSON[];
}

export interface CommerceSubscriptionJSON extends ClerkResourceJSON {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add comprehensive JSDoc for the new public API

Per repo guidelines, all public APIs need JSDoc. Add the experimental notice and field-level docs for payer_id, subscription_items, and next_payment.

Apply this diff:

- export interface CommerceSubscriptionJSON extends ClerkResourceJSON {+ /**+ * @experimental This is an experimental API for the Billing feature that is available under a public beta,+ * and the API is subject to change. Pin the SDK version to avoid breaking changes.+ *+ * Data model returned by `BillingApi.getOrganizationBillingSubscription`.+ * - `subscription_items`: list of items in this subscription.+ * - `next_payment`: next scheduled charge for the subscription, or null if none.+ */+ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
/**
* @experimentalThis is anexperimentalAPIfortheBillingfeaturethat is availableunderapublicbeta,
*andtheAPIissubjecttochange.PintheSDKversiontoavoidbreakingchanges.
*
*Datamodelreturnedby`BillingApi.getOrganizationBillingSubscription`.
*-`subscription_items`: listofitemsinthissubscription.
*-`next_payment`: nextscheduledchargeforthesubscription,ornullifnone.
*/
exportinterfaceCommerceSubscriptionJSONextendsClerkResourceJSON{
🤖 Prompt for AI Agents
In packages/backend/src/api/resources/JSON.ts around line 980, add a
comprehensive JSDoc comment immediately above the exported interface
CommerceSubscriptionJSON: mark the API as experimental, document the interface
purpose, and add field-level @property tags describing payer_id (the unique
identifier for the payer, format and nullable/optional status),
subscription_items (array shape, item type and meaning), and next_payment (next
payment date/time or object shape and nullable/optional status). Ensure the
JSDoc follows repo conventions, uses @experimental or an explicit experimental
notice, and documents types/optionality for each field so the public API is
fully documented.

object: typeof ObjectType.CommerceSubscription;
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';
payer_id: string;
created_at: number;
updated_at: number;
active_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[];
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}
Comment on lines +980 to +994

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public contract mismatch with packages/types: high risk of consumer breakage

This CommerceSubscriptionJSON differs from packages/types/src/json.ts in multiple ways:

  • status union includes terminal/internal states; SDK restricts to 'active' | 'past_due'
  • payer_id exists here; SDK has no top-level payer_id
  • active_at is nullable here; SDK requires it
  • updated_at is non-null here; SDK allows null
  • subscription_items is non-null array here; SDK allows null
  • next_payment optional shape matches, fine

Two safe paths:

Option A — Align with SDK shape (preferred for a public surface):

 export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
object: typeof ObjectType.CommerceSubscription;
- status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete';- payer_id: string;+ status: 'active' | 'past_due';
created_at: number;
- updated_at: number;- active_at: number | null;+ active_at: number;+ updated_at: number | null;
past_due_at: number | null;
- subscription_items: CommerceSubscriptionItemJSON[];+ subscription_items: CommerceSubscriptionItemJSON[] | null;
next_payment?: {
date: number;
amount: CommerceMoneyAmountJSON;
};
eligible_for_free_trial?: boolean;
}

Additionally, update CommerceSubscription.fromJSON to handle possibly-null subscription_items and nullable updated_at; and drop payerId unless it exists at another level.

Option B — Keep extended backend shape but scope it:

  • Rename to InternalCommerceSubscriptionJSON (or add @internal) and avoid exporting it via public index files.
  • Add a mapper that produces the SDK-aligned shape for anything exposed publicly.
  • Ensure BillingApi.getOrganizationBillingSubscription returns the class CommerceSubscription (stable surface), not this extended JSON.

Given the scope of impact, please choose one path before merging. I can provide a follow-up patch for either approach, including the mapper changes in CommerceSubscription.ts.


🏁 Script executed:

#!/bin/bashset -e
# Locate json.ts in packages/types/src
json_file=$(fd -t f json.ts packages/types/src | head -n1)echo"Using file: $json_file"# Show the CommerceSubscriptionJSON interface and its key fields
rg -n 'interface CommerceSubscriptionJSON' -C10 "$json_file"
rg -n 'status' -C2 "$json_file"
rg -n 'payer_id' -C2 "$json_file"
rg -n 'active_at' -C2 "$json_file"
rg -n 'updated_at' -C2 "$json_file"
rg -n 'subscription_items' -C2 "$json_file"

Length of output: 4023


🏁 Script executed:

#!/bin/bashset -e
types_file="packages/types/src/json.ts"echo"SDK JSON File: $types_file"# Extract the CommerceSubscriptionJSON block
start=$(rg -n "export interface CommerceSubscriptionJSON" -n "$types_file"| cut -d: -f1)# Capture through the matching closing brace (assuming no nested braces inside)
relative_end=$(tail -n +$start"$types_file"| sed -n '1,/^}/p'| tail -n1 | grep -n '^}'| cut -d: -f1)
end=$((start + relative_end -1))echo"--- CommerceSubscriptionJSON Definition ---"
sed -n "${start},${end}p""$types_file"echoecho"--- Key Property Definitions ---"echo"status:"
rg -n "status:" -C2 "$types_file"| sed -n '1,5p'echoecho"updated_at:"
rg -n "updated_at:" -C2 "$types_file"| sed -n '1,5p'echoecho"subscription_items:"
rg -n "subscription_items:" -C2 "$types_file"| sed -n '1,5p'

Length of output: 1458


🏁 Script executed:

#!/bin/bashset -e
# Print lines 970-1040 of the backend JSON file to inspect CommerceSubscriptionJSON
sed -n '970,1040p' packages/backend/src/api/resources/JSON.ts

Length of output: 857


Public JSON contract mismatch in CommerceSubscriptionJSON (packages/backend/src/api/resources/JSON.ts)

The CommerceSubscriptionJSON defined in the backend package diverges from the SDK’s CommerceSubscriptionJSON in packages/types/src/json.ts. Left unaddressed, these differences will break downstream consumers:

status
– Backend:
'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete'
– SDK:
Extract<CommerceSubscriptionStatus, 'active' | 'past_due'> (only 'active' or 'past_due')
payer_id
– Backend: payer_id: string present at top level
– SDK: no payer_id field
active_at
– Backend: active_at: number | null
– SDK: active_at: number (non-nullable)
updated_at
– Backend: updated_at: number (non-nullable)
– SDK: updated_at: number | null
subscription_items
– Backend: subscription_items: CommerceSubscriptionItemJSON[] (non-nullable)
– SDK: subscription_items: CommerceSubscriptionItemJSON[] | null

Two paths forward:

  1. Align backend JSON with SDK public shape (preferred for stability)
    – Restrict status to 'active' | 'past_due'
    – Remove top-level payer_id
    – Make active_at: number (non-nullable) and updated_at: number | null
    – Allow subscription_items: CommerceSubscriptionItemJSON[] | null
    – Update CommerceSubscription.fromJSON to handle nullable fields and drop payerId

  2. Scope extended backend shape as internal
    – Rename to InternalCommerceSubscriptionJSON (or mark @internal) and don’t re-export it publicly
    – Add a mapper in CommerceSubscription.ts that converts the internal JSON into the SDK’s public shape
    – Ensure public methods (e.g. BillingApi.getOrganizationBillingSubscription) return the public CommerceSubscription

Given the high risk of breaking changes, please choose one approach before merging. I can follow up with a patch for either path.


export interface WebhooksSvixJSON {
svix_url: string;
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@ export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';
export * from './CommerceSubscription';
export * from './CommerceSubscriptionItem';

export type {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePlanJSON,
CommerceSubscriptionJSON,
CommerceSubscriptionItemJSON,
} from './api/resources/JSON';

Expand DownExpand Up@@ -144,6 +145,7 @@ export type {
User,
TestingToken,
CommercePlan,
CommerceSubscription,
CommerceSubscriptionItem,
} from './api/resources';

Expand Down
Loading