> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.tabs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.tabs.com/_mcp/server.

# Add/Edit a Step

## 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](/bill-customers/create-commitments/add-edit-schedule#3-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](#2-increase-a-commitments-amount-edit-a-steps-commitmentvalue)) 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](/bill-customers/create-commitments/add-edit-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)

```javascript
POST /v3/contracts/:id/commitments/:commitmentId/steps
```

```json
{
  "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](/bill-customers/create-commitments/add-edit-schedule).

### 2. Increase a commitment's amount (edit a step's `commitmentValue`)

```javascript
PATCH /v3/contracts/:id/commitments/:commitmentId/steps/:stepId
```

```json
{ "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:

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

or

```json
{ "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](#1-add-a-step-new-sequential-tier)).

### 3. Edit the prepaid amount

```javascript
PATCH /v3/contracts/:id/commitments/:commitmentId/steps/:stepId
```

```json
{ "prepaidValue": 6000 }
```

Or toggle prepaid on or off together with a value:

```json
{ "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

| Error                                                                                                   | Cause                                               | Fix                                                                                                                                                                    |
| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404 "Step X not found."`                                                                               | Wrong `stepId`                                      | Re-fetch through `GET /v3/commitments/:commitmentId` to confirm current step IDs                                                                                       |
| `400 "Step X does not belong to commitment Y."`                                                         | `stepId` and `commitmentId` pair mismatched         | Confirm 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-invoice         | Only `commitmentValue` and `prepaidValue` remain changeable post-invoice; split the call                                                                               |
| `400 "...requires a positive prepaidValue..."`                                                          | Enabled prepaid without a positive value            | Always send `prepaidValue > 0` alongside `prepaidEnabled: true`                                                                                                        |
| `400 "...is inconsistent with prepaidValue=X..."`                                                       | Disabled prepaid but left a positive `prepaidValue` | Omit `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 strategy                     | PATCH the schedule's `prepaymentScheduleType` first (see [Add/Edit a Schedule](/bill-customers/create-commitments/add-edit-schedule)), then enable prepaid on the step |
| `400 "commitmentUnitType mismatch on schedule X..."`                                                    | New step's unit type doesn't match siblings         | Match 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 step                     | Only 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 schedule         | Delete 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

| ID                                              | Where to get it                                                                                                                                  |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `contractId`                                    | `GET /v3/contracts` or `GET /v3/contracts/:id`                                                                                                   |
| `commitmentId`                                  | `GET /v3/commitments` (list) or the create response from [Create Commitments](/bill-customers/create-commitments)                                |
| `scheduleId` (needed when appending a new step) | `GET /v3/commitments/:commitmentId`: `schedules[].id`                                                                                            |
| `stepId`                                        | `GET /v3/commitments/:commitmentId`: `schedules[].steps[].id`; also returned as `stepId` in the response of `POST .../steps` when you append one |
| `billingTermId`                                 | `GET /v3/contracts/:id/billing-terms` or `GET /v3/billing-terms`                                                                                 |

## Next steps

* [Create Commitments](/bill-customers/create-commitments)
* [Add/Edit a Schedule](/bill-customers/create-commitments/add-edit-schedule)