Outcome-Based Professional Services
The outcome-based services pricing pattern: instead of billing for hours worked or seats provisioned, you bill when defined milestones are delivered - a project phase signed off, a healthcare encounter completed, an SLA target met, a regulatory filing accepted. The economics align supplier and client around results instead of effort. The hard parts are defining the milestones unambiguously, capturing the evidence that triggers each charge, and getting both sides to agree the milestone has actually been hit.
Real-world examples. Consulting firms (McKinsey/BCG/Bain billing on project phases), creative agencies (per-deliverable invoicing), legal practices (matter-based billing), healthcare providers (per-encounter or per-procedure billing), construction (progress-payment milestones), software-as-a-service-with-success-fee variants (charge X% of customer's revenue gain), regulated services (charge per accepted regulatory filing). Common shape: contract defines a list of milestones with associated fees, each milestone has acceptance criteria, charge fires when the criteria are met, both parties sign off on the milestone, audit trail tracks the evidence end-to-end.
The shape of the problem
Outcome-based billing replaces "regular calendar invoice" with "event-driven invoice," which surfaces unique constraints:
- Milestone definition is the contract. What exactly counts as "phase 1 delivered"? Acceptance criteria need to be unambiguous, machine-checkable where possible, and tied to evidence both sides can verify.
- Asymmetric trigger sources. Some milestones trigger automatically (SLA target met, regulatory acceptance received via API), some require manual approval (client signs off on a deliverable), some need both (system says it's done, client confirms before charge fires).
- Evidence trail. When the customer disputes the charge three months later, you need to show the sign-off, the supporting documents, and the timestamp. Compliance auditors expect the same.
- Partial milestones. Phase 1 delivered at 90% completion - charge 90% now, 10% on full delivery? Many contracts allow tiered acceptance.
- Milestone sequencing and dependencies. Phase 2 can't bill until Phase 1 is accepted. Phase 3 fires only if Phase 2 hits a specific KPI.
- Hold-back / retention. Common in construction: 10% of every milestone held back until the entire project is signed off, released as a final settlement.
Kontier blueprint
A milestone is a phase of the subscription that ends on an event, carrying a line whose cadence is on_event. The line bills once, when the event fires; the event fires when the usage you send for its metric reaches the phase's threshold. Because phases run in order, the milestones are sequenced by the deal itself.
| Concern | Kontier primitive |
|---|---|
| Milestone definition with fee | A phase with ends.kind: "event" and a line stating cadence: on_event and an agreed price |
| Milestone sequencing | Phases run one after the other (starts.kind: "after_previous"); a phase's event is only measured from that phase's start |
| Triggering a milestone | POST /v1/usage-events with the phase's metric_key, sent by your system once sign-off is in |
| Evidence trail | The usage event's metadata (sign-off document, approver), read back with GET /v1/usage-events |
| Knowing it fired | The subscription.event_fired webhook (metric, crossing instant, cut instant) |
| Retention / holdback | A separate final milestone phase that bills the retained share |
Build it
IDs in the examples are placeholders: use the id each create call returns. Here the customer is 0f8fad5b-d9cb-469f-a165-70867728950e, the milestone product 3b241101-e2bb-4255-8caf-4136c566a962 (published as version 1), and the subscription 5d0b6a0e-8a4c-4b6e-9c51-2f7e3c1a9d40.
1. Set up the customer and the milestone product
Code
Give the product a list price with POST /v1/prices and publish it with POST /v1/changes ("operation": "catalog.publish_version", "subject_kind": "product"); see Products. Each engagement then states its own agreed fee per milestone on the subscription, so one product serves every engagement.
2. Create the engagement and write its milestones
A subscription is created empty (a DRAFT with an empty revision 1); the milestones are written as its phases.
Code
A typical 3-phase consulting project worth $500,000, plus a $50,000 KPI bonus. Each phase ends when its acceptance event fires, and its one line bills the milestone fee at that moment:
Code
How to read it:
ends: {"kind": "event", "event": {"metric": "phase_1_accepted", "threshold": "1"}}closes the phase as soon as the usage sent forphase_1_acceptedon this subscription adds up to 1.cadence: {"kind": "on_event", "event": "phase_1_accepted"}bills the line once, at that event end, instead of every period. Anon_eventline is only accepted on a phase that ends on an event (otherwise422 ON_EVENT_LINE_NEEDS_EVENT_END).price: {"kind": "agreed", "amount": "100000.00"}is the fee negotiated for this engagement, in the subscription's currency.- The last phase says
then: "stop": when the bonus event fires, the subscription ends. If the KPI is never reached, cancel the subscription withPOST /v1/subscriptions/{id}/cancelonce the bonus window has passed.
PUT replaces the whole draft, so sending it again gives the same draft; it needs no Idempotency-Key. Start the engagement:
Code
To sell the engagement through a signed offer instead, attach the subscription to a quote (POST /v1/quotes/{id}/revisions); the buyer's signature publishes and starts it.
3. Gate acceptance on your side
There is no approval workflow on milestones in the public API. The sign-off happens in your own process (the delivery lead's review, the client's written acceptance); Kontier bills when you tell it the milestone is accepted. Send the acceptance event only once both sign-offs are in.
4. Trigger Phase 1 sign-off
When the consulting team finishes the assessment and the client signs off, send one usage event for the phase's metric. Put the evidence in metadata:
Code
The event is evaluated against the live revision after it is stored. Its timestamp is the crossing instant: phase 1 ends there, the Phase 1 fee is billed once, and phase 2 opens. subscription.event_fired is sent with the metric, the crossing instant (fired_at) and the instant the cut was placed (cut_at). A retried call with the same idempotency_key does not count twice.
Only usage dated inside the phase counts: an acceptance event for phase 2 sent while phase 1 is still running does not end phase 2. That is what enforces the order of the milestones.
The event names a metric no meter bills, so it can also appear in GET /v1/subscriptions/{id}/usage-without-line (usage no line bills). That is expected for acceptance signals; close the finding by setting its status to waived:
Code
Response 200 (abridged)
A finding's status is open, resolved (the deal turned out to cover the usage) or waived. Only waived can be set; a finding that is already closed answers 409 USAGE_FINDING_CLOSED. The older state field and POST …/waive are deprecated and removed on 2026-11-30.
5. Inspect progress and evidence
Where the engagement stands - which phase is current, which have ended and when:
Code
Each phase carries a window with start, end and state (past, current, future or pending). A phase waiting on an event that has not fired has no end instant yet: its end is null. The older GET …/revisions/live/timeline is deprecated and removed on 2026-11-30. The evidence you recorded is on the events themselves:
Code
6. Apply 10% retention across the engagement
There is no escrow account or withholding rule for retention. Model the holdback in the deal: state each milestone's fee net of the retained 10% (Phase 1 bills 90000.00 instead of 100000.00), and add a final phase before the end that bills the retained total on a engagement_final_signoff event:
Code
Variations
- Time-and-materials hybrid. Add a metered line for billable hours to each phase beside the milestone line, fed by usage events. Milestone lines bill on acceptance; hours bill every period.
- Monthly retainer plus milestones. Add a recurring line (no
cadencestated, orevery_period) to the phases; it bills every period while the milestone line bills once. - Partial acceptance. Split a milestone into two phases (for example 90% on delivery, 10% on full acceptance), each with its own event and fee.
- Healthcare per-encounter billing. Per-encounter charges are usage, not milestones: a metered line with one usage event per encounter, the procedure code in
metadata. - SLA-conditional charges. Let your monitoring system send the event (
"metric_key": "sla_target_met") when the SLA dashboard reports the target hit. - Regulatory acceptance billing. Your filing system sends the event when the regulator confirms the filing, with the regulator's confirmation ID in
metadata. - Bonus + clawback. The bonus milestone bills on the KPI event; a later deterioration is handled by issuing a credit note against that invoice.
- Changing the plan mid-engagement. To re-scope milestones that have not started yet, open a draft with
POST /v1/subscriptions/{id}/revisions/draft, send the whole draft with the new phases toPUT /v1/subscriptions/{id}/revisions/draft(the phases that have started stay as they are), and publish it withPOST /v1/subscriptions/{id}/revisions/draft/publish.
What you don't have to build
- Milestone sequencing: phases run in order, and a phase's event is measured only from that phase's start
- One-time billing at the moment of acceptance (
on_eventlines) - Price, tax and invoice generation for each accepted milestone
- An evidence trail tied to each charge (the triggering usage event and its
metadata) - Notification when a milestone fires (
subscription.event_fired)
Not available in the public API (do not build against these):
- Multi-party sign-off workflows with timeouts and escalation on milestones
- Retention escrow accounting and timed release (model it as a final milestone, as above)
Next steps
- Subscriptions - revisions, phases, lines and the value ladder
- Usage - sending events and idempotency
- Quotes - selling an engagement with a signed offer
- Credit Notes - reversing a milestone charge