-
Notifications
You must be signed in to change notification settings - Fork 110
docs(agents): add "Pricing" page to agents docs #1134
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,9 @@ | |
| "...custom-code-agent", | ||
|
|
||
| "---Observability---", | ||
| "conversations" | ||
| "conversations", | ||
|
|
||
| "---Pricing---", | ||
| "pricing" | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| --- | ||
| title: "Pricing" | ||
| pageTitle: 'Pricing' | ||
| description: 'Plans, limits, and billing for Novu Connect (Agent Communication Infrastructure).' | ||
| icon: CreditCard | ||
| --- | ||
|
|
||
| import { ConnectPricingCalculator } from '@/components/agents/pricing-calculator'; | ||
|
Check notice on line 8 in content/docs/agents/pricing.mdx
|
||
|
|
||
| Novu Connect bills on your plan, the number of **active conversations** per billing cycle, and how many agents and channel connections you keep running. Agents are included in your current Novu subscription. You do not need a separate Connect subscription. Workflow run overage and agent conversation overage are billed separately. | ||
|
Check notice on line 10 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ## Plans | ||
|
|
||
| Connect limits follow your Novu plan tier. The table below compares price, conversation volume, and the features that change between tiers. | ||
|
|
||
| | | Free | Pro | Team | Enterprise | | ||
| | --- | --- | --- | --- | --- | | ||
| | **Price** | $0 | $30 / mo | $250 / mo | Custom | | ||
| | **Active conversations** | 100 | 1,000 | 5,000 | Custom | | ||
| | **Extra conversation** | Not available | $0.02 | $0.015 | Custom | | ||
| | **Agents** | 2 | 5 | 10 | Unlimited | | ||
| | **Channels** | 2 | 5 | All available | All available | | ||
| | **Branding** | Novu branding shown | Removable | Removable | Removable | | ||
| | **Custom email domain** | No | No | Yes | Yes | | ||
| | **History** | 24 hours | 7 days | 90 days | Custom | | ||
|
|
||
| Free stops counting new conversations when you hit 100 until your billing cycle resets or you upgrade. Pro and Team bill overage at the rates above. Enterprise includes HIPAA BAA, SSO and SCIM, audit logs, and dedicated support. Contact [sales@novu.co](mailto:sales@novu.co) for Enterprise pricing. | ||
|
Check warning on line 27 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ## Estimate your cost | ||
|
|
||
| Pick a plan and set your expected conversation volume to see how base price and overage add up for one billing cycle. | ||
|
|
||
| <ConnectPricingCalculator /> | ||
|
|
||
| ## Active conversations | ||
|
|
||
| This is the main unit Connect uses for billing. An **active conversation** is a thread where an agent is involved. You are billed once per billing cycle for that thread, no matter how many messages are exchanged while it stays active. | ||
|
Check notice on line 37 in content/docs/agents/pricing.mdx
|
||
|
|
||
| In Connect, every conversation is **active**, **resolved**, or **reopened**. Resolving a conversation ends its active period for billing. If the user messages again and Connect reopens it, that counts as a new active conversation. For lifecycle details, refer to [Agent Conversations](/agents/conversations). | ||
|
Check notice on line 39 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ### What counts | ||
|
|
||
| 1. A user starts a thread, or a resolved conversation is reopened. | ||
|
Check warning on line 43 in content/docs/agents/pricing.mdx
|
||
| 2. That counts as one active conversation for the current billing cycle. | ||
| 3. More messages in the same active thread do not add to the count. | ||
|
Check notice on line 45 in content/docs/agents/pricing.mdx
|
||
| 4. When the conversation is resolved, it stops being active. | ||
|
Check warning on line 46 in content/docs/agents/pricing.mdx
|
||
| 5. A later reopen starts a new active conversation. | ||
|
|
||
| ### Billing cycle comes first | ||
|
|
||
| Your billing cycle is the main window for counting conversations. Retention limits and platform session rules only matter when they end sooner than your billing cycle. | ||
|
Check notice on line 51 in content/docs/agents/pricing.mdx
|
||
|
|
||
| If a conversation starts at 11:00 AM today and your plan renews tomorrow at 7:00 AM, it counts only until 7:00 AM. It does not carry into the next cycle just because the thread is still open on the channel. | ||
|
Check notice on line 53 in content/docs/agents/pricing.mdx
|
||
|
|
||
| Connect applies whichever limit ends first: billing cycle reset, plan history retention, or the platform's own session rules. | ||
|
|
||
| ### By platform | ||
|
|
||
| Platforms handle threads differently. Connect maps each one to the same active, resolved, and reopened model. | ||
|
|
||
| | Platform | One active conversation | New conversation starts when | | ||
| | --- | --- | --- | | ||
| | **Slack** | One DM or channel thread with the agent, even with many messages or participants | The conversation is resolved and reopened, or a new thread starts | | ||
|
Check notice on line 63 in content/docs/agents/pricing.mdx
|
||
| | **Microsoft Teams** | One chat or channel thread with the agent | Same as Slack | | ||
| | **Email** | One thread with the same subject and participants | The conversation is resolved and a later reply reopens it | | ||
|
Check warning on line 65 in content/docs/agents/pricing.mdx
|
||
| | **WhatsApp** | One user chat within the 24-hour messaging window | The session expires and the user sends a new message | | ||
| | **Telegram** | One chat with the bot | A new session starts after the previous one was resolved | | ||
|
Check warning on line 67 in content/docs/agents/pricing.mdx
|
||
|
|
||
| | Scenario | Billed as | | ||
| | --- | --- | | ||
| | 80 messages in one Slack DM all month | 1 active conversation | | ||
| | Agent and six teammates in one Slack channel all week | 1 active conversation | | ||
| | Email resolved on the 10th, reopened on the 20th (same cycle) | 2 active conversations | | ||
|
Check failure on line 73 in content/docs/agents/pricing.mdx
|
||
| | WhatsApp 24-hour window expires, user replies the next day | 2 active conversations | | ||
| | Activity in January, none in February | 1 in January, 0 in February | | ||
| | One-way outbound email with no reply path | 1 active conversation | | ||
| | Thread starts at 11:00 AM today, plan renews at 7:00 AM tomorrow | 1 in the current cycle, counted until 7:00 AM | | ||
|
Check notice on line 77 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ## Agents and channels | ||
|
|
||
| These limits control how much Connect infrastructure you can run in parallel. They are capacity caps, not metered add-ons. | ||
|
Check notice on line 81 in content/docs/agents/pricing.mdx
|
||
|
|
||
| **Agents** are distinct personas or bots in your workspace. One agent on four channels is still one agent. Paused agents do not count. | ||
|
Check notice on line 83 in content/docs/agents/pricing.mdx
|
||
|
|
||
| **Channels** are external connections: one Slack workspace, one WhatsApp number, one Telegram bot, or one email domain. Multiple Slack channels in the same workspace count as one connection. | ||
|
Check notice on line 85 in content/docs/agents/pricing.mdx
|
||
|
|
||
| At your limit, upgrade or pause an existing agent or channel. Connect does not bill per extra agent or channel. | ||
|
Check warning on line 87 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ## Included on every plan | ||
|
|
||
| Every tier ships with the same channels, agent providers, and core Connect capabilities. Plans differ in volume and team features, not in what you can build. | ||
|
|
||
| - **Channels:** Email, Slack, Microsoft Teams, WhatsApp, Telegram | ||
|
Check warning on line 93 in content/docs/agents/pricing.mdx
|
||
| - **Agent providers:** Managed Claude Code, AWS Bedrock, Google Vertex AI, Azure AI Foundry, Claude Platform on AWS, Mistral, LangChain and LangGraph, custom code | ||
|
Check warning on line 94 in content/docs/agents/pricing.mdx
|
||
| - **Capabilities:** Identity resolution, tools and skills, MCP OAuth, credentials vault, shared context across channels, streaming, reactions, replies, actions, forms, cards | ||
|
Check warning on line 95 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ## FAQ | ||
|
Check warning on line 97 in content/docs/agents/pricing.mdx
|
||
|
|
||
| Answers to common questions about conversation limits, plan changes, and compliance. | ||
|
|
||
| ### What happens when I exceed my conversation limit? | ||
|
Check warning on line 101 in content/docs/agents/pricing.mdx
|
||
|
|
||
| Pro and Team bill overage at $0.02 and $0.015 per extra conversation. Free is capped until your cycle resets or you upgrade. Enterprise uses committed volume. | ||
|
Check warning on line 103 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ### What happens when I hit my agent or channel limit? | ||
|
Check warning on line 105 in content/docs/agents/pricing.mdx
|
||
|
|
||
| You upgrade or disable an existing agent or channel. Connect does not meter agents or channels separately. | ||
|
Check notice on line 107 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ### Are channels or agent providers limited to higher plans? | ||
|
Check notice on line 109 in content/docs/agents/pricing.mdx
|
||
|
|
||
| No. Every channel and provider is on every plan. Plans differ in how many agents and connections you can run, plus team and security features. | ||
|
Check notice on line 111 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ### Can I change plans without losing data? | ||
|
Check warning on line 113 in content/docs/agents/pricing.mdx
|
||
|
|
||
| Yes. Agents, conversations, and connections stay in place. On downgrade, items above the new limit are paused, not deleted. | ||
|
Check notice on line 115 in content/docs/agents/pricing.mdx
|
||
|
|
||
| ### Is Connect HIPAA compliant? | ||
|
Check warning on line 117 in content/docs/agents/pricing.mdx
|
||
|
|
||
| HIPAA BAAs are on Enterprise. Contact [sales@novu.co](mailto:sales@novu.co) if you need to route protected health information through Connect. | ||
|
Check notice on line 119 in content/docs/agents/pricing.mdx
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| 'use client'; | ||
|
|
||
| import { useMemo, useState } from 'react'; | ||
| import { cn } from '@/lib/cn'; | ||
|
|
||
| type PlanId = 'free' | 'pro' | 'team'; | ||
|
|
||
| type Plan = { | ||
| id: PlanId; | ||
| name: string; | ||
| monthlyPrice: number; | ||
| includedConversations: number; | ||
| overageRate: number | null; | ||
| agents: number | 'Unlimited'; | ||
| channels: string; | ||
| }; | ||
|
|
||
| const PLANS: Plan[] = [ | ||
| { | ||
| id: 'free', | ||
| name: 'Free', | ||
| monthlyPrice: 0, | ||
| includedConversations: 100, | ||
| overageRate: null, | ||
| agents: 2, | ||
| channels: '2', | ||
| }, | ||
| { | ||
| id: 'pro', | ||
| name: 'Pro', | ||
| monthlyPrice: 30, | ||
| includedConversations: 1000, | ||
| overageRate: 0.02, | ||
| agents: 5, | ||
| channels: '5', | ||
| }, | ||
| { | ||
| id: 'team', | ||
| name: 'Team', | ||
| monthlyPrice: 250, | ||
| includedConversations: 5000, | ||
| overageRate: 0.015, | ||
| agents: 10, | ||
| channels: 'All available', | ||
| }, | ||
| ]; | ||
|
|
||
| const MAX_CONVERSATIONS = 20000; | ||
| const FREE_TO_PRO_THRESHOLD = 300; | ||
| const PRO_TEAM_TO_FREE_THRESHOLD = 100; | ||
|
|
||
| function formatCurrency(value: number): string { | ||
| return new Intl.NumberFormat('en-US', { | ||
| style: 'currency', | ||
| currency: 'USD', | ||
| minimumFractionDigits: value % 1 === 0 ? 0 : 2, | ||
| maximumFractionDigits: 2, | ||
| }).format(value); | ||
| } | ||
|
|
||
| function formatNumber(value: number): string { | ||
| return new Intl.NumberFormat('en-US').format(value); | ||
| } | ||
|
|
||
| export function ConnectPricingCalculator(): React.ReactElement { | ||
| const [selectedPlanId, setSelectedPlanId] = useState<PlanId>('pro'); | ||
| const [conversations, setConversations] = useState(1500); | ||
|
|
||
| const selectedPlan = PLANS.find((plan) => plan.id === selectedPlanId) ?? PLANS[1]; | ||
|
|
||
| const estimate = useMemo(() => { | ||
| const extraConversations = Math.max(0, conversations - selectedPlan.includedConversations); | ||
| const overageCost = | ||
| selectedPlan.overageRate === null ? 0 : extraConversations * selectedPlan.overageRate; | ||
| const total = selectedPlan.monthlyPrice + overageCost; | ||
| const isOverFreeLimit = | ||
| selectedPlan.id === 'free' && conversations > selectedPlan.includedConversations; | ||
|
|
||
| return { | ||
| extraConversations, | ||
| overageCost, | ||
| total, | ||
| isOverFreeLimit, | ||
| }; | ||
| }, [conversations, selectedPlan]); | ||
|
|
||
| function handleConversationsChange(value: number): void { | ||
| const nextValue = Math.min(Math.max(0, value), MAX_CONVERSATIONS); | ||
|
|
||
| setConversations(nextValue); | ||
| setSelectedPlanId((currentPlanId) => { | ||
| if (currentPlanId === 'free' && nextValue > FREE_TO_PRO_THRESHOLD) { | ||
| return 'pro'; | ||
| } | ||
|
|
||
| if ( | ||
| (currentPlanId === 'pro' || currentPlanId === 'team') && | ||
| nextValue < PRO_TEAM_TO_FREE_THRESHOLD | ||
| ) { | ||
| return 'free'; | ||
| } | ||
|
Comment on lines
+91
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user explicitly clicks Team and then drags the slider below 100, the plan silently resets to Free. There's also a one-way threshold inconsistency: Free auto-promotes to Pro at 300 conversations but Pro/Team never auto-promotes to Team — a user on the Pro plan with 5,001 conversations (above the Team threshold) simply sees growing overage instead of being guided toward Team. Both behaviours could confuse users trying to compare plans. Prompt To Fix With AIThis is a comment left during a code review.
Path: src/components/agents/pricing-calculator.tsx
Line: 91-101
Comment:
**Auto-switching overrides manual Team plan selection unexpectedly**
When a user explicitly clicks Team and then drags the slider below 100, the plan silently resets to Free. There's also a one-way threshold inconsistency: Free auto-promotes to Pro at 300 conversations but Pro/Team never auto-promotes to Team — a user on the Pro plan with 5,001 conversations (above the Team threshold) simply sees growing overage instead of being guided toward Team. Both behaviours could confuse users trying to compare plans.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| return currentPlanId; | ||
| }); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="not-prose my-6 rounded-lg border bg-fd-card p-5 shadow-sm"> | ||
| <div className="mb-5"> | ||
| <h3 className="text-base font-medium text-fd-foreground">Estimate your monthly cost</h3> | ||
| <p className="mt-1 text-sm text-fd-muted-foreground"> | ||
| Pick a plan and set how many active conversations you expect each billing cycle. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="mb-6"> | ||
| <p className="mb-2 text-sm font-medium text-fd-foreground">Plan</p> | ||
| <div className="grid gap-2 sm:grid-cols-3"> | ||
| {PLANS.map((plan) => { | ||
| const isSelected = plan.id === selectedPlanId; | ||
|
|
||
| return ( | ||
| <button | ||
| key={plan.id} | ||
| type="button" | ||
| onClick={() => setSelectedPlanId(plan.id)} | ||
| className={cn( | ||
| 'rounded-md border px-3 py-2 text-left transition-colors', | ||
| isSelected | ||
| ? 'border-[#4b73ec] bg-[#f3f6ff] dark:border-[#00d5ff] dark:bg-[#0f1a24]' | ||
| : 'border-fd-border hover:border-fd-muted-foreground/40' | ||
| )} | ||
| > | ||
| <span className="block text-sm font-medium text-fd-foreground">{plan.name}</span> | ||
| <span className="block text-xs text-fd-muted-foreground"> | ||
| {plan.monthlyPrice === 0 | ||
| ? '$0 / month' | ||
| : `${formatCurrency(plan.monthlyPrice)} / month`} | ||
| </span> | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="mb-6"> | ||
| <div className="mb-2 flex items-end justify-between gap-3"> | ||
| <label | ||
| htmlFor="connect-pricing-conversations" | ||
| className="text-sm font-medium text-fd-foreground" | ||
| > | ||
| Active conversations per billing cycle | ||
| </label> | ||
| <input | ||
| id="connect-pricing-conversations-input" | ||
| type="number" | ||
| min={0} | ||
| max={MAX_CONVERSATIONS} | ||
| value={conversations} | ||
| onChange={(event) => handleConversationsChange(Number(event.target.value))} | ||
| className="w-28 rounded-md border border-fd-border bg-fd-background px-2 py-1 text-right text-sm text-fd-foreground" | ||
| /> | ||
|
Comment on lines
+148
to
+162
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Prompt To Fix With AIThis is a comment left during a code review.
Path: src/components/agents/pricing-calculator.tsx
Line: 148-162
Comment:
**Number input has no accessible label**
The `<label>` element's `htmlFor="connect-pricing-conversations"` points to the range slider, but the numeric `<input>` has `id="connect-pricing-conversations-input"` with no associated label. Screen readers will not announce the purpose of the number field when it receives focus.
How can I resolve this? If you propose a fix, please make it concise. |
||
| </div> | ||
| <input | ||
| id="connect-pricing-conversations" | ||
| type="range" | ||
| min={0} | ||
| max={MAX_CONVERSATIONS} | ||
| step={50} | ||
| value={conversations} | ||
| onChange={(event) => handleConversationsChange(Number(event.target.value))} | ||
| className="w-full accent-[#4b73ec] dark:accent-[#00d5ff]" | ||
| /> | ||
| <div className="mt-1 flex justify-between text-xs text-fd-muted-foreground"> | ||
| <span>0</span> | ||
| <span>{formatNumber(MAX_CONVERSATIONS)}</span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="rounded-md border border-fd-border bg-fd-background p-4"> | ||
| <dl className="space-y-2 text-sm"> | ||
| <div className="flex items-center justify-between gap-4"> | ||
| <dt className="text-fd-muted-foreground">Plan price</dt> | ||
| <dd className="font-medium text-fd-foreground"> | ||
| {formatCurrency(selectedPlan.monthlyPrice)} | ||
| </dd> | ||
| </div> | ||
| <div className="flex items-center justify-between gap-4"> | ||
| <dt className="text-fd-muted-foreground">Included conversations</dt> | ||
| <dd className="font-medium text-fd-foreground"> | ||
| {formatNumber(selectedPlan.includedConversations)} | ||
| </dd> | ||
| </div> | ||
| <div className="flex items-center justify-between gap-4"> | ||
| <dt className="text-fd-muted-foreground">Extra conversations</dt> | ||
| <dd className="font-medium text-fd-foreground"> | ||
| {formatNumber(estimate.extraConversations)} | ||
| </dd> | ||
| </div> | ||
| <div className="flex items-center justify-between gap-4"> | ||
| <dt className="text-fd-muted-foreground">Overage charges</dt> | ||
| <dd className="font-medium text-fd-foreground"> | ||
| {selectedPlan.overageRate === null | ||
| ? 'Not available on Free' | ||
| : formatCurrency(estimate.overageCost)} | ||
| </dd> | ||
| </div> | ||
| <div className="border-t border-fd-border pt-2"> | ||
| <div className="flex items-center justify-between gap-4"> | ||
| <dt className="font-medium text-fd-foreground">Estimated total</dt> | ||
| <dd className="text-lg font-semibold text-fd-foreground"> | ||
| {formatCurrency(estimate.total)} | ||
| </dd> | ||
| </div> | ||
| </div> | ||
| </dl> | ||
|
|
||
| {estimate.isOverFreeLimit ? ( | ||
| <p className="mt-3 text-sm text-fd-muted-foreground"> | ||
| Free is capped at {formatNumber(selectedPlan.includedConversations)} active | ||
| conversations per billing cycle. Upgrade to Pro or Team to keep running, or wait for | ||
| your next cycle to reset. | ||
| </p> | ||
| ) : null} | ||
|
|
||
| {selectedPlan.overageRate !== null && estimate.extraConversations > 0 ? ( | ||
| <p className="mt-3 text-sm text-fd-muted-foreground"> | ||
| Overage is billed at {formatCurrency(selectedPlan.overageRate)} per extra conversation | ||
| on the {selectedPlan.name} plan. | ||
| </p> | ||
| ) : null} | ||
| </div> | ||
|
|
||
| <p className="mt-3 text-xs text-fd-muted-foreground"> | ||
| Enterprise pricing is custom. For committed volume, HIPAA BAA, SSO, or SCIM, contact{' '} | ||
| <a href="mailto:sales@novu.co" className="text-fd-primary underline underline-offset-2"> | ||
| sales@novu.co | ||
| </a> | ||
| . | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatCurrencytruncates Team overage rate to wrong valuemaximumFractionDigits: 2causesformatCurrency(0.015)to render as$0.01instead of$0.015. In JavaScript,0.015is stored as0.01499999...in IEEE 754, so rounding to 2 decimal places floors to$0.01— a 33% understatement of the Team plan's per-conversation rate. This appears in the overage hint on line 228 when the Team plan is selected with extra conversations.maximumFractionDigitsshould be increased to3to accommodate this rate.Prompt To Fix With AI