Where items live
Items are nested three levels down. A proposal has areas, an area has client-selectable options, and each option holds the items the client is quoted for if they pick it:id is the ProposalAreaOptionId you target when adding, moving, or copying items, and each item’s id is the ProposalItemId in every per-item path. Neither is discoverable any other way, so an integration that edits items starts with a GET.
GET /public/proposals/{ProposalId} also sets a Last-Modified header from the proposal’s lastModifiedDate, which is the cheapest way to poll for outside changes.
Webhooks fire at proposal level only —
proposal.status_changed, proposal.build.status_update, and proposal.outline.status_update. Nothing fires when an item is added, repriced, or deleted, so there is no push signal for item edits made in the Portal.io UI. Re-fetch the proposal instead.Identifying an item
Item type decides whether the item’s name comes back in
model or in name, which is the most common surprise when mapping the model:
PartandCustomItem—brandandmodel,nameisnull.Labor—name, withbrandandmodelnull.Fee— the name you submitted comes back inmodel, notname.
description is a deprecated legacy field. Read shortDescription.
Quantities and totals
quantity is a decimal stored to 2 decimal places, so fractional values are normal — 5.5 hours of labor in the payload above. Zero is legal too, and does not remove the item; delete items does that.
total.amount is the per-unit sellPrice multiplied by quantity, before tax. It is not a tax-inclusive figure and it does not include nested items unless the parent has combined pricing switched on with set combined pricing — which only takes effect on a top-level, non-recurring item that has children.
Recurring-service items sit outside the option’s regular total: they are excluded from total on the area option and counted in totalRecurringService instead. Sum both if you need the full monthly-plus-one-off picture.
Percentage pricing is dynamic
Sell price and cost accept either an absolute amount or a percentage of a basis. Percentages are whole numbers —15 means 15%, resolved to 2 decimal places — and the basis decides how stable the resulting price is:
A percentage-priced item has no fixed
sellPrice of its own: writing to any other item that its basis covers changes it. If your system stores prices, re-read the proposal after a batch of writes rather than trusting the values returned by the individual calls.
When cost is null
cost on an item is null when you do not have permission to see costs. A null cost means “you cannot see it”, not “this item has no cost” — the difference matters if you are calculating margin or deciding whether to write a cost.
List item suppliers is the exception: it returns cost details regardless of that restriction. It is also where the supplier’s numeric id lives, since an item only carries supplier as a display name.