Add/Edit a Step

View as Markdown

Why edit a step, what’s editable, and what locks it down

A step (UsageCommitmentStep) is a value tier within a schedule. It carries the commitmentValue, commitmentUnitType, pricePriority, prepaid config, and the base billing terms it tracks. Edit a step (rather than a schedule) when the change concerns the amount or prepaid config of one tier, without touching dates, cadence, or which event types are being tracked.

What’s editable on a step

Send a partial update to PATCH .../steps/:stepId. Only four fields, all optional:

  • commitmentValue: new commitment amount
  • prepaidValue: new prepaid amount
  • prepaidEnabled: toggle prepaid on or off
  • pricePriority: LOW or HIGH

Intentionally NOT editable on a step, per the DTO’s own documentation:

  • commitmentUnitType: changing it would diverge from sibling steps in the schedule (all steps in a schedule must share one unit type).
  • sequence: server-managed.
  • Base BT linkages: all steps in a schedule share the same event-type set, and a single step’s BT edit can’t preserve that invariant on its own. BT changes must go through the schedule-level billingTermsToAdd and billingTermsToDelete (see Add billing terms to steps within a schedule).

Step values are absolute, not additive

Every value you send on this PATCH, pre- or post-invoice, is the new total, not a delta to add to the current value. If a step’s commitmentValue is 1000 and you PATCH with { "commitmentValue": 2000 }, the step’s value becomes 2000, not 3000. This holds even on the amendment path (below): the server computes the amendment delta internally as new − old for its own ledger bookkeeping, but the number you send is always the absolute target value, never an increment.

Edit versus amendment: what locks a step down, and how it degrades gracefully

Once a step has a finalized invoice against any of its billing terms (sent, DRAFT+READY, or notice-issued, through getFinalizedInvoiceFilter), the PATCH endpoint doesn’t reject the request outright. Instead, it routes through one of two paths:

  • Pre-invoice: direct edit. The API rewrites commitmentValue, prepaidValue, prepaidEnabled, and pricePriority in place. Response: { success: true, path: "edit" }.
  • Post-finalized-invoice: append-only amendment. prepaidEnabled and pricePriority become immutable; changing either returns 400: "cannot update prepaidEnabled or pricePriority once the step is locked by a finalized invoice (sent, ready-to-send, or notice-issued); only commitmentValue and prepaidValue are amendable." commitmentValue and prepaidValue are still changeable, but instead of overwriting the original value, the API records the delta as a UsageCommitmentStepAmendment row, preserving the original invoiced baseline so historical invoices still reconcile. Response: { success: true, path: "amendment" }. A no-op PATCH (deltas resolve to zero) short-circuits to { success: true, path: "edit" } without creating an amendment row.

This is why “increase a commitment’s amount” (Example 2) works the same way whether or not the step has already been invoiced: you send the same PATCH either way, and the server decides whether it’s a direct edit or an amendment.

Separately, appending or deleting a step has its own lock: only the last (highest-sequence) step in a schedule can be deleted, and a schedule can never be left with zero steps.

When to add a new step versus a new schedule

Ask: is the amount tiering within the current date range, or does the date range, cadence, or prepay strategy itself need to change?

  • New step: you’re adding a tier within the same schedule, same dates, same cadence. This is the “sequential commitment” pattern: usage fills the current step’s capacity, then rolls into the next step’s pricing (enteredAt).
  • New schedule: the change is calendar-based (a new phase, different cadence, different prepay strategy); see Add/Edit a Schedule.

The hard constraint: a new step must share its schedule’s commitmentUnitType and resolve to the same event-type set as its siblings. If the new tier needs a different unit type or a fundamentally different set of event types, it can’t be a step in the existing schedule; it needs its own schedule instead.

Examples

1. Add a step (new sequential tier)

POST /v3/contracts/:id/commitments/:commitmentId/steps
{
"scheduleId": "550e8400-e29b-41d4-a716-446655440000",
"commitmentValue": 3000,
"commitmentUnitType": "DOLLARS",
"pricePriority": "HIGH",
"prepaidEnabled": false,
"billingTermIds": ["550e8400-e29b-41d4-a716-446655440001"]
}

Required: scheduleId (which schedule on this commitment to append to), commitmentValue, commitmentUnitType, pricePriority, prepaidEnabled, billingTermIds (non-empty). Optional: prepaidValue, overageBillingTerms[] (each referencing a base BT already in billingTermIds through usageBillingTermId), containerConfig (naming and ERP-item overrides for the prepaid and true-up container BTs).

A successful call returns { payload: { stepId, sequence } }: sequence is server-assigned (the next ordinal in that schedule).

Caveats:

  • The call returns 400: "Schedule X does not belong to commitment Y." if scheduleId isn’t found under this commitment.
  • The call returns 400: "commitmentUnitType mismatch on schedule X: existing steps use Y; cannot append a step with Z. All steps in a schedule must share the same commitmentUnitType."
  • The call returns 400: "non-prepaid UNITS steps can only have one associated event type..." if the new step is UNITS, non-prepaid, and its BTs map to more than one event type.
  • The call returns 400: "billing terms X and Y share event type Z and have overlapping date ranges..." if two of the step’s own BTs share an event type with overlapping windows.
  • If prepaidEnabled: true, the schedule must already have a prepaymentScheduleType, or the call returns 400: "Cannot append a prepaid step to schedule X: schedule has no prepaidScheduleType set..."; and prepaidValue must be a positive number, or the call returns 400: "prepaidEnabled=true requires a positive prepaidValue. Received: X."
  • Each overageBillingTerms[].usageBillingTermId must also appear in this same step’s billingTermIds, or the call returns 400: "Step N: overage's usageBillingTermId X must also appear in the step's billingTermIds."
  • The call returns 404 if the parent commitment isn’t found.

Steps are for adding sequential tiers without changing the schedule’s date range, for example: after the first $5k of usage this period, charge a different rate or BT for the next tier. If you need a new date range instead, use Add/Edit a Schedule.

2. Increase a commitment’s amount (edit a step’s commitmentValue)

PATCH /v3/contracts/:id/commitments/:commitmentId/steps/:stepId
{ "commitmentValue": 15000 }

The same call works whether the step is pre- or post-invoice; see the edit-vs-amendment explanation above. The response tells you which path the server took:

{ "success": true, "path": "edit" }

or

{ "success": true, "path": "amendment" }

Caveats:

  • commitmentValue here is the new absolute value, not an amount to add: sending 15000 sets the step’s value to 15000 regardless of what it was before. See above.
  • If the step has a finalized invoice and you also try to change prepaidEnabled or pricePriority in the same call, the API rejects the whole call, returning 400: "cannot update prepaidEnabled or pricePriority once the step is locked by a finalized invoice..." Split the commitmentValue change into its own call.
  • The call returns 404: "Step X not found.", or 400: "Step X does not belong to commitment Y." if the stepId and commitmentId pair is wrong.
  • If a commitment spans multiple steps or schedules, decide which one you actually mean to bump: there’s no “increase the commitment’s total” shortcut, so edit the specific step whose tier should grow. If you need to add net-new capacity as a distinct tier rather than grow an existing one, add a step instead (Example 1).

3. Edit the prepaid amount

PATCH /v3/contracts/:id/commitments/:commitmentId/steps/:stepId
{ "prepaidValue": 6000 }

Or toggle prepaid on or off together with a value:

{ "prepaidEnabled": true, "prepaidValue": 6000 }

Caveats:

  • Enabling prepaid on a step whose schedule has no prepaymentScheduleType fails, returning 400: "Cannot enable prepaid on step X: schedule Y has no prepaidScheduleType set. Set the schedule's prepayment behavior (FULL_UPFRONT / PER_COMMITMENT_PERIOD / SPLIT_ACROSS_BILLING_PERIODS) before enabling prepaid."
  • prepaidEnabled: true requires a resolved positive prepaidValue (from this call or the step’s existing value if you omit it), or the call returns 400: "prepaidEnabled=true requires a positive prepaidValue. Resolved value (from dto or existing step state): X."
  • prepaidEnabled: false is inconsistent with a positive prepaidValue, returning 400: "prepaidEnabled=false is inconsistent with prepaidValue=X. Omit prepaidValue or set it to 0 when prepaid is disabled."
  • Once the step has a finalized invoice, prepaidEnabled itself becomes immutable (see above); only prepaidValue can still change, and it changes through the amendment ledger rather than in place.

Common errors and troubleshooting

ErrorCauseFix
404 "Step X not found."Wrong stepIdRe-fetch through GET /v3/commitments/:commitmentId to confirm current step IDs
400 "Step X does not belong to commitment Y."stepId and commitmentId pair mismatchedConfirm both IDs belong to the same commitment
400 "cannot update prepaidEnabled or pricePriority once the step is locked by a finalized invoice..."Tried to change a locked field post-invoiceOnly commitmentValue and prepaidValue remain changeable post-invoice; split the call
400 "...requires a positive prepaidValue..."Enabled prepaid without a positive valueAlways send prepaidValue > 0 alongside prepaidEnabled: true
400 "...is inconsistent with prepaidValue=X..."Disabled prepaid but left a positive prepaidValueOmit prepaidValue or set it to 0 when disabling prepaid
400 "Cannot enable prepaid on step X: schedule Y has no prepaidScheduleType set..."Schedule has no prepay strategyPATCH the schedule’s prepaymentScheduleType first (see Add/Edit a Schedule), then enable prepaid on the step
400 "commitmentUnitType mismatch on schedule X..."New step’s unit type doesn’t match siblingsMatch the schedule’s existing unit type, or create a new schedule instead
400 "Step X is not the last step in its schedule..."Tried to delete a non-last stepOnly the last (highest-sequence) step in a schedule can be deleted
400 "Step X is the only alive step in schedule Y and cannot be deleted..."Tried to delete the only step in a scheduleDelete the commitment or schedule instead, or leave at least one step

Error codes referenced in this guide

  • 200: step patched or deleted successfully ({ success: true, path: "edit" | "amendment" } for PATCH)
  • 201: step appended successfully ({ stepId, sequence })
  • 400: validation error: shape, invariant (unit-type or event-type mismatch), or invoice-lock on prepaidEnabled and pricePriority
  • 404: step or commitment not found for this manufacturer or contract
  • 500: internal server error

Get the IDs you need

IDWhere to get it
contractIdGET /v3/contracts or GET /v3/contracts/:id
commitmentIdGET /v3/commitments (list) or the create response from Create Commitments
scheduleId (needed when appending a new step)GET /v3/commitments/:commitmentId: schedules[].id
stepIdGET /v3/commitments/:commitmentId: schedules[].steps[].id; also returned as stepId in the response of POST .../steps when you append one
billingTermIdGET /v3/contracts/:id/billing-terms or GET /v3/billing-terms

Next steps