Skip to content

PR: Add workflow to include billing info in user tokens - B2B case#23

Open
ahn-nath wants to merge 4 commits into
kinde-starter-kits:mainfrom
ahn-nath:add-billing-details-to-tokens-b2b
Open

PR: Add workflow to include billing info in user tokens - B2B case#23
ahn-nath wants to merge 4 commits into
kinde-starter-kits:mainfrom
ahn-nath:add-billing-details-to-tokens-b2b

Conversation

@ahn-nath
Copy link
Copy Markdown
Contributor

@ahn-nath ahn-nath commented Dec 12, 2025

Explain your changes

Summary
Adds a B2B-specific workflow that fetches the active Organization’s billing information during token generation and adds it to the user tokens as an org_billing custom claim.

The workflow validates that an organization context exists, fetches the organization's billing profile via the Kinde Management API, retrieves entitlements and agreements in parallel, and gracefully handles errors or missing contexts.

Files changed

  • addBillingDetailsToTokensB2B.ts (new workflow implementation)

What this implements

  • Runs on the user:tokens_generation event and sets the org_billing claim on both access and ID tokens.
  • Validates the execution context:
    • Checks if event.context.organization.code exists.
    • Skips execution/logging if the user is not signing into an organization context.
  • Calls the appropriate Management APIs:
    • GET /organization?code={code}&expand=billing (to get the Org's customer_id)
    • GET /billing/entitlements?customer_id=...
    • GET /billing/agreements?customer_id=...
  • Includes the full organization billing payload in the claim.
  • Adds TypeScript types for OrganizationResponse, OrgBillingClaim, Entitlement, and Agreement.
  • Implements parallel data fetching (Promise.all) for entitlements and agreements to minimize latency (<2s execution target).

Notes & follow-ups

  • Token size: Similar to the B2C workflow, this implementation injects the raw billing payload. For organizations with massive entitlement lists, we may need to implement summary/filtering logic in a future PR to avoid hitting the 4KB header limit.
  • Separation of Concerns: This workflow is distinct from the B2C workflow. It specifically isolates Organization data, ensuring users don't inadvertently get personal billing claims when logging into a business context (and vice versa).

Checklist

🛟 If you need help, consider asking for advice over in the [Kinde community](https://thekindecommunity.slack.com).

Summary by CodeRabbit

  • New Features

    • Adds an org_billing custom claim to access and ID tokens when a user signs into an Organization.
    • Includes the Organization's customer ID, billing profile, entitlements, and agreements in the token payload.
    • Logic includes a check for organization context, skipping the billing fetch if the user is not signing into an organization.
  • Performance

    • Implements parallel fetching of agreements and entitlements to ensure minimal impact on authentication latency.

Summary by CodeRabbit

  • New Features
    • Adds a B2B token enrichment workflow that injects organization billing details (customer ID, entitlements, agreements) into identity and access tokens.
    • Handles missing data gracefully and performs efficient backend retrievals to minimize latency.
    • Workflow is available for use in token issuance to support downstream billing and access enforcement.

✏️ Tip: You can customize this high-level summary in your review settings.

A new file was added with a workflow that can be used to extend the access and ID tokens with billing information for Business to Business models.
@ahn-nath ahn-nath requested a review from a team as a code owner December 12, 2025 20:00
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 12, 2025

📝 Walkthrough

Walkthrough

Adds a B2B token enrichment workflow that validates an organization code, fetches billing_customer_id, retrieves entitlements and agreements (parallel/conditional), builds an org_billing claim, and injects it into access and ID tokens. (New file: userTokens/addBillingDetailsToTokensB2BWorkflow.ts)

Changes

Cohort / File(s) Summary
B2B Token Enrichment Workflow
userTokens/addBillingDetailsToTokensB2BWorkflow.ts
New TypeScript workflow export workflowSettings and default OrganizationBillingWorkflow(event). Validates org code, creates Kinde API client, fetches organization (billing_customer_id), fetches entitlements (and agreements if needed) in parallel, composes org_billing claim, injects into accessToken and idToken, logs and rethrows errors.

Sequence Diagram

sequenceDiagram
    participant Event as Event/Caller
    participant Workflow as OrganizationBillingWorkflow
    participant KindeAPI as Kinde API
    participant Token as Token Claims

    Event->>Workflow: Trigger with WorkflowEvent
    Note over Workflow: Validate org code (early exit if missing)

    Workflow->>KindeAPI: Fetch organization (expand billing)
    KindeAPI-->>Workflow: Organization + billing_customer_id

    Note over Workflow: Exit if no customer_id

    rect rgba(200,220,240,0.5)
        par Fetch entitlements
            Workflow->>KindeAPI: Fetch entitlements (expand plans)
            KindeAPI-->>Workflow: Entitlements[]
        and Fetch agreements (conditional)
            Workflow->>KindeAPI: Fetch agreements (if not present)
            KindeAPI-->>Workflow: Agreements[]
        end
    end

    Note over Workflow: Assemble org_billing claim (customer_id, entitlements, agreements)

    rect rgba(220,240,220,0.5)
        Workflow->>Token: Inject org_billing into accessToken
        Workflow->>Token: Inject org_billing into idToken
    end

    Token-->>Event: Tokens with org_billing claim
    Note over Workflow: Log success or error (rethrow on failure)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • onderay
  • Krish0369

Poem

🐇 I nibble through endpoints with curious paws,
I chase the customer and tally its laws,
Entitlements hop, agreements align,
I stitch an org_billing claim tidy and fine,
Tokens bound — billing hops into place with applause!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a B2B workflow that includes billing information in user tokens.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ahn-nath ahn-nath changed the title Create addBillingDetailsToTokensB2BWorkflow.ts PR: Add workflow to include billing info in user tokens - B2B case Dec 12, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
userTokens/addBillingDetailsToTokensB2BWorkflow.ts (2)

172-176: Error logging: consider logging the stack (when available).
Current log prints .message (Line 174) but drops stack, which is typically what you’ll need to debug workflow failures.

-        console.error("B2B Workflow Error:", (err as Error).message ?? err);
+        const e = err as Error;
+        console.error("B2B Workflow Error:", e?.stack ?? e?.message ?? err);
         throw err;

1-20: Bindings pattern is standard Kinde workflow practice; consider clarifying with a comment.

The bindings declared as "kinde.accessToken" and "kinde.idToken" in workflowSettings.bindings are made available by the Kinde workflow runtime as typed global identifiers (kinde.accessToken, kinde.idToken, etc.). This is the standard pattern across all workflow files in this repository. However, the mapping from string binding keys to runtime identifiers is implicit. Consider adding a clarifying comment (e.g., // kinde bindings injected by workflow runtime) at the top of the implementation block where kinde.accessToken.setCustomClaim() is called to improve code clarity for future maintainers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e12a855 and dbd5e06.

📒 Files selected for processing (1)
  • userTokens/addBillingDetailsToTokensB2BWorkflow.ts (1 hunks)

Comment thread userTokens/addBillingDetailsToTokensB2BWorkflow.ts
Comment thread userTokens/addBillingDetailsToTokensB2BWorkflow.ts
Comment thread userTokens/addBillingDetailsToTokensB2BWorkflow.ts
Comment on lines +165 to +169
// [4] Set Custom Claims
// We inject this data into the tokens so the frontend/API can enforce limits immediately.
kinde.accessToken.setCustomClaim("org_billing", billingClaimObject);
kinde.idToken.setCustomClaim("org_billing", billingClaimObject);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure of the repository to locate the file
find . -name "addBillingDetailsToTokensB2BWorkflow.ts" -type f

Repository: kinde-starter-kits/workflow-examples

Length of output: 132


🏁 Script executed:

# Read the file to check imports and the context around lines 165-169
cat -n ./userTokens/addBillingDetailsToTokensB2BWorkflow.ts | head -180

Repository: kinde-starter-kits/workflow-examples

Length of output: 7011


🏁 Script executed:

# Search for other usages of setCustomClaim or token binding patterns in the repo
find . -type f -name "*.ts" | xargs grep -l "setCustomClaim\|kinde\.accessToken\|kinde\.idToken" 2>/dev/null | head -5

Repository: kinde-starter-kits/workflow-examples

Length of output: 225


🏁 Script executed:

# Check one of the other files to see how tokens are properly used
cat -n ./userTokens/customClaimsAccessTokenWorkflow.ts | head -120

Repository: kinde-starter-kits/workflow-examples

Length of output: 1973


🏁 Script executed:

# Check the B2C file to see if it has similar pattern issues
cat -n ./userTokens/addBillingDetailsToTokensB2C.ts | head -150

Repository: kinde-starter-kits/workflow-examples

Length of output: 5515


🏁 Script executed:

# Read the rest of the B2C file to see how tokens are used
cat -n ./userTokens/addBillingDetailsToTokensB2C.ts | tail -50

Repository: kinde-starter-kits/workflow-examples

Length of output: 2140


🏁 Script executed:

# Search for any documentation or README files that might explain the workflow pattern
find . -type f \( -name "README*" -o -name "*.md" \) | xargs grep -l "setCustomClaim\|accessToken\|idToken" 2>/dev/null | head -3

Repository: kinde-starter-kits/workflow-examples

Length of output: 62


🏁 Script executed:

# Search for any usage of idTokenCustomClaims or token claim patterns
grep -r "idTokenCustomClaims\|accessTokenCustomClaims" . --include="*.ts" --include="*.js"

Repository: kinde-starter-kits/workflow-examples

Length of output: 250


🏁 Script executed:

# Check package.json for @kinde/infrastructure version and details
cat package.json | grep -A 5 -B 5 "@kinde/infrastructure"

Repository: kinde-starter-kits/workflow-examples

Length of output: 223


🌐 Web query:

@kinde/infrastructure 0.4.1 API documentation token claims accessToken idToken

💡 Result:

Summary for @kinde/infrastructure (v0.4.1) — token claims (accessToken / idToken)

  • Library exposes type-safe helpers to add custom claims inside workflows:

    • accessTokenCustomClaims() — returns an object you assign custom access-token claims to. [1][2]
    • idTokenCustomClaims() — returns an object you assign custom ID-token claims to. [1][2]
  • Low-level workflow bindings (alternative to the helpers):

    • kinde.accessToken.setCustomClaim(name, value) — add claim to access token. [2]
    • kinde.idToken.setCustomClaim(name, value) — add claim to ID token. [3]
  • Client SDK helpers to read claims:

    • getClaim(claim: string, tokenKey?: "accessToken" | "idToken") — reads a claim from access or ID token (defaults to accessToken). [4]
  • Notes:

    • Some standard/prohibited claims cannot be modified from a workflow (see Kinde docs / prohibited claims). [1][2]
    • Avoid adding sensitive data to tokens (tokens are client-visible). [1][5]

Sources:
[1] Kinde docs — access-token binding / usage with @kinde/infrastructure.
[2] Kinde docs — access-token binding page.
[3] Kinde docs — id-token binding page.
[4] Kinde React SDK docs — getClaim API.
[5] npm: @kinde/infrastructure package README (examples showing accessToken/idToken helpers).


Replace direct token binding access with @kinde/infrastructure helper functions.

Lines 167-168 use kinde.accessToken.setCustomClaim() and kinde.idToken.setCustomClaim(), but this pattern is not how workflows properly wire token objects in @kinde/infrastructure v0.4.1. Use the typed helper functions instead:

import { accessTokenCustomClaims, idTokenCustomClaims } from "@kinde/infrastructure";

// Later in your workflow:
const accessToken = accessTokenCustomClaims<OrgBillingClaim>();
Object.assign(accessToken, billingClaimObject);

const idToken = idTokenCustomClaims<OrgBillingClaim>();
Object.assign(idToken, billingClaimObject);

Additionally, embedding full entitlements/agreements arrays into both tokens will bloat JWTs. Consider injecting only IDs/codes and version hashes, or place the full payload in only the access token.

🤖 Prompt for AI Agents
In userTokens/addBillingDetailsToTokensB2BWorkflow.ts around lines 165 to 169,
replace the direct token binding calls kinde.accessToken.setCustomClaim(...) and
kinde.idToken.setCustomClaim(...) with the typed helpers from
@kinde/infrastructure: import accessTokenCustomClaims and idTokenCustomClaims,
create typed claim objects via those helpers and merge billingClaimObject into
them (e.g., Object.assign), and avoid embedding full entitlements/agreements
arrays into both tokens — instead include only IDs/codes or version hashes, or
keep the full payload only on the access token to prevent JWT bloat.

@Arobce
Copy link
Copy Markdown
Contributor

Arobce commented Dec 16, 2025

Hi @ahn-nath,

One thing I wanted to flag for awareness around typing usage:

I noticed generic type parameters being used in several places such as kindeAPI.get<OrganizationResponse>, kindeAPI.get<EntitlementsResponse>, etc. From what I can tell, these Kinde API helpers do not formally expose generics in their method signatures. While this does not affect runtime behaviour and does improve readability and intent, it is not something the methods officially support.

It might be worth either removing the generics and typing the response after the call, or adding a brief comment to clarify that the generics are for developer clarity rather than enforced typing.

Nothing blocking here, and amazing job on this!

@ahn-nath
Copy link
Copy Markdown
Contributor Author

@Arobce
Thank you for this feedback, and I am now aware of this, thanks to you. I will handle a fix/update tomorrow.

@ahn-nath
Copy link
Copy Markdown
Contributor Author

@Arobce

Hello, Roshan. Again, thank you very much for your feedback. I have decided to remove the typing because this aligns better with TypeScript principles. I sent my work for review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@userTokens/addBillingDetailsToTokensB2BWorkflow.ts`:
- Around line 155-157: The expression assigning agreements contains a stray '+'
character before agrResp which causes a syntax error; remove the leading '+' so
the ternary uses agrResp?.data?.agreements and call
ensureArray<Agreement>(agrResp.data.agreements) in the true branch, otherwise
fall back to initialAgreements (refer to the agreements variable, agrResp,
initialAgreements, and ensureArray<Agreement> to locate the line in
addBillingDetailsToTokensB2BWorkflow.ts).
- Around line 35-44: The OrganizationResponse type is out of sync with the real
API: update the billing shape in the OrganizationResponse interface (symbol:
OrganizationResponse) to include billing_customer_id?: string (instead of or in
addition to customer_id?) so TypeScript matches the actual Kinde API field; then
ensure code paths that read billing_customer_id (e.g., the access in the
workflow that currently reads billing_customer_id) are typed correctly by this
updated interface.
🧹 Nitpick comments (1)
userTokens/addBillingDetailsToTokensB2BWorkflow.ts (1)

143-153: Inconsistent typing approach - generic still present on line 150.

The PR objective states generics were removed and responses typed explicitly. Line 146 correctly uses a type assertion (as Promise<{ data: EntitlementsResponse }>), but line 150 still uses a generic (kindeAPI.get<AgreementsResponse>). Apply the same pattern for consistency.

♻️ Proposed fix for consistent typing
         const [entResp, agrResp] = await Promise.all([
             kindeAPI.get({
                 endpoint: `billing/entitlements?customer_id=${customerId}&expand=plans`
             }) as Promise<{ data: EntitlementsResponse }>,
             
             // Only call agreements if we could not get it from the previous call
             initialAgreements.length === 0
-            ? kindeAPI.get<AgreementsResponse>({
+            ? kindeAPI.get({
                 endpoint: `billing/agreements?customer_id=${customerId}`
-            }): Promise.resolve(null)  
+            }) as Promise<{ data: AgreementsResponse }> : Promise.resolve(null)
         ]);

Comment thread userTokens/addBillingDetailsToTokensB2BWorkflow.ts
Comment on lines +155 to +157
const entitlements = ensureArray<Entitlement>(entResp?.data?.entitlements);
const agreements =
+ agrResp?.data?.agreements ? ensureArray<Agreement>(agrResp.data.agreements) : initialAgreements;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Stray + character will cause a syntax error.

Line 157 has a + character at the beginning of the expression which is syntactically invalid and will prevent the code from compiling.

🐛 Proposed fix
         const entitlements = ensureArray<Entitlement>(entResp?.data?.entitlements);
         const agreements =
-+            agrResp?.data?.agreements ? ensureArray<Agreement>(agrResp.data.agreements) : initialAgreements;
+            agrResp?.data?.agreements ? ensureArray<Agreement>(agrResp.data.agreements) : initialAgreements;
🤖 Prompt for AI Agents
In `@userTokens/addBillingDetailsToTokensB2BWorkflow.ts` around lines 155 - 157,
The expression assigning agreements contains a stray '+' character before
agrResp which causes a syntax error; remove the leading '+' so the ternary uses
agrResp?.data?.agreements and call
ensureArray<Agreement>(agrResp.data.agreements) in the true branch, otherwise
fall back to initialAgreements (refer to the agreements variable, agrResp,
initialAgreements, and ensureArray<Agreement> to locate the line in
addBillingDetailsToTokensB2BWorkflow.ts).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants