Amend a scheduled contract's instalments
Rewrites the instalment schedule of a scheduled contract, or converts a fixed-occurrence recurring contract into one. The caller enumerates every charge that still has money on it with a disposition: keep (reference only), amend (new amount, due date or legal-entity split), or delete, plus any new charges. Only a change to the money collected creates a replacement contract; a date-only change, or any change with modify_in_place, is applied to the contract itself.
Where you can do this
- Merchant View: Edit a contract (Save)
- Merchant View: Edit a contract (Convert to Scheduled)
- API: Update a contract (
PUT /contracts/{contract_id}) — body carries a charges array of dispositions: id or alt_key only (keep), amount and/or due (amend), deleted (delete), or no reference (new); optional modify_in_place
Before you start
- The contract has a charge schedule (
charges[]), or is a recurring fixed-amount contract (which the edit converts to a charge schedule). One-off contracts are refused. - The contract is in progress: status ACTIVE, PENDING, FAILING, UNRESOLVED, PENDING (unconfirmed) or ERROR. Cancelling, suspended, cancelled, completed and failed contracts cannot take a schedule edit.
- Every existing charge with an outstanding balance is listed with a disposition, except charges that are already cancelling or in ERROR, which may be omitted.
- The contract is not being edited or collected by another process at the same moment (the edit takes the contract's lock).
Inputs
| Input | Required | Meaning and constraints |
|---|---|---|
| charges[] | yes | Non-empty array. Each entry is one disposition: keep {id} or {alt_key}; amend {id or alt_key, amount and/or due, legal_entities}; delete {id or alt_key, deleted: true}; new {amount, due, alt_key, legal_entities} with no id or matching alt_key. delete: true is accepted as a synonym of deleted. |
| charges[].id | no | Charge id (ch_ prefix accepted). Resolves the existing charge first; must be on this contract. |
| charges[].alt_key | no | The charge's reporting code. Resolves an existing charge when no id is given (or the id does not match); must be unique within the request. Stamped onto new charges. |
| charges[].amount | new: yes | Greater than zero. On an existing charge, not below what is already paid plus written off. Omit to keep the current amount. |
| charges[].due | new: yes | ISO date-time or the literal now, which is replaced with the current time. Not more than 24 hours in the past; the platform behind the API allows 7 days, so the tighter limit is the one that applies; a date on the same calendar day as the charge's current due date counts as unchanged. |
| modify_in_place | no | true applies the schedule to this contract even when the money changes. Without it, a money change creates a replacement contract. |
| amount | no | Replacement mode only: the new contract's total; must equal the amounts of the new and carried-over charges plus any outstanding that is brought forward. |
| description, alt_key, metadata, event_date, payment_method, next_charge, next_payment, keep_active_until, suppress_checks | no | May accompany the schedule and are applied to the contract, or to the replacement, as for Amend a contract's terms. |
| occurrences, frequency | no | Cannot accompany the schedule when modify_in_place is set; see Amend a contract's terms for the rules without it. |
Rules
Checked by the API means Shuttle refuses the request however it is sent; a screen name means only that screen refuses it, and a direct API call would be accepted.
| Rule | Checked by | What you see |
|---|---|---|
| A one-off due-now contract cannot be updated | The API | VALIDATION_ERROR: "cannot update non-recurring contracts" |
A scheduled single payment (a one-off with a scheduled_date) has no schedule to edit: it is one charge with a due date, changed through amount and scheduled_date (Amend a contract's terms). A charges array sent for one is not forwarded to Shuttle | The API | no error; the single charge is re-saved from amount and scheduled_date |
charges must be an array and must not be empty | The API | VALIDATION_ERROR: "charges must be an array" / INVALID: "Invalid value for field scheduledCharges (cannot be empty)." |
A charge schedule cannot be sent alongside a resume (a cancelled or cancelling contract with resume, or a suspended contract with next_charge or status ACTIVE) | The API | VALIDATION_ERROR: "Cannot change charges on resume" |
| The contract must have a charge schedule or be a recurring fixed-amount contract | The API | INVALID: "Invalid value for field contract (type … does not support scheduled charge edits)." |
| The contract must be in progress and not cancelling; suspended, cancelled, completed and failed contracts are refused | The API | INVALID: "Invalid value for field contract (status PAYMENTCANCELLING is not editable)." or "… (status PAYMENTSUSPENDED is not editable)." and so on |
| Every existing charge with an outstanding balance must be referenced by id or alt_key, unless it is paid, cancelling or in ERROR | The API | INVALID: "Invalid value for field scheduledCharges (missing required charge |
| A delete entry must reference an existing charge and carry no amount, due or legal_entities | The API | VALIDATION_ERROR: "id or alt_key required for deleted charge" / INVALID: "… scheduledCharges[].delete (delete requires id or matching code referencing an existing charge)." or "…delete (cannot combine delete with amount/whenDue/legalEntities changes)." |
| An id that is not a charge on this contract is refused | The API | INVALID: "Invalid value for field scheduledCharges[ |
| alt_key must be unique within the request | The API | INVALID: "Invalid value for field scheduledCharges (duplicate code '' in request — code must be unique within a contract)." |
| A new charge whose alt_key matches any charge already on the contract, including cancelled and written-off ones, is resolved as an edit of that charge rather than refused (and is then rejected only if that charge is immutable) | The API resolves; Merchant View screen refuses | no server error unless the matched charge is immutable; Merchant View blocks Save ("Must be unique or combine charges") and suggests unused keys |
| A new charge needs an amount above zero and a due date | The API | VALIDATION_ERROR: "amount required for new charge and must be greater than zero" / "due date required for new charge"; INVALID: "… scheduledCharges[new].amount (required for new charges, must be greater than zero)." / "…[new].whenDue (required for new charges)." |
| Any amount sent must be above zero and must parse; any due date must parse | The API | VALIDATION_ERROR: "charge amount must be greater than zero" / "charge due date is invalid"; INVALID: "… scheduledCharges[].amount (must be greater than zero)." |
| A paid (COMPLETED, CANCELLED, FAILED), in-flight (PENDING, UNRESOLVED, UNCONFIRMED) or locked charge cannot be deleted, re-amounted or moved to another day; the whole edit is rejected, not just that row. Merchant View renders these rows read-only and sends them as keep | The API; Merchant View screen mirrors all but locked | INVALID: "Invalid value for field scheduledCharges[ |
| Locked charges are derived server-side: the earliest active or failing charges, by due date, whose cumulative amounts cover the total of in-flight charges. A client cannot predict them | The API | the rejection above; Merchant View appends Shuttle's message to its save-error toast |
| An amended amount cannot drop below what is already paid plus written off on that charge | The API | INVALID: "Invalid value for field scheduledCharges[].amount (cannot be less than already settled amount |
| In replacement mode, a kept or amended existing charge with no recorded amount or due date must be given one | The API | INVALID: "… scheduledCharges[ |
With modify_in_place and charges, occurrences and frequency cannot also be sent | The API | INVALID: "Invalid value for field numberOfPayments (not applicable with modifyInPlace and scheduledCharges)." / "…frequency (not applicable with modifyInPlace and scheduledCharges)." |
Without modify_in_place, the edit creates a replacement contract only when money moves: an existing charge's amount changes, a new charge carries an amount, or a charge still holding money is deleted or omitted. Date-only changes, and keep entries, are applied in place | The API | no error; the response carries the replacement contract when one was created |
| The contract is locked for the edit; if another process holds it for longer than 30 seconds the edit fails | The API | INVALID with Shuttle's lock code BOLT-7059 and no message text |
| A replacement that fails after the new contract exists is rolled back: the new contract is cancelled and the old one restored | The API | INVALID: "(BOLT-7004) Internal Error: Failed to create contract" |
| Merchant View locks the whole editor while the contract status is PENDING ("Schedule editing is locked because a payment is currently in progress."); the server permits the edit and only protects the in-flight and locked charges. By design: the risk is an operator acting on what the screen showed a moment ago rather than what is true now, while a payment is moving. | Merchant View screen only | controls disabled |
Merchant View only offers the in-place choice when the total has changed, and only with the in-place option enabled; otherwise the amendment always creates a replacement. The server accepts modify_in_place on any schedule edit. By design: the screen is guarding against a human mistake. Through the API you may send it on any schedule edit. | Merchant View screen only | checkbox hidden or disabled |
| Merchant View offers Convert to Scheduled only on an in-progress known-amount contract with a finite number of occurrences and no unsaved changes; the server accepts a schedule on any in-progress recurring fixed-amount contract. By design: the screen needs a finite number of occurrences to draw the converter at all. | Not the API; Merchant View screen refuses | control disabled, "Cannot convert unlimited occurrences to scheduled charges" |
| Merchant View blocks two editable rows with the identical due time and warns that past-due rows will collect on save; neither is a server rule. By design: the screen's own split-charge flow can produce two rows at the same time, and a due date in the past is usually an error, so neither is allowed to be saved by accident. The API applies neither rule. | Merchant View screen only | Save disabled / warning "Saving will trigger N overdue charges immediately." |
What happens
- A date-only change, or any change sent with
modify_in_place, is applied to the contract itself: deleted or omitted charges become CANCELLED, amended charges take their new amount and due date, new charges are added ACTIVE, and the contract's totals and next charge date follow the new schedule. - A change to the money collected without
modify_in_place(an amount changed, a charge added, or a charge still holding money deleted or omitted) replaces the contract: a new contract with its own id carries the kept, amended and new charges, in-flight charges move across keeping their ids, and the old contract ends CANCELLED with its remaining unpaid charges written off. - The response carries the contract that now holds the schedule: the replacement when one was created, otherwise the amended contract.
- Webhooks: in place,
CHARGE.UPDATEper cancelled or amended charge,CHARGE.CREATEDper new charge andCONTRACT.UPDATE. On replacement,CHARGE.WRITTENOFFper written-off charge andCONTRACT.COMPLETEfor the old contract, thenCHARGE.CREATEDper charge andCONTRACT.STARTfor the new one. - A charge whose due date is now or already past is collected straight after save; otherwise collection resumes at the earliest unpaid charge's due date less the payment method's advance notice, and a decline follows the client's retry schedule.
- A contract left with nothing outstanding completes.
Who can do this
- Roles: Admin only today; intended for Support as well, and being aligned.
- Permission keys (for clients managing permissions directly): the session must hold
agreement,payment_contractor*on the client or the contract's division, checked by Shuttle for the contract's account; a session bound to an account can only edit that account's contracts. Applies to every entry point. - Admin app: the edit screen requires
payment_contract; it is reached from the contract view's Edit control, offered only on an in-progress recurring or scheduled contract that has not been superseded. The in-place checkbox is disabled without the optionrecurring_edit_in_place, so the amendment always creates a replacement;recurring_editgates the edit screen as for Amend a contract's terms.
Related
Updated 33 minutes ago
Did this page help you?