From 503fccaff2cabb17bce705282ae2992c7ae8d3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 16 Jul 2025 16:33:54 -0300 Subject: [PATCH 1/5] chore(backend): Add webhook event types for Commerce related events We recently introduced webhook events for Commerce: payment attempts and subscription events. This commit adds type safety so developer can create type-safe webhook handlers. --- packages/backend/src/api/resources/JSON.ts | 135 ++++++++++++++++++ .../backend/src/api/resources/Webhooks.ts | 31 +++- packages/backend/src/index.ts | 11 ++ 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 369a215d0e9..bb7f41cac2b 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -63,6 +63,10 @@ export const ObjectType = { TestingToken: 'testing_token', Role: 'role', Permission: 'permission', + CommercePayer: 'commerce_payer', + CommercePaymentAttempt: 'commerce_payment_attempt', + CommerceSubscription: 'commerce_subscription', + CommerceSubscriptionItem: 'commerce_subscription_item', } as const; export type ObjectType = (typeof ObjectType)[keyof typeof ObjectType]; @@ -757,6 +761,137 @@ export interface IdPOAuthAccessTokenJSON extends ClerkResourceJSON { updated_at: number; } +export interface CommercePayerJSON extends ClerkResourceJSON { + object: typeof ObjectType.CommercePayer; + instance_id: string; + user_id?: string; + first_name?: string; + last_name?: string; + email: string; + organization_id?: string; + organization_name?: string; + image_url: string; + created_at: number; + updated_at: number; +} + +export interface CommercePayeeJSON { + id: string; + gateway_type: string; + gateway_external_id: string; + gateway_status: string; +} + +export interface CommerceAmountJSON { + amount: number; + amount_formatted: string; + currency: string; + currency_symbol: string; +} + +export interface CommerceTotalsJSON { + subtotal: CommerceAmountJSON; + tax_total: CommerceAmountJSON; + grand_total: CommerceAmountJSON; +} + +export interface CommercePaymentSourceJSON { + id: string; + gateway: string; + gateway_external_id: string; + gateway_external_account_id?: string; + payment_method: string; + status: string; + card_type?: string; + last4?: string; +} + +export interface CommercePaymentFailedReasonJSON { + code: string; + decline_code: string; +} + +export interface CommerceSubscriptionCreditJSON { + amount: CommerceAmountJSON; + cycle_days_remaining: number; + cycle_days_total: number; + cycle_remaining_percent: number; +} + +export interface CommercePlanJSON { + id: string; + instance_id: string; + product_id: string; + name: string; + slug: string; + description?: string; + is_default: boolean; + is_recurring: boolean; + is_prorated: boolean; + amount: number; + period: string; + interval: number; + has_base_fee: boolean; + currency: string; + annual_monthly_amount: number; + publicly_visible: boolean; +} + +export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { + object: typeof ObjectType.CommerceSubscriptionItem; + status: string; + credit: CommerceSubscriptionCreditJSON; + proration_date: string; + plan_period: string; + period_start?: number; + period_end?: number; + canceled_at?: number; + past_due_at?: number; + lifetime_paid: number; + next_payment_amount: number; + next_payment_date: number; + amount: CommerceAmountJSON; + plan: CommercePlanJSON; + plan_id: string; +} + +export interface CommercePaymentAttemptJSON extends ClerkResourceJSON { + object: typeof ObjectType.CommercePaymentAttempt; + instance_id: string; + payment_id: string; + statement_id: string; + gateway_external_id: string; + status: string; + created_at: number; + updated_at: number; + paid_at?: number; + failed_at?: number; + failed_reason?: CommercePaymentFailedReasonJSON; + billing_date: number; + charge_type: string; + payee: CommercePayeeJSON; + payer: CommercePayerJSON; + totals: CommerceTotalsJSON; + payment_source: CommercePaymentSourceJSON; + subscription_items: CommerceSubscriptionItemJSON[]; +} + +export interface CommerceSubscriptionJSON extends ClerkResourceJSON { + object: typeof ObjectType.CommerceSubscription; + status: string; + active_at?: number; + canceled_at?: number; + created_at: number; + ended_at?: number; + past_due_at?: number; + updated_at: number; + latest_payment_id: string; + payer_id: string; + payer: CommercePayerJSON; + payment_source_id: string; + items: CommerceSubscriptionItemJSON[]; +} + export interface WebhooksSvixJSON { svix_url: string; } diff --git a/packages/backend/src/api/resources/Webhooks.ts b/packages/backend/src/api/resources/Webhooks.ts index aba2cbe33f5..18d7c333c59 100644 --- a/packages/backend/src/api/resources/Webhooks.ts +++ b/packages/backend/src/api/resources/Webhooks.ts @@ -1,4 +1,7 @@ import type { + CommercePaymentAttemptJSON, + CommerceSubscriptionItemJSON, + CommerceSubscriptionJSON, DeletedObjectJSON, EmailJSON, OrganizationDomainJSON, @@ -62,6 +65,29 @@ export type PermissionWebhookEvent = Webhook< export type WaitlistEntryWebhookEvent = Webhook<'waitlistEntry.created' | 'waitlistEntry.updated', WaitlistEntryJSON>; +export type CommercePaymentAttemptWebhookEvent = Webhook< + 'paymentAttempt.created' | 'paymentAttempt.updated', + CommercePaymentAttemptJSON +>; + +export type CommerceSubscriptionWebhookEvent = Webhook< + 'subscription.created' | 'subscription.updated' | 'subscription.active' | 'subscription.past_due', + CommerceSubscriptionJSON +>; + +export type CommerceSubscriptionItemWebhookEvent = Webhook< + | 'subscriptionItem.created' + | 'subscriptionItem.updated' + | 'subscriptionItem.active' + | 'subscriptionItem.canceled' + | 'subscriptionItem.upcoming' + | 'subscriptionItem.ended' + | 'subscriptionItem.abandoned' + | 'subscriptionItem.incomplete' + | 'subscriptionItem.past_due', + CommerceSubscriptionItemJSON +>; + export type WebhookEvent = | UserWebhookEvent | SessionWebhookEvent @@ -73,6 +99,9 @@ export type WebhookEvent = | OrganizationInvitationWebhookEvent | RoleWebhookEvent | PermissionWebhookEvent - | WaitlistEntryWebhookEvent; + | WaitlistEntryWebhookEvent + | CommercePaymentAttemptWebhookEvent + | CommerceSubscriptionWebhookEvent + | CommerceSubscriptionItemWebhookEvent; export type WebhookEventType = WebhookEvent['type']; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index fe902146b11..1df2f628240 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -100,6 +100,17 @@ export type { PaginatedResponseJSON, TestingTokenJSON, WebhooksSvixJSON, + CommercePayerJSON, + CommercePayeeJSON, + CommerceAmountJSON, + CommerceTotalsJSON, + CommercePaymentSourceJSON, + CommercePaymentFailedReasonJSON, + CommerceSubscriptionCreditJSON, + CommercePlanJSON, + CommerceSubscriptionItemJSON, + CommercePaymentAttemptJSON, + CommerceSubscriptionJSON, } from './api/resources/JSON'; /** From 50a1a290bbaeb51422a18db0612d0084bfa7d10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Mon, 21 Jul 2025 10:50:19 -0300 Subject: [PATCH 2/5] fix: statuses field can be stricter Use union types for every status field. --- packages/backend/src/api/resources/JSON.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index bb7f41cac2b..1b7aaf116ce 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -779,7 +779,7 @@ export interface CommercePayeeJSON { id: string; gateway_type: string; gateway_external_id: string; - gateway_status: string; + gateway_status: 'active' | 'pending' | 'restricted' | 'disconnected'; } export interface CommerceAmountJSON { @@ -801,7 +801,7 @@ export interface CommercePaymentSourceJSON { gateway_external_id: string; gateway_external_account_id?: string; payment_method: string; - status: string; + status: 'active' | 'disconnected'; card_type?: string; last4?: string; } @@ -829,7 +829,7 @@ export interface CommercePlanJSON { is_recurring: boolean; is_prorated: boolean; amount: number; - period: string; + period: 'month' | 'annual'; interval: number; has_base_fee: boolean; currency: string; @@ -839,10 +839,10 @@ export interface CommercePlanJSON { export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { object: typeof ObjectType.CommerceSubscriptionItem; - status: string; + status: 'abandoned' | 'active' | 'canceled' | 'ended' | 'expired' | 'incomplete' | 'past_due' | 'upcoming'; credit: CommerceSubscriptionCreditJSON; proration_date: string; - plan_period: string; + plan_period: 'month' | 'annual'; period_start?: number; period_end?: number; canceled_at?: number; @@ -861,14 +861,14 @@ export interface CommercePaymentAttemptJSON extends ClerkResourceJSON { payment_id: string; statement_id: string; gateway_external_id: string; - status: string; + status: 'pending' | 'paid' | 'failed'; created_at: number; updated_at: number; paid_at?: number; failed_at?: number; failed_reason?: CommercePaymentFailedReasonJSON; billing_date: number; - charge_type: string; + charge_type: 'checkout' | 'recurring'; payee: CommercePayeeJSON; payer: CommercePayerJSON; totals: CommerceTotalsJSON; @@ -878,7 +878,7 @@ export interface CommercePaymentAttemptJSON extends ClerkResourceJSON { export interface CommerceSubscriptionJSON extends ClerkResourceJSON { object: typeof ObjectType.CommerceSubscription; - status: string; + status: 'abandoned' | 'active' | 'canceled' | 'ended' | 'expired' | 'incomplete' | 'past_due' | 'upcoming'; active_at?: number; canceled_at?: number; created_at: number; From f3d9b2ad58e8e7cf7626f7dfc9481ebba66fe6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Mon, 21 Jul 2025 17:18:38 -0300 Subject: [PATCH 3/5] chore: changeset --- .changeset/lucky-papers-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-papers-act.md diff --git a/.changeset/lucky-papers-act.md b/.changeset/lucky-papers-act.md new file mode 100644 index 00000000000..f98e5bfa7d4 --- /dev/null +++ b/.changeset/lucky-papers-act.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Add types for Commerce webhooks From f0b00a4728dd7eccb4be2cd2860c80fc054f08a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Mon, 21 Jul 2025 21:27:54 -0300 Subject: [PATCH 4/5] fix: period_start is always defined --- packages/backend/src/api/resources/JSON.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 1b7aaf116ce..ece16d8dd6b 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -843,7 +843,7 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { credit: CommerceSubscriptionCreditJSON; proration_date: string; plan_period: 'month' | 'annual'; - period_start?: number; + period_start: number; period_end?: number; canceled_at?: number; past_due_at?: number; From c5b082b62cf6c056d3e0f0d47a370af850971ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Mon, 21 Jul 2025 22:06:00 -0300 Subject: [PATCH 5/5] chore: remove is_prorated No longer being used. --- packages/backend/src/api/resources/JSON.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index ece16d8dd6b..e48b8361fe9 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -827,7 +827,6 @@ export interface CommercePlanJSON { description?: string; is_default: boolean; is_recurring: boolean; - is_prorated: boolean; amount: number; period: 'month' | 'annual'; interval: number;