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.
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), picks an offering, picks the rate plan (and version), then adds line items with optional discounts.
LOCK THE MATH
Price
Subtotal, discounts, and Total ACV (Annual Contract Value) computed in real time. Rate plan version pinned at quote create — pricing cannot drift during negotiation.
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.
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 tenant 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
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.
DRAFTSales rep is still editing. Customer cannot see this. Pricing math runs but quote_number is not yet visible externally.Sales rep (submit) or scheduler (expire)
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)
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 in real time as line items are added.
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 the rate plan, then the rate plan version. The rate plan version is what gets pinned to the quote — once chosen, the math is locked even if the operator later modifies the rate plan.
3Step 3 — Price: Add line items (quantity, unit price, discount). The wizard auto-computes subtotal, total discount, and Total ACV. Each line item supports per-line discount in addition to the quote-level discount.
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. Sales rep can Save as Draft (stays editable) or Submit for Approval (transitions DRAFT → IN_REVIEW, triggers approval rules engine).
PRO TIP
The sticky ACV sidebar on the right of the wizard recalculates instantly on every line item change. This is what reps show on screen-shares during live deal negotiations — no spreadsheet required.
quote_numberVARCHAR, unique per tenantHuman-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 tenant.
billing_entity_idUUID NOT NULLWhich billing entity (legal entity) issues this quote. Drives invoice branding, tax jurisdiction, and bank details after conversion.
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_idVARCHAR(36) NOT NULLWhich offering this quote sells. Must be PUBLISHED and visible to the tenant.
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.
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).
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.
sort_orderINTDisplay order on the PDF and in the line items tab.
WARNING
The amount field is always recomputed server-side from quantity, unit_price, discount_percentage, and discount_amount. Client-supplied amounts are ignored. This is the load-bearing pricing invariant.
When a quote is submitted for approval, Aforo evaluates all configured rules for your tenant 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.
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.
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 — a future tenant might want "If customer’s industry = HEALTHCARE, require legal review" or other graph-walk rules.
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.
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.
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 the tenant'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"
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:
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.
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-tenant probe) return HTTP 404 — same shape as not-found — so attackers cannot enumerate token validity by comparing differential error responses.
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.
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.
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.
"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.
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, Total ACV. Status badge top-right.
Line ItemsSortable table of all line items with description, qty, unit_price, discount %, amount, type. Total row at the bottom mirroring quote subtotal/discount/ACV.
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.
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.
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.
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.