Sign in →
2 min read

CPQ: Configure · Price · Quote

Sales-led B2B quoting workflow that turns a prospect conversation into a signed quote, an internal approval, a customer-accepted contract, and a provisioned subscription — all in one transaction.

Updated 2026-11-11Suggest edits

What is CPQ?#

CPQ stands for Configure · Price · Quote. It is the industry-standard sales motion for enterprise B2B deals where pricing is negotiated, contracts require legal review, and procurement teams demand a formal quote document before signing.

Without CPQ, sales-led deals collapse into ad-hoc spreadsheets, manually drafted PDFs, and out-of-band approval emails. The result: pricing leaks, missed approvals, and post-deal provisioning drift. CPQ replaces that chaos with one workflow: the sales rep configures the deal (which offering, which rate plan, which discount), the system prices it (mathematically, against locked rate plan versions), and a formal quote is generated for approval and signature.

BUILD THE DEAL
Configure
Sales rep selects an existing customer or enters a prospect (email + company), then picks an offering and one of its rate cards. Aforo derives the quote lines from the rate card: the base fee and one usage line per billable unit.
LOCK THE MATH
Price
Committed (fixed) ACV, estimated usage ACV, and their total are computed as the rep works. The quote records the rate card version it was priced from, and the subscription created at conversion bills that same version.
GENERATE THE DOCUMENT
Quote
Internal approval routed via configurable rules. On approval, a branded PDF is generated and a signed URL is emailed to the customer for click-through acceptance.
INFO
Aforo's CPQ targets enterprise SaaS, AI infrastructure, and API platform deals where negotiation is the norm. It is NOT a replacement for self-serve checkout — for that, use + Offerings + the storefront.

How Aforo Provides CPQ#

Aforo's CPQ ships as a first-class feature inside Pricing Studio. There is no separate CPQ product to license, no third-party integration to configure, no Salesforce CPQ data sync to maintain. CPQ lives alongside your rate plans, offerings, and subscriptions — because quotes reference all three and convert into the fourth atomically.

The architecture mirrors what Zuora, Chargebee, and Salesforce CPQ do for the largest SaaS companies — but co-located in your billing platform so there is no data-sync lag, no out-of-band reconciliation, and no separate audit trail. Approvals, acceptance, PDF generation, and conversion all share the same workspace context, the same access controls, and the same audit trail as the rest of Aforo.

CapabilityWithout CPQWith Aforo CPQ
Pricing accuracyReps copy/paste from outdated spreadsheetsRate plan version locked at quote create — math is canonical
Approval governanceSlack DMs and email chainsConfigurable rules engine (discount %, total ACV, discount amount)
Customer signaturePDF attached to email, signed manuallyClick-through acceptance with IP + UA + timestamp audit
ProvisioningManual handoff to ops team post-signatureAtomic Quote → Subscription conversion in one transaction
Audit trailScattered across email + spreadsheet + ticketingEvery quote action captured in one immutable audit trail

Quote Lifecycle (8-State Machine)#

Every quote moves through a deterministic state machine. Transitions are enforced server-side — invalid transitions are rejected immediately, and repeating a transition you've already made (for example, submitting the same draft for approval twice) is a safe no-op.

quote-state-machine.txt
DRAFT ──submitForApproval──▶ IN_REVIEW ──approve──▶ APPROVED ──send──▶ SENT
DRAFT ──send (when no approval rule matches)───────────────────────▶ SENT
IN_REVIEW ──reject──▶ REJECTED (terminal)
SENT ──accept (customer)──▶ ACCEPTED ──convertToSubscription──▶ CONVERTED (terminal)
SENT ──reject (customer)──▶ REJECTED (terminal)
SENT ──expire (hourly scheduler, past valid_until)──▶ EXPIRED (terminal)

State Definitions#

StateMeaningWho Can Transition Out
DRAFTSales rep is still editing. Customer cannot see this. Pricing math runs but quote_number is not yet visible externally.Sales rep — submit for approval, or send directly if no approval rule matches
IN_REVIEWSubmitted to internal approval queue. Approval rules engine routed the quote to a specific approver tier.Approver (approve / reject)
APPROVEDInternal sign-off complete. Customer-facing PDF can now be generated and the signed URL minted.Sales rep (send)
SENTSigned-URL acceptance link emailed to the customer. Quote is visible to customer at /quote/:token (public route).Customer (accept / reject) or scheduler (expire)
ACCEPTEDCustomer clicked Accept after agreeing to terms. IP + UA + timestamp captured. Quote is legally binding under click-through doctrine.Conversion job (convertToSubscription)
CONVERTEDAtomically created: Customer (if prospect) + Subscription + initial API key (if Standard/Agentic API). Terminal state.—
REJECTEDEither internal approver rejected during IN_REVIEW, or customer rejected after SENT. Terminal state.—
EXPIREDHourly scheduler flipped a SENT quote to EXPIRED past valid_until. Only SENT quotes expire — a DRAFT never does. Terminal state.—

Quote Builder Wizard (5 Steps)#

Sales reps build quotes through a 5-step wizard at /cpq/new. The wizard preserves field state on Back, validates each step before allowing Next, and shows a sticky ACV sidebar on the right that recomputes as the term, discount, and usage estimates change.

1Step 1 — Customer: Pick an existing customer from the dropdown (customer_id is set, prospect fields cleared) OR enter prospect details for a net-new lead (prospect_email + prospect_name + prospect_company; customer_id is null until conversion creates one).
2Step 2 — Configure: Pick the offering, then one of the rate cards that offering includes, the term length, and the start date. A subscription only bills rate cards its offering includes, so the picker lists only those.
3Step 3 — Price: Review what the rate card charges. Fixed charges (the base fee) are listed with quantity and total. Each billable unit gets a usage card with its pricing terms in words and an optional expected quantity per billing period. Outcome-based units also take an expected outcome mix. Add a quote-level discount if needed; it applies to fixed charges only.
4Step 4 — Terms: Set payment terms (NET_15 / NET_30 / NET_45 / NET_60 / NET_90 / DUE_ON_RECEIPT), billing frequency (MONTHLY / QUARTERLY / SEMI_ANNUALLY / ANNUALLY), term length in months, valid_until (default 30 days), and currency (defaults to USD).
5Step 5 — Review: Final read-only preview with the three ACV figures. Sales rep can Save as Draft (stays editable) or Submit for Approval. Submit saves what is on screen first, then runs the approval rules: a matching rule moves the quote to IN_REVIEW, otherwise it goes straight to SENT.
PRO TIP
The sticky ACV sidebar recalculates on every change to the term, the discount, or a usage estimate. It is what reps show on screen-shares during live negotiations.

Usage on a Quote#

A rate card can charge a base fee, usage, or both. A quote carries one line per priced part: the recurring base fee and one usage line per billable unit. A usage line states the unit's pricing terms in words, for every pricing model: the per-unit rate, every tier or price band, the included quantity, block size, minimum fee, percentage, and for outcome-based pricing the weight of each outcome (for example SUCCESS 1 · HITL_REQUIRED 0). The same lines appear in the builder, the detail drawer, the PDF, and the customer acceptance page.

Usage has no fixed price until it happens, so a quote keeps three figures apart:

FigureWhat it isExact?
Committed (fixed) ACVBase fee × billing periods in the term, plus any flat per-period unit fees, minus the discount.Yes
Estimated usage ACVEach usage line priced at the expected quantity the rep entered, × billing periods in the term. For outcome-based units the quantity is weighted by the expected outcome mix; with no mix, every call is assumed to succeed.No. An estimate. Usage is billed on actual usage.
Total estimated ACVCommitted + estimated usage. This is the number TOTAL_ACV approval rules judge.Only as exact as the estimate
WARNING
The estimate is not a commitment and not a cap. The customer is billed for actual usage at the quoted rates, whatever the estimate said. The discount never applies to usage rates: the subscription bills usage at the rate card's rates, so the quote does not promise otherwise.

Estimates are optional. A usage line with no estimate reads Not estimated and adds nothing to the total, so the total can no longer be judged against an ACV threshold. To submit such a quote the rep must either enter an expected quantity or tick Send without a usage estimate, and the quote then always goes to an approver. See Approval Rules Engine.

Quote Fields Reference#

Each quote carries the following fields.

Identity & Workspace Fields#

FieldTypeDescription
quote_idUUID PKPrimary key. Generated server-side.
quote_numberVARCHAR, unique per workspaceHuman-readable sequential number (e.g., Q-2026-00042). Visible to customer on PDF.
tenant_idVARCHAR(64) NOT NULLMulti-tenant scope. Every quote belongs to exactly one Aforo workspace.
billing_entity_idUUID NOT NULLWhich billing entity (legal entity) issues this quote. Drives invoice branding, tax jurisdiction, and bank details after conversion.
statusENUM (8 values)DRAFT / IN_REVIEW / APPROVED / SENT / ACCEPTED / CONVERTED / REJECTED / EXPIRED.

Customer Fields (Dual-Mode)#

A quote is built for either an existing customer or a prospect — never both. When a prospect quote is accepted and converted, Aforo automatically creates a customer record for them — idempotent, so a retried conversion never produces a duplicate.

FieldTypeWhen Set
customer_idVARCHAR(36), nullableNOT NULL when quoting an existing customer. NULL until conversion when prospect quote is accepted.
prospect_emailVARCHAR(255), nullableRequired when customer_id is null. Used as the destination for the signed-URL acceptance email.
prospect_nameVARCHAR(255), nullableBuyer’s full name. Required when customer_id is null.
prospect_companyVARCHAR(255), nullableCompany name. Required when customer_id is null. Becomes the customer’s display_name on conversion.

Offering & Rate Plan Fields#

FieldTypeDescription
offering_idVARCHAR(36) NOT NULLWhich offering this quote sells. Must be PUBLISHED and visible in the workspace.
rate_plan_idVARCHAR(36) NOT NULLWhich rate plan within the offering. Drives pricing model + tiers.
rate_plan_version_idVARCHAR(36) NOT NULLPinned version. The whole point of CPQ — the operator can edit the rate plan after quote creation and the quote’s math doesn’t change.

Pricing Fields (NUMERIC 22,8 precision)#

FieldTypeDescription
currencyCHAR(3) NOT NULLISO 4217 (USD, EUR, GBP, etc.). Defaults to USD.
subtotalNUMERIC(22,8) NOT NULLFixed charges before discount: subscription and one-time lines plus flat per-period unit fees. Usage is not part of the subtotal.
discountNUMERIC(22,8) DEFAULT 0Discount applied to the fixed charges, capped at the subtotal. Never applied to usage.
committed_acvNUMERIC(22,8) NOT NULLSubtotal minus discount. Exact.
estimated_usage_acvNUMERIC(22,8) NOT NULLUsage lines priced at the expected quantities. An estimate; 0 when nothing is estimated.
total_acvNUMERIC(22,8) NOT NULLcommitted_acv + estimated_usage_acv. Used for approval routing and dashboard aggregation.

Terms Fields#

FieldTypeValues
payment_termsENUMNET_15, NET_30 (default), NET_45, NET_60, NET_90, DUE_ON_RECEIPT
billing_frequencyENUMMONTHLY, QUARTERLY, SEMI_ANNUALLY, ANNUALLY (default)
term_length_monthsINT NOT NULLLength of the contract in months. Defaults to 12.
valid_untilTIMESTAMPTZ NOT NULLQuote expiration. Hourly scheduler flips a SENT quote to EXPIRED past this date.

Acceptance & Audit Fields#

FieldTypeDescription
acceptance_tokenVARCHAR(64)32-byte cryptographic random in hex (256 bits entropy). Acts as the bearer for the public /quote/:token route.
acceptance_token_expires_atTIMESTAMPTZToken-specific expiry. Defaults to valid_until but can be shorter for time-pressure deals.
accepted_atTIMESTAMPTZ, nullableServer timestamp when customer clicked Accept.
accepted_ipINET, nullableCustomer IP captured at accept time. Audit trail for click-through doctrine.
accepted_user_agentTEXT, nullableCustomer browser/device fingerprint. Audit trail.
signature_envelope_idVARCHAR(128), nullablev2 DocuSign integration. Null in v1.

Conversion Output Fields#

FieldTypeDescription
converted_subscription_idVARCHAR(36), nullableSet automatically when the quote converts from ACCEPTED to CONVERTED.
converted_customer_idVARCHAR(36), nullablePopulated atomically on conversion if quote was prospect-only (customer was created from prospect during conversion).

Audit Timestamps#

Every quote carries created_at, updated_at, submitted_at, approved_at, sent_at, rejected_at, expired_at, and converted_at — all TIMESTAMPTZ, populated by the state machine on transition.

Line Item Fields#

Each quote has one or more line items. Deleting a DRAFT quote removes its line items automatically.

FieldDescription
lineTypeSUBSCRIPTION (recurring base fee), ONE_TIME, DISCOUNT (negative amount), or USAGE (one per billable unit of the rate card).
descriptionLine description. Renders verbatim on the PDF.
quantitySUBSCRIPTION: billing periods in the term. USAGE: the expected quantity per billing period, 0 when not estimated.
unitPricePrice per unit in the quote currency. For tiered and staircase usage the prices live in the terms, not here.
totalSUBSCRIPTION: quantity × unitPrice. USAGE: estimated charge per billing period × billing periods, 0 when not estimated.
usage.pricingTermsUSAGE lines: the pricing terms in words, as printed on the quote.
usage.pricingModelUSAGE lines: PER_UNIT, FLAT_RATE, PERCENTAGE, INCLUDED_QUOTA, GRADUATED, VOLUME_TIERED, STAIRCASE, or OUTCOME_BASED.
usage.estimated / estimatedQuantity / expectedOutcomeMix / estimatedPeriodCostUSAGE lines: whether an estimate was entered, the inputs, and the resulting charge for one billing period.
sortOrderDisplay order on the PDF and in the Line Items tab.
WARNING
Usage lines and their pricing terms are always derived from the rate card by Aforo. A USAGE line sent in a create or update request is ignored; send usageEstimates (metric id + expected quantity) instead. To see the lines and totals before saving, call POST /api/v1/quotes/preview-pricing.

Approval Rules Engine#

When a quote is submitted for approval, Aforo evaluates all configured rules for your workspace and selects the first matching rule, ordered by threshold (lowest threshold wins). The matched rule's required approver role becomes the tier the quote routes to.

Rule Types (v1)#

Rule TypeThreshold FieldExample
DISCOUNT_PCTdiscount_threshold (NUMERIC 5,2)"If quote discount > 15%, require APPROVER role" — Sales reps can give up to 15% without approval; above that triggers manager sign-off.
TOTAL_ACVacv_threshold (NUMERIC 22,8)"If Total ACV > $250,000, require APPROVER role" — Big-dollar deals always require approval regardless of discount. Total ACV is committed ACV plus estimated usage ACV, so a usage-heavy quote is judged on its estimate.
DISCOUNT_AMOUNTdiscount_amount_threshold (NUMERIC 22,8)"If total discount > $50,000, require APPROVER role" — Absolute-dollar guard regardless of deal size.
CUSTOM(v2 placeholder)Deferred to v2 — some teams will want "If customer’s industry = HEALTHCARE, require legal review" or other graph-walk rules.
INFO
Usage quoted without an estimate always needs approval. A quote with a usage line and no expected quantity has no usable total, so no threshold can judge it. Aforo applies a built-in rule named Usage not estimated (APPROVER role) before any rule you configured. You cannot disable it; the way to avoid it is to enter an estimate.

Rule Fields#

FieldTypeDescription
rule_idUUID PKPrimary key.
tenant_idVARCHAR(64) NOT NULLPer-workspace rule scope. Workspace A’s rules never apply to Workspace B.
nameVARCHAR(255) NOT NULLHuman-readable rule name (e.g., "Standard discount guard").
rule_typeENUMDISCOUNT_PCT, TOTAL_ACV, DISCOUNT_AMOUNT, CUSTOM.
discount_thresholdNUMERIC(5,2), nullableUsed when rule_type = DISCOUNT_PCT.
acv_thresholdNUMERIC(22,8), nullableUsed when rule_type = TOTAL_ACV.
discount_amount_thresholdNUMERIC(22,8), nullableUsed when rule_type = DISCOUNT_AMOUNT.
required_approver_roleVARCHAR(64) NOT NULLKeycloak role that the approving user must hold. Typically "APPROVER" or "OWNER".
statusENUMACTIVE (rule is evaluated) or DISABLED (rule is skipped).

Approval Records#

When the engine routes a quote to an approver tier, a quote_approvals row is created with decided_at = NULL. The pending-queue UI scans the partial index idx_quote_approvals_pending WHERE decided_at IS NULL for the hot path.

FieldTypeDescription
approval_idUUID PKPrimary key.
quote_idUUID, FKParent quote.
required_roleVARCHAR(64) NOT NULLSnapshot of the rule’s required_approver_role at submit time. Pinned so role taxonomy changes don’t invalidate in-flight quotes.
rule_idUUID, nullableWhich rule routed this quote here. Null if manually routed.
requested_byVARCHAR(255)User ID of the sales rep who submitted.
requested_atTIMESTAMPTZ NOT NULLWhen the approval was requested.
decided_byVARCHAR(255), nullableUser ID of the approver. Null while pending.
decided_atTIMESTAMPTZ, nullableWhen the approver clicked Approve or Reject.
decisionENUM, nullableAPPROVED or REJECTED. Null while pending.
rejection_reasonTEXT, nullableRequired if decision = REJECTED. Surfaced back to the sales rep.

Customer Acceptance Flow#

After internal approval, the sales rep clicks Send to Customer. This (a) mints a 32-byte cryptographic acceptance token, (b) constructs a customer-facing URL https://<tenant-storefront>/quote/<token>, (c) emails it to the customer (or copies it to clipboard for manual delivery), and (d) transitions the quote to SENT.

Public Route (No Auth Required)#

The token IS the bearer. There is no JWT, no login, no SSO. The customer clicks the link, lands on QuoteAcceptPage in the storefront-ui, and sees the quote with your workspace's branding (logo, legal name, address from billing_entity).

Page StateWhen ShownCTA
LoadingInitial token fetch in flightSkeleton with role=status aria-busy + shimmer
Token-not-foundToken mistyped, missing, or never existed"Quote not found" page; no CTA
ExpiredToken past valid_until OR acceptance_token_expires_at"This quote has expired" + sales-rep contact email
ClosedQuote status ≠ SENT (already accepted, rejected, etc.)"This quote is no longer accepting responses"
Has-dataStatus = SENT, token validQuote summary, fixed charges, usage pricing (terms in words, plus the estimate or "Not estimated" per billable unit), the three ACV figures, then Accept (with terms checkbox) / Decline / Request changes
AcceptedCustomer clicked Accept, conversion succeeded"Welcome! Your subscription is now active. Check your email for next steps."
RejectedCustomer clicked Decline"Thank you for your response. Your sales rep has been notified."

Click-Through Acceptance Doctrine#

v1 ships with click-through acceptance — legally binding under U.S. case law (Specht v. Netscape, ProCD v. Zeidenberg) and equivalent EU + APAC jurisdictions. On Accept, the server captures:

  • Timestamp — accepted_at (server clock, TIMESTAMPTZ)
  • IP address — accepted_ip (INET; X-Forwarded-For respected behind ALB)
  • User agent — accepted_user_agent (TEXT; full UA string for forensics)
  • Token — the cryptographic token itself (proof the customer received the email)
INFO
v2 wires DocuSign for high-touch enterprise deals where signature ceremony is required. The schema already carries signature_envelope_id VARCHAR(128) for that — null in v1.

Brute-Force Defense#

The public route is protected by Kong route route-pricing-public-quotes with per-IP rate limit 10 requests/min. All error paths (token expired, status closed, cross-workspace probe) return HTTP 404 — same shape as not-found — so attackers cannot enumerate token validity by comparing differential error responses.

Quote → Subscription Conversion#

When the customer accepts, Aforo converts the quote into a live subscription as one atomic operation. If any step fails, the whole conversion rolls back and the quote stays ACCEPTED so it can be safely retried. Converting an already-converted quote is a no-op that returns the existing subscription.

Conversion Steps (in order)#

1Validate that the quote is ACCEPTED. An already-converted quote returns its existing subscription (idempotent); a quote in any other state is rejected.
2Resolve the customer — use the existing customer on the quote, or create one automatically for a prospect quote.
3Create the subscription, inheriting the offering, rate plan version, and billing entity from the quote.
4For Standard API and Agentic API products, automatically issue an initial API key. AI Agent and MCP Server products are registered by the operator first.
5Mark the quote CONVERTED and link it to the new subscription and customer.
6Notify downstream systems — welcome email to the customer, ops handoff, and analytics — only after the conversion has fully committed.
WARNING
If subscription creation throws (e.g., billing entity misconfiguration), the whole transaction rolls back. The quote stays ACCEPTED. The customer's Accept click is preserved. The sales rep can retry conversion from the drawer.

CPQ Inbox + Browse Page#

The merged CPQ page at /cpq replaces the prior split between Quotes and Approval Queue. It combines an inbox-style attention strip for approvers with a full browse-and-filter quote table for everyone.

Page Anatomy#

SectionAudienceContent
"Pending your approval" attention cardUsers with APPROVER, OWNER, or ADMIN roleLive count of quotes routed to this user’s approver tier. Click to filter the table to those quotes.
KPI stripEveryone6 cells: Total Quotes / Draft / In Review / Approved / Sent / Total ACV (lifetime). Updates live as quotes transition.
Status filter chipsEveryoneAll / Draft / In Review / Approved / Sent / Accepted / Converted / Rejected / Expired. Multi-select; aria-pressed for active.
Search inputEveryoneFilters by quote_number, customer name, prospect email, prospect company.
Quote tableEveryoneColumns: Quote # / Customer / Offering / Status / Total ACV / Valid Until / Updated. Click row to open drawer.
"+ New Quote" buttonUsers with SALES_REP, OWNER, or ADMIN roleRoutes to /cpq/new (5-step wizard).
PRO TIP
The attention card is gated by role at three layers: sidebar visibility, ROUTE_ROLES route guard, AND in-page useCurrentUser check. Non-approvers see the table and can submit drafts, but the approval bucket is invisible to them.

Quote Detail Drawer (5 Tabs)#

Clicking any quote row opens a slide-in drawer with 5 tabs. Action buttons in the drawer header are state-aware — they appear only when the state machine allows that transition.

TabContent
OverviewQuote metadata: number, customer, offering, rate plan, term length, billing frequency, payment terms, valid_until. Financial summary with Committed (fixed) ACV, Estimated usage ACV, and Total estimated ACV. Status badge top-right.
Line ItemsFixed charges with description, quantity, unit price, and total. Below them, one row per billable unit with its pricing terms and the usage estimate (or "Not estimated").
ApprovalsChronological list of every quote_approvals row. Approver name, required role, decision (APPROVED/REJECTED), decided_at, rejection_reason if applicable. For pending approvals, APPROVER-role users see inline Approve + Reject buttons.
PDF PreviewEmbedded iframe rendering /quotes/{id}/preview (Thymeleaf HTML). For PDF download, button hits /quotes/{id}/pdf which returns application/pdf bytes for browser native download.
ActivityTimeline of every state transition: submitted, approved, sent, accepted, converted. Each entry shows actor + timestamp + delta. Useful for forensic audit of long-running deals.

State-Aware Drawer Actions#

When Status =Buttons Visible (role-gated)
DRAFTEdit (returns to wizard), Submit for Approval, Delete (CASCADE removes line items + approvals)
IN_REVIEWApprove + Reject (APPROVER only), Recall (sales rep can pull back to DRAFT)
APPROVEDSend to Customer (mints token + email), Edit (back to DRAFT, voids prior approval), Mark as Expired
SENTResend Email, Copy Link, Expire Manually, Edit (back to DRAFT — voids token)
ACCEPTEDConvert to Subscription (retry if initial conversion failed), View Customer
CONVERTEDView Subscription, Download PDF, No edits allowed (terminal state)
REJECTED / EXPIREDClone to New Quote (creates a fresh DRAFT pre-populated with the same line items)

Roles & RBAC#

Two CPQ-specific composite Keycloak roles ship in realm-aforo-prod.json. Both compose MEMBER for baseline platform access.

RoleComposesCapabilities
SALES_REPMEMBERCreate + edit DRAFT quotes, submit for approval, send approved quotes, view all quotes in their workspace.
APPROVERMEMBERView pending approval queue, Approve/Reject IN_REVIEW quotes, view "Pending your approval" attention card.
OWNER / ADMIN(broader platform roles)All CPQ operations + approval rule CRUD + override approver decisions.

v1 Scope & v2 Roadmap#

v1 ships the core flow: configure → price → quote → internal approval → customer acceptance → auto-conversion. Each deferred item below becomes its own follow-up prompt when an enterprise customer asks.

Deferred Capabilityv2 Plan
DocuSign / e-signatureThe schema already carries signature_envelope_id. v1 uses click-acceptance with timestamp + IP + UA audit — legally binding under click-through doctrine. v2 wires DocuSign for high-touch enterprise deals where signature ceremony is required.
Multi-step approval routingSales → Legal → Finance → CFO chains. v1 ships single-level approval. v2 introduces approval workflows with sequential or parallel approver tiers.
Contract redlines / negotiation trackingv1 forces the sales rep to regenerate the quote if terms change. v2 introduces a redline diff view between quote revisions.
Renewal automationv1 renewals use standard rate plan auto-renew. v2 preserves negotiated terms across renewal cycles — e.g., a 20% discount granted in year 1 carries forward.
Quote templates / pre-built bundlesv1 builds every quote from scratch. v2 adds a template library so reps can clone a "Standard Enterprise" template and just edit the line quantities.
Volume commitments / minimum spend with true-upv1 supports discounts + custom rates. v2 adds annual minimum spend commitments with quarterly true-up reconciliation.
Multi-product bundles in a single quotev1 limits a quote to one rate plan. v2 supports multiple rate plans per quote (e.g., "Enterprise API + Premium Support + Custom AI Model" in one signed contract).

CPQ depends on the broader Aforo monetization fabric. Quotes reference rate plan versions, offerings, and billing entities; conversions produce subscriptions that flow into the billing engine.

Plan Studio
Rate plans + offerings that quotes reference. Pin rate plan versions to lock pricing math.
Billing & Invoicing
After conversion, the subscription drives invoicing through the 10-stage billing pipeline.
Margin Guards
Quote ACV is one input to margin protection — deep discounts can trigger margin alerts pre-approval.
Audit & Compliance
Every quote state transition is captured in immutable audit logs. SOC2-ready out of the box.
Aforo Documentation — Aforo Docs