> 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 Schedule

## Why edit a schedule, what's editable, and what locks it down

A **schedule** (`UsageCommitmentSchedule`) is a calendar-time phase of a commitment: its own `startDate` and `endDate`, `commitmentInterval` (billing cadence), and `prepaymentScheduleType`. Edit a schedule (rather than a step) when the change concerns *when* or *how often* billing happens, or which billing terms are eligible inputs to the steps in that phase, not the tier amounts themselves.

### What's editable on a schedule

Send a partial update to `PATCH .../schedules/:scheduleId`. All fields are optional; send only what's changing.

* `endDate`: new end date (inclusive). **Only valid on the LAST schedule** of the commitment (changing an earlier schedule's end would break contiguity with the one after it).
* `startDateChange`: `{ mode: "extend" | "shift", startDate }`. Only valid on the **FIRST schedule** of the commitment.
  * `extend` mutates only that schedule's `startDate` (move it earlier, or shorten it up to, but not past, the first sent invoice's period start).
  * `shift` translates **every** schedule and every commitment period on the whole commitment by the delta between the current first-schedule `startDate` and the new one. This is blocked entirely if *any* non-voided invoice exists anywhere on the commitment.
* `commitmentInterval`: new billing cadence for this schedule. The API rejects this if any period on this schedule already has a non-voided invoice, or if the schedule has 2+ steps (multi-step schedules must stay full-duration).
* `prepaymentScheduleType`: new prepay behavior. The API rejects this if any period on this schedule already has a non-voided invoice.
* `billingTermsToAdd` and `billingTermsToDelete`: per-step base-BT link changes (see [Example 3](#3-add-billing-terms-to-steps-within-a-schedule)).
* `cascadeBillingTerms` (boolean, **required** on every PATCH): whether linked base BTs (and their performance obligations) should move or resize along with the schedule's date change. `true` moves the BT dates with the schedule; `false` leaves the schedule to mutate alone, and you own BT realignment separately.
  * On `extend` (and on plain `endDate` changes), the cascade is scoped to the target schedule's own linked BTs.
  * On `shift`, the cascade is **commitment-wide**: the API re-syncs every base BT linked to *any* schedule on the commitment (`sync-base-bt-and-pob-dates`), plus every container BT (prepaid, true-up, overage) through `sync-container-billing-terms`, not just the BTs on the first schedule. This matches shift's own semantics, since shift translates every schedule, not just the first. See [Example 2](#2-extend-or-shift-a-schedule).

### What locks a schedule down

* **Any non-voided invoice on a period in this schedule** blocks `commitmentInterval` changes, `prepaymentScheduleType` changes, and shrinking `endDate` below the latest invoiced period's end. It does **not** block extending `endDate` forward, or extending `startDate` earlier, as long as it doesn't cross an invoiced period.
* **`shift`-mode `startDateChange` is blocked entirely** if any non-voided invoice exists *anywhere* on the commitment (not just this schedule): shift moves every period on every schedule, so one invoiced period anywhere makes the whole operation unsafe.
* **Deleting a schedule** requires all of the following: it's the last (highest-sequence) schedule, it's not the *only* schedule on the commitment, and none of its commitment periods have a non-voided invoice.
* **Date and cadence changes must align to a billing-period boundary** of every linked base BT's cadence (when `dueIntervalUnit` isn't `NONE`); an unaligned `endDate` would leave the last billing period partial.

## When to add a new step versus a new schedule

Ask: is the date range (and cadence or prepay behavior) changing, or just the amount or BT mix within the current range?

* **New schedule**: the commitment is entering a new calendar phase: a new interval, a new prepay strategy, or simply the next contiguous period starting now. This is the "step-up commitment" pattern. New schedules are always appended contiguously: the new `startDate` must be exactly the day after the current last schedule's `endDate`.
* **New step**: the amount tiers *within* the current schedule's existing date range, without touching dates or cadence. This is the "sequential commitment" pattern; see [Add/Edit a Step](/bill-customers/create-commitments/add-edit-step).

The hard constraint that enforces this split: **all steps within one schedule must share the same `commitmentUnitType` and resolve to the same event-type set.** If you need to change either of those, adding a step won't work; you need a new schedule.

## Examples

### 1. Add a schedule (start a new phase)

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

The API checks ownership of the commitment (under that contract, for that manufacturer) before the call proceeds; mismatches return `404`.

```json
{
  "startDate": "2025-07-01",
  "endDate": "2025-12-31",
  "commitmentInterval": "MONTHLY",
  "prepaymentScheduleType": "PER_COMMITMENT_PERIOD",
  "steps": [
    {
      "sequence": 0,
      "commitmentValue": 5000,
      "commitmentUnitType": "DOLLARS",
      "pricePriority": "LOW",
      "prepaidEnabled": false,
      "billingTermIds": ["550e8400-e29b-41d4-a716-446655440001"]
    }
  ]
}
```

Required: `startDate`, `endDate` (both `YYYY-MM-DD`, `endDate` inclusive; `endDate` must be after `startDate`), `steps[]` (min 1, each with caller-supplied `sequence`, strictly increasing). The schedule's own `sequence` is **server-assigned** here (unlike at commitment creation); the response returns it along with the new `scheduleId` and `stepIds[]`.

**Caveats:**

* Contiguity: `startDate` must be exactly the day after the current last schedule's `endDate`. The API enforces this structurally, not as a hand-checkable string match, so send the day immediately following.
* `commitmentInterval` cannot be set if `steps.length >= 2`; the call returns `400`: `"Multi-step schedules must be full-duration: omit commitmentInterval (interval/unit must be null)."`
* Any step with `prepaidEnabled: true` requires `prepaymentScheduleType` to be set on the schedule; otherwise the call returns `400`: `"prepaidEnabled steps require the schedule to carry a prepaymentScheduleType (FULL_UPFRONT / PER_COMMITMENT_PERIOD / SPLIT_ACROSS_BILLING_PERIODS)."`
* `endDate` must align to every linked BT's billing-period boundary, or the call returns `400`: `"Schedule endDate (X) does not align with the billing-period boundaries of billing term Y..."`; if `commitmentInterval` is set, it must also align to the schedule's own cadence (`"...does not align with the schedule's own {unit} cadence anchored at {startDate}..."`).
* The call returns `404` if the commitment isn't found (or doesn't belong to that contract or manufacturer).

Think of "add a schedule" as starting a new phase of this commitment. This call purely appends; existing schedules and steps stay untouched.

### 2. Extend or shift a schedule

```javascript
PATCH /v3/contracts/:id/commitments/:commitmentId/schedules/:scheduleId
```

**Extend the end date (last schedule only):**

```json
{ "endDate": "2026-06-30", "cascadeBillingTerms": true }
```

* `endDate` only applies to the **last** schedule; any other schedule returns `400`: `"Schedule X (sequence Y) is not the last schedule on commitment Z (max W); endDate changes are only allowed on the last schedule to preserve contiguity."`
* Extending forward is unbounded; shrinking is bounded below by the latest invoiced period's end, or the call returns `400`: `"Cannot shrink endDate to X: schedule Y has an invoiced period ending Z; new endDate must be ≥ the latest invoiced period end."`
* `cascadeBillingTerms: true` extends or shortens the schedule's linked base BTs (and their POBs) to match; `false` leaves BTs untouched, and you own realignment.

**Extend the start date earlier (first schedule only):**

```json
{ "startDateChange": { "mode": "extend", "startDate": "2024-10-01" }, "cascadeBillingTerms": false }
```

* This is only valid when the URL `scheduleId` is the **first** schedule; otherwise the call returns `400`: `"Schedule X (sequence Y) is not the first schedule on commitment Z (min W); startDate changes are only allowed on..."`
* The call is blocked if it would cross a period that already has a non-voided invoice, returning `400`: `"Cannot extend startDate to X: schedule Y has an invoiced period starting Z; new startDate must be ≤ ..."`
* With `cascadeBillingTerms: false`, the new `startDate` must still align with linked base BTs' billing-period boundaries (the BT grid stays put), or the call returns `400`: `"New startDate (X) does not align with billing-period boundaries of billing term Y..."` With `cascadeBillingTerms: true`, the BT grid re-anchors to the new start, so the API doesn't enforce this alignment against the old anchor.

**Shift the whole commitment's timeline:**

```json
{ "startDateChange": { "mode": "shift", "startDate": "2025-02-01" }, "cascadeBillingTerms": true }
```

* This translates every schedule and every commitment period on the commitment by the delta between the old and new first-schedule `startDate`. It is **blocked entirely** if any non-voided invoice exists anywhere on the commitment, returning `400`: `"Cannot shift commitment X: at least one non-voided invoice has been sent. Shift translates every schedule and period by the delta, so no invoiced period can be..."`
* **`cascadeBillingTerms` on shift is commitment-wide, not schedule-local.** With `cascadeBillingTerms: true`, the API re-syncs every base BT linked to *any* schedule on the commitment (not just the schedule in the URL) to its schedule's new dates, through `sync-base-bt-and-pob-dates`, and resizes every container BT (prepaid, true-up, overage, on both edges) plus every attached POB (on both service edges) through `sync-container-billing-terms` for the whole commitment. This is deliberate: shift moves every schedule's dates, so a schedule-scoped cascade would leave every other schedule's BTs behind.
* With `cascadeBillingTerms: false`, the API leaves BTs exactly where they are while every schedule and period on the commitment moves underneath them. You're responsible for realigning every linked BT afterward, not just one schedule's worth.

**Composition:** you can send `startDateChange` and `endDate` in the same PATCH only when the commitment has a single schedule (so "first" and "last" are the same row); in that case the API applies `startDateChange` first, then `endDate` wins as the final absolute end. `commitmentInterval` and `prepaymentScheduleType` target the URL `scheduleId` directly and compose freely with `startDateChange` on any schedule.

### 3. Add billing terms to steps within a schedule

This is a **schedule-level operation**, not a step-level one, even though it changes which BTs a specific step is linked to. The reason: **all steps in a schedule must share the same event-type set.** Adding a BT to one step directly could silently break that invariant for its siblings, so the schedule endpoint enforces it across every step you touch in one call, all at once.

```javascript
PATCH /v3/contracts/:id/commitments/:commitmentId/schedules/:scheduleId
```

```json
{
  "cascadeBillingTerms": false,
  "billingTermsToAdd": [
    { "550e8400-e29b-41d4-a716-446655440010": ["550e8400-e29b-41d4-a716-446655440020"] }
  ],
  "billingTermsToDelete": [
    { "550e8400-e29b-41d4-a716-446655440010": ["550e8400-e29b-41d4-a716-446655440021"] }
  ]
}
```

Shape: each element of `billingTermsToAdd` and `billingTermsToDelete` is an object `{ [stepId]: billingTermId[] }`; you can send multiple step keys per call, and they get merged. Adding a BT that's already linked to the step is an idempotent no-op.

**Caveats:**

* Every `stepId` referenced must belong to the schedule in the URL, or the call returns `400`: `"Step X does not belong to schedule Y."`
* Only **base** BT links (`billingTermCommitmentType: null`) can be added or removed this way; container (prepaid and true-up) and overage BTs are managed separately.
* The same BT id cannot appear in both `billingTermsToAdd` and `billingTermsToDelete` for the same step in one call, or the call returns `400`: `"Billing term(s) X appear in both billingTermsToAdd and billingTermsToDelete for step Y. Split into separate requests if you need both."`
* The resulting event-type set across all steps in the schedule must still match; the API returns a `400` if the add/delete combination would make one step's event types diverge from its siblings.
* `billingTermsToAdd` entries must align with the schedule's `endDate` and BT cadence the same way new-schedule BTs do.

## Common errors and troubleshooting

| Error                                                                                                                          | Cause                                                                       | Fix                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `400 "...is not the last schedule..."`                                                                                         | Tried to change `endDate` on a non-last schedule                            | Only the last schedule can have its `endDate` changed; add a new schedule instead if you need to insert a new phase               |
| `400 "...is not the first schedule..."`                                                                                        | Tried `startDateChange` on a non-first schedule                             | Only the first schedule can have its `startDate` changed                                                                          |
| `400 "Cannot shift commitment..."`                                                                                             | `shift` mode attempted with a non-voided invoice anywhere on the commitment | Use `extend` instead (schedule-local, and only blocked by invoices in the affected date range), or accept that history can't move |
| `400 "Cannot shrink endDate..."` or `"Cannot extend startDate..."`                                                             | Date change would cross an already-invoiced period                          | Pick a date at or beyond the latest or earliest invoiced period boundary                                                          |
| `400 "...does not align with billing-period boundaries..."` or `"...does not align with the schedule's own {unit} cadence..."` | New date doesn't land on a BT or schedule cadence boundary                  | Pick a date on a period boundary (for example, month-end for a monthly BT)                                                        |
| `400 "Multi-step schedules must be full-duration..."`                                                                          | Tried to set `commitmentInterval` on a schedule with 2+ steps               | Omit `commitmentInterval` for multi-step schedules, or reduce to one step                                                         |
| `400 "...Billing term(s)...appear in both billingTermsToAdd and billingTermsToDelete..."`                                      | Same BT and step pair in both arrays in one call                            | Split into two sequential PATCH calls                                                                                             |
| `400 "Schedule X does not belong to commitment Y"` (404 through `NotFoundException`)                                           | Wrong `scheduleId`, or it belongs to a different commitment                 | Re-fetch through `GET /v3/commitments/:commitmentId` to confirm the schedule's ID and parent commitment                           |
| `400 "Cannot delete schedule..."` variants                                                                                     | Not the last schedule, only remaining schedule, or has an invoiced period   | Only the last, non-sole, non-invoiced schedule can be deleted                                                                     |

## Error codes referenced in this guide

* `200`: schedule patched or deleted successfully
* `201`: schedule appended successfully
* `400`: validation error: structural (contiguity, sequencing), invoice-gate lock, alignment, or event-type-set mismatch
* `404`: commitment or schedule not found for this manufacturer or contract (includes cross-tenant mismatches, and concurrent-delete races)
* `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`                                                  | `GET /v3/commitments/:commitmentId` (or `GET /v3/contracts/:id/commitments/:commitmentId`): the response's `schedules[]` array includes each schedule's `id` and `sequence`; also returned in the response of `POST .../schedules` when you append one |
| `stepId` (for `billingTermsToAdd` and `billingTermsToDelete`) | Same `GET /v3/commitments/:commitmentId` call: each `schedules[].steps[]` entry includes its `id`                                                                                                                                                      |
| `billingTermId`                                               | `GET /v3/contracts/:id/billing-terms` or `GET /v3/billing-terms`                                                                                                                                                                                       |

## Next steps

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