Skip to main content
A proposal is the primary sales document you send to a client. It captures everything about a project — the rooms and zones involved, the equipment and labor for each configuration, and the financial totals the client sees. Every proposal belongs to a single dealer account and is owned by a salesperson on that account.

Proposal structure

Proposals follow a strict four-level hierarchy: a Proposal contains one or more Areas, each Area contains one or more Options, and each Option contains Line Items.

Proposal

The top-level entity. Has an ID, a human-readable number, a name, a salesperson, a status, a financial summary, and an optional assigned contact and location.

Area

A named room or zone within the proposal — for example, “Living Room” or “Master Bedroom”. Each area gets one default option automatically when it is created. Area names must be unique within the proposal.

Option

A configuration within an area. Each area supports up to 3 options. Options have a status, a client-facing description, and internal installer notes.

Line items

Individual equipment or labor items within an option. You add these from the Portal.io catalog.

Proposal fields

FieldDescription
idUnique numeric identifier. Use this in all subsequent API calls.
numberHuman-readable sequential number assigned by Portal.io.
nameDisplay name for the proposal.
statusCurrent lifecycle state (see Proposal statuses).
financialSummaryTotals for parts, labor, fees, discounts, and tax.
customerThe assigned contact (person or company).

Area constraints

  • Area names must be unique within the proposal.
  • Creating an area automatically creates one default Option in Draft status inside it.
  • A 400 error is returned if you try to create a second area with the same name.

Option constraints

  • Each area supports a maximum of 3 options.
  • Options are created in Draft status.
  • Each option has a ClientDescription (shown to the customer) and InternalNotes (visible to your team only).
  • Attempting to add a fourth option to an area returns a 400 error.

Proposal statuses

Portal.io proposals move through a defined set of statuses:
StatusMeaning
DraftProposal is being built and can be edited.
SubmittedProposal has been sent to the client for review.
ViewedByClientClient has opened and viewed the proposal.
AcceptedClient has accepted the proposal.
DeclinedClient has declined the proposal.
DelayedProposal has been put on hold.
CompletedWork on the proposal is finished.
EmailFailedThe proposal email failed to deliver.
ExpiredThe proposal has passed its expiration date.
This status list is sourced from the current API specification and should be validated against the latest API behavior. If you encounter a status not listed here, please contact support@portal.io.
Once a proposal reaches Accepted, Completed, or another terminal state, the API will not allow edits — any write operation on that proposal returns a 409 Conflict. Use a change order to make modifications after approval.

Financial summary

The financial summary is included on every proposal response. It breaks down costs into:
  • Parts subtotal — total equipment cost before discounts
  • Parts discount — applied discount amount and type (Percentage or flat)
  • Labor total — total labor charges
  • Fee total — additional fees
  • Sales tax — calculated automatically based on the assigned location
Tax is calculated using the ClientLocation method: Portal.io looks up the applicable tax rates for the address on the assigned location and applies them to the taxable portions of the proposal. Until you assign a location, tax totals remain zero.

Client vs. dealer content

Every proposal carries two separate content layers:
  • Client-facing content — the proposal description and each option’s ClientDescription. This content appears on documents sent to the customer.
  • Internal content — proposal-level InternalNotes and each option’s InternalNotes. This content is only visible to your dealer team and never appears on customer documents.
Use separate endpoints to update each layer:
  • POST /public/proposals/{ProposalId}/description — updates the client-facing proposal description
  • POST /public/proposals/{ProposalId}/internalnotes — updates the internal installer notes
  • POST /public/proposals/{ProposalId}/area-options/{AreaOptionId}/clientdescription — updates an option’s client description
  • POST /public/proposals/{ProposalId}/area-options/{AreaOptionId}/installernotes — updates an option’s installer notes

Change orders

When a proposal is already approved or completed and the client needs modifications, you create a change order rather than editing the original proposal directly. Change orders are linked to their parent proposal and have their own:
  • Unique ID and number
  • Status (following the same lifecycle as proposals)
  • Financial summary (showing only the delta — the incremental cost of the changes)
  • Assigned customer and dates
To retrieve change orders for a proposal:
  • GET /public/proposals/{ProposalId}/changeorders — lists all change orders
  • GET /public/proposals/{ProposalId}/changeorders/{ChangeOrderId} — retrieves a specific change order

Building a proposal with the API

Use this sequence to build a complete proposal from scratch:
1

Create the proposal

Call POST /public/proposals with a SalesPersonId (required) and an optional Name. The response includes the new proposal id and number — save the id for all subsequent calls.
curl -X POST https://api.portal.io/public/proposals \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d SalesPersonId=42 \
  -d Name="Smith Residence AV"
2

Add areas

Call POST /public/proposals/{id}/area for each room or zone. Each area you create gets a default option automatically.
curl -X POST https://api.portal.io/public/proposals/1001/area \
  -d Name="Living+Room"
3

Add additional options (optional)

If you want to present the client with multiple configurations per area, call POST /public/proposals/{id}/area/{AreaId}/option to add up to 2 more options (3 total per area). You can supply a ClientDescription and InternalNotes at creation time.
4

Add catalog items to options

Use the catalog endpoints to search for equipment and labor, then add line items to each option. Refer to the Catalog section of the API reference for the available endpoints.
5

Assign a contact and location

Assign a contact with POST /public/proposals/{id}/contact/{ContactId}. If the contact has a single primary location, Portal.io assigns it automatically. Otherwise, assign the location separately with POST /public/proposals/{id}/location/{LocationId}. Assigning a location triggers tax recalculation.
6

Use AI Builder to generate content (optional)

Upload source content (text, audio, or video) and trigger AI outline generation and proposal building. The AI Builder works asynchronously — use the Proposal Build Status Changed and Proposal Outline Status Changed webhook events to know when results are ready.

AI Builder

Portal.io includes an AI Builder that can auto-generate proposal content from uploaded source material such as meeting notes, audio recordings, or video files. The AI Builder operates asynchronously across two phases:
  1. Outline generationPOST /public/proposals/{ProposalId}/ai/outline starts the process. Poll GET /public/api/proposals/{ProposalId}/ai/outline or listen for the Proposal Outline Status Changed webhook to know when the outline is ready.
  2. Proposal build — once you have a completed outline, call POST /public/proposals/{ProposalId}/ai/build. Listen for the Proposal Build Status Changed webhook to know when the build completes.

Available API operations

OperationEndpoint
List proposalsGET /public/proposals
Create proposalPOST /public/proposals
Get proposal detailsGET /public/proposals/{ProposalId}
Update proposal name/salespersonPOST /public/proposals/{ProposalId}
Update client descriptionPOST /public/proposals/{ProposalId}/description
Update internal notesPOST /public/proposals/{ProposalId}/internalnotes
Add areaPOST /public/proposals/{ProposalId}/area
Add option to areaPOST /public/proposals/{ProposalId}/area/{AreaId}/option
List change ordersGET /public/proposals/{ProposalId}/changeorders
Get change orderGET /public/proposals/{ProposalId}/changeorders/{ChangeOrderId}
Assign contactPOST /public/proposals/{ProposalId}/contact/{ContactId}
Assign locationPOST /public/proposals/{ProposalId}/location/{LocationId}