Browse developers
Start here
Working with the API
Receiving events
Proposals
Reference
- Developers
- Proposals
Proposals: the pricing rules that matter
How money is represented, why per-cadence figures are never added together, and what freezes when a proposal is sent.
Last updated
One rule governs everything below, and it is worth stating before anything else: render the totals the API returns; never compute one. The figures on a proposal are the binding consideration in an agreement a client signs, they feed the VAT a company reports, and they are arranged in a shape that is easy to add up incorrectly. Every figure you need is already on the response.
Money is a string
Amounts cross the wire as fixed two-decimal strings, never as JSON numbers.
{ "subtotal": "150.00", "vatAmount": "34.50", "totalAmount": "184.50" }
That is deliberate. A JSON number is a double, and a double cannot hold 184.50 exactly, so a
value that survives one hop intact can come back a penny short after two. Keep it a string, or
parse it into your own decimal type. Never into a float.
Send amounts the same way. All arithmetic on our side is done in integer pence or cent, and
rounding is half-up at explicitly chosen points. An amount with more than two decimal places is
refused rather than quietly rounded, with code: pricing-invalid and
pricingCode: PRECISION_EXCEEDED; see Errors for the full list.
VAT is applied per bucket
A proposal carries a single VAT rate, and it is applied to each bucket of charges separately rather than to a merged sum. In every bucket:
subtotalis net of VAT, in both VAT-inclusive and VAT-exclusive pricing modesvatAmountis the VAT on that bucket alonetotalAmountis gross, andsubtotal + vatAmountequals it exactly
That last property holds because the three are computed together rather than derived from one another after rounding. If your own recalculation disagrees by a penny, yours is the one that is wrong.
Per-cadence figures are never added together
Charges are grouped by how often they recur, and each group carries its own subtotal and its own VAT:
{
"oneOffAmount": "600.00",
"oneOffVatAmount": "138.00",
"oneOffTotalAmount": "738.00",
"recurringGroups": [
{
"frequency": "MONTHLY",
"multiplier": 12,
"subtotal": "150.00",
"vatAmount": "34.50",
"totalAmount": "184.50",
"annualisedSubtotal": "1800.00",
"annualisedVatAmount": "414.00",
"annualisedTotalAmount": "2214.00"
},
{
"frequency": "QUARTERLY",
"multiplier": 4,
"subtotal": "400.00",
"vatAmount": "92.00",
"totalAmount": "492.00",
"annualisedSubtotal": "1600.00",
"annualisedVatAmount": "368.00",
"annualisedTotalAmount": "1968.00"
}
]
}
Do not add those groups together. A monthly charge plus a quarterly charge plus a one-off fee is not a number the client ever pays, and it does not describe any period. Present each group against its own cadence, exactly as the client will be invoiced.
The response also carries merged totalAmount and vatAmount fields. Those exist because
database columns exist, they are used for sorting a list, and they are not displayable figures.
Treat them as internal.
The cadences today are ONE_OFF, WEEKLY, FORTNIGHTLY, MONTHLY, BIMONTHLY, QUARTERLY,
SEMI_ANNUALLY and ANNUALLY, and the set grows. Handle an unrecognised value through
multiplier, which is the periods per year, rather than switching exhaustively on frequency.
Annualised figures are run rates
annualisedSubtotal, annualisedVatAmount and annualisedTotalAmount restate a cadence at an
annual scale. Label them as run rates wherever you print them. They are not a contract value
and not an amount that will be invoiced: multiplier is 52 for a weekly cadence and 26 for a
fortnightly one, and the calendar does not agree, since roughly one year in six carries a
fifty-third weekly run.
Note also that annualisedVatAmount is the VAT on the annual face value rather than the per-period
VAT multiplied up. Both answers are correct; they answer different questions, and the per-period
figure is the one that appears on an invoice.
Estimates make every figure a lower bound
A service line may be an estimate, with a range. The engine prices an estimate at its lower bound, and the upper bound is carried for display only and is never summed into anything.
Where a proposal contains any estimate line, hasEstimateLines is true, and every bucket total on
that proposal is therefore a lower bound. Render those figures with a "from" label. The flag is
proposal-wide rather than per bucket, so when it is set, label all of them. An unlabelled figure
understates the engagement, and it is the headline number on both the proposal the client reads and
the letter they sign.
What a proposal contains is frozen when it is sent
At the moment of sending we freeze the content, the resolved merge tokens and the pricing snapshot onto the proposal. From then on, changing a price in your service catalogue does not alter a proposal already sitting with a client, and neither does editing the template it came from. What the client sees is what they were sent.
The consequence for your integration: a sent proposal is not editable. A write against one is
refused with 400 and code: proposal-not-editable, carrying the current status. An illegal
state change, such as accepting something already declined, is 409 with
code: proposal-transition-invalid and the from and to states. Neither is worth retrying. The
way to correct a proposal that has gone out is to issue a new one.
Because acceptance runs a real signing ceremony, an accepted proposal has an envelope behind it,
and PROPOSAL_ACCEPTED carries its envelopeId alongside the accepted fees. See
Webhooks for that payload.
In short
Take oneOffTotalAmount and each row of recurringGroups and put them on the screen against their
own cadence. Prefix them with "from" when hasEstimateLines is true. Say "run rate" next to
anything annualised. Do not sum, do not re-derive, and do not parse a money string into a float.