Add/Edit a Schedule

View as Markdown

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).
  • 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.

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.

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)

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.

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

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

Extend the end date (last schedule only):

{ "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):

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

{ "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.

PATCH /v3/contracts/:id/commitments/:commitmentId/schedules/:scheduleId
{
"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

ErrorCauseFix
400 "...is not the last schedule..."Tried to change endDate on a non-last scheduleOnly 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 scheduleOnly the first schedule can have its startDate changed
400 "Cannot shift commitment..."shift mode attempted with a non-voided invoice anywhere on the commitmentUse 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 periodPick 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 boundaryPick 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+ stepsOmit 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 callSplit 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 commitmentRe-fetch through GET /v3/commitments/:commitmentId to confirm the schedule’s ID and parent commitment
400 "Cannot delete schedule..." variantsNot the last schedule, only remaining schedule, or has an invoiced periodOnly 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

IDWhere to get it
contractIdGET /v3/contracts or GET /v3/contracts/:id
commitmentIdGET /v3/commitments (list) or the create response from Create Commitments
scheduleIdGET /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
billingTermIdGET /v3/contracts/:id/billing-terms or GET /v3/billing-terms

Next steps