Migrate Payment Plans with an Agent
A migration is two exports: the customers, then the schedules that bill them. Migrate customers with an agent covers the first; this page is the second. The payment-plan import is the same envelope — stage, freeze, merchant approves, poll — applied to recurring plans, with two things that make plans different: every row has to name a customer who already exists, and every plan it creates is a draft that bills nothing until a payment method arrives.
Sandbox-only today, and for a sandbox OAuth connection only, on the same grounds as the customer import. Row fields, the projection, and what is shared with the customer import are in the tool reference.
Before you begin
Import the customers first, and carry your source system’s customer id as
each customer’s reference. It is a
candidate key
— unique per account, matched without regard to case or surrounding
whitespace — and it is how plan rows name their customer:
customer_reference on a plan row is reference on the customer. The two
exports join on the id they already share, and you never need to look up
the cus_… ids of customers you just created.
Nothing binds a plan import to a customer import. Resolution happens by
reference at finalisation, so it does not matter whether a customer was
imported this morning, created through the API last year, or keyed in by
hand — only that a customer with that reference exists on the account when
you finalise. A customer created without a reference can only be named by
customer_id.
The shape of it
agent merchant (dashboard)───── ────────────────────begin_payment_plan_importappend_payment_plan_import_rows × Nfinalize_payment_plan_import ──manifest──▶ Payment plan imports → Approve / Rejectget_payment_plan_import (poll) ◀──────── (Stop while running) ◀──────── (Roll back afterwards)As for customers: approval is not an API call, the agent’s last write is
finalize_payment_plan_import, and from there it can only poll or withdraw.
1. Begin
Declare the exact number of plan rows and where they came from.
declared_count is exact — count the source before you begin.
{ "import_key": "billing-export-2026-09-24", "source_reference": "Q3 billing export (subscriptions.csv)", "declared_count": 1180, "field_mapping": { "source_row_key": "subscription_id", "customer_reference": "customer_id", "name": "plan_name", "frequency_type": "interval_unit", "frequency_period": "interval_count", "start_date": "next_billing_date", "recurring_amount_cents": "amount_minor" }}There is no customer-import reference to pass. field_mapping is display
provenance for the merchant — here it records that the source’s
customer_id column became customer_reference — and transforms nothing.
{ "payment_plan_import": "pim_4Qw8Rt2YuIoP6AsDfGhJkLzX", "status": "staging", "declared_count": 1180, "limits": { "max_rows_per_chunk": 500, "max_declared_count": 10000 }, "staging_expires_at": "2026-10-01T09:00:00Z", "replayed": false}2. Stage rows in chunks
Up to 500 rows per call, each chunk under its own chunk_key. Every row
names its customer in exactly one way and ends in exactly one way;
the row fields are the whole
surface.
{ "payment_plan_import": "pim_4Qw8Rt2YuIoP6AsDfGhJkLzX", "chunk_key": "chunk-000001", "rows": [ { "source_row_key": "sub_10021", "customer_reference": "crm-001201", "name": "Gym membership", "category": "Membership", "frequency_type": "M", "frequency_period": 1, "start_date": "2026-10-01", "recurring_amount_cents": 4900, "until_further_notice": true }, { "source_row_key": "sub_10022", "customer_id": "cus_7LmNp3RsTuVw", "name": "Term fees", "frequency_type": "W", "frequency_period": 2, "start_date": "2026-10-05", "first_date": "2026-09-28", "first_amount_cents": 15000, "recurring_amount_cents": 6000, "number_of_payments": 10, "failure_behaviour": "TRY_AGAIN" } ]}The first row names its customer by the merchant’s reference and runs
monthly until further notice. The second names an existing customer by
cus_… id, takes a one-off first payment of $150 on 28 September, then
$60 fortnightly for ten payments, retrying a failed collection rather than
adding it to the end. A third kind of end condition, total_amount_cents,
ends the plan once that total has been collected.
start_date is the recurring anchor — the first recurring collection
and the date every later one is counted from, not the date the plan
originally began on the old platform. With no first_date it is also the
next collection: for a migrated schedule, its next due date. When a row
carries first_date (a one-off collection on or before start_date,
which needs first_amount_cents), that is the first collection and
start_date is the first recurring one — as in the second row above,
where $150 is collected on 28 September and the fortnightly $60 starts on
5 October. Both dates must be today or later when you finalise, so stage
and finalise a dataset within the same day where you can; a date that
lapses in between becomes an invalid row (below), and the platform will not
move it for you.
The dates in these examples are real calendar dates and start expiring on 28 September 2026 — replace them with dates from today onward before you stage anything.
Two things are rejected at this step that the customer import never sees:
a row with both customer_id and customer_reference (or neither), and a
row with two end conditions (or none). Frequencies the plan model cannot
express — frequency_type: "M" with frequency_period above 12, or W
above 52 — are also rejected here. Duplicate source_row_keys, chunks past
declared_count, and the replay and correction rules for chunk_key are
as for customers.
3. Finalise and read the projection
Once staged_total equals declared_count:
{ "payment_plan_import": "pim_4Qw8Rt2YuIoP6AsDfGhJkLzX" }This is where each row’s customer is looked up and each start_date is
checked against today, alongside the plan model’s own validation:
{ "payment_plan_import": "pim_4Qw8Rt2YuIoP6AsDfGhJkLzX", "status": "requires_approval", "action": "aoa_2Bn5Vc8XzLkJ7HgFdSaQwErT", "dataset_hash": "sha256:89ab…4567", "row_count": 1180, "projection": { "creates": 1163, "creates_as": "draft", "invalid": 17, "unresolved_customers": 11, "lapsed_start_dates": 4, "customers_referenced": 1102, "fields_set": ["customer_reference", "customer_id", "name", "category", "frequency_type", "frequency_period", "start_date", "first_date", "first_amount_cents", "recurring_amount_cents", "number_of_payments", "until_further_notice", "failure_behaviour"], "errors_sample": [ { "source_row_key": "sub_10877", "errors": ["Customer crm-004410 was not found by reference"] }, { "source_row_key": "sub_10912", "errors": ["Start date 2026-09-22 has passed — supply the next due date"] } ] }, "decide_by": "2026-09-27T00:00:00Z", "replayed": false}Read it before you tell your human it is ready. creates is how many
plans approval will create, and creates_as is always "draft" — the
manifest itself records that no plan will bill on approval.
customers_referenced is how many distinct customers gain a plan. Two
counts break invalid down: unresolved_customers are rows whose customer
does not exist on the account under that customer_id or
customer_reference; lapsed_start_dates are rows whose start_date is
now in the past. Both are invalid, both stay in the frozen dataset for the
merchant to see, and both are skipped at execution.
Neither is something the platform will fix. If a customer is unresolved,
check its reference on the account (list_customers with the
reference filter) — it was most likely never set, or set differently. If
a date has lapsed, compute the next due date from the source schedule. Then
cancel_payment_plan_import and begin a new import with the corrected
rows: after finalisation the dataset is frozen, and any append answers
post_finalisation_mutation. A small invalid count you are content to
leave behind is also a reasonable choice — it is the merchant’s to make,
and the projection is how they make it.
4. The merchant approves — in the dashboard
Hand over the pim_… reference and the dataset_hash. Under Payment
plan imports for the account, the merchant sees the source, the connected
application, the fingerprint, the field sources, the deadline, and What
approving does: how many plans it creates, that they are created as
drafts, how many customers they attach to, and the unresolved and lapsed
counts with a sample of the invalid rows. Approve import confirms it;
Reject ends it. The deadline, expiry, and disconnection rules are
as for customers.
5. Poll
Execution starts on approval. Poll get_payment_plan_import until the
status is terminal; the body is the same state-shaped body as the customer
import’s, with progress while executing and outcome afterwards:
{ "status": "partially_completed", "progress": null, "outcome": { "created": 1163, "failed": 17, "failures_sample": [ { "source_row_key": "sub_10877", "errors": ["Customer crm-004410 was not found by reference"] } ] }, "rollback_action": null}created is the number of draft plans that now exist, each bound to its
resolved customer and to no payment instrument.
What “draft” means
A draft plan is a schedule waiting for a way to be paid. It generates no
payment intents and collects nothing, however far past its start_date
the calendar moves. It leaves draft by the paths that already exist,
none of which the import touches:
- The customer adds a payment method. When a payment method is collected for the customer — through a setup intent and the hosted widget, or the dashboard’s Collect Payment Details and recovery flows — their draft plans activate automatically. See plan states.
- The merchant activates it from the plan’s page in the dashboard, or
you do, with
activate_payment_planonce an instrument is on the customer.
So the migration’s last mile is activation, not the import: the book of
plans is in place as drafts. A plan activates automatically the moment a
new payment method arrives on its customer, and begins scheduling
according to its own dates — a start_date still in the future is
collected on that date, not immediately. A customer who already had a
payment method gets no such trigger: their imported plans stay draft
until you activate them explicitly (activate_payment_plan, or the
merchant in the dashboard).
What stop and rollback look like from the agent’s side
Both are merchant controls in the dashboard, seen by the agent only through
get_payment_plan_import. Stop behaves exactly as for customers: rows
not yet claimed never run, drafts already created remain, and the next poll
answers stopped.
Rollback is where plans differ. From a terminal-executed state the
merchant can roll the import back, and the compensating action closes and
archives the plans the import created — but only those still draft, with
no payment intents, and not edited since creation. A plan that has been
activated (by the customer’s instrument arriving or by the merchant), has
billed, or has been edited is skipped and reported to the merchant with its
reason. The next poll answers rolled_back with rollback_action naming
the compensating action. One rollback per import; agents can neither
initiate nor propose one.
A customer who now owns an import-created plan has a subsequent record, so a later rollback of the customer import skips that customer too. Roll back plans before customers if both need unwinding.
Withdrawing
cancel_payment_plan_import withdraws an import while it is staging or
requires_approval — a wrong export, an unresolved_customers count that
says the references were never set, a batch of lapsed dates. Once approved
it answers state_conflict.
{ "payment_plan_import": "pim_4Qw8Rt2YuIoP6AsDfGhJkLzX" }Things agents trip on
- Lapsed dates.
start_dateis checked at finalise, not at append. A dataset staged on Friday and finalised on Monday can lose its weekend rows tolapsed_start_dates. Re-date and re-stage in a new import; nothing re-dates for you. - References never set on the customers.
customer_referenceresolves againstCustomer.reference. If the customer import did not carry the source id asreference, every plan row that names it isunresolved_customers. Fix the customers (or name them bycustomer_id) rather than guessing. - Cadences past the ceiling.
frequency_typeisWorMandfrequency_periodat most 52 weeks or 12 months. Fortnightly isW×2, quarterlyM×3, annualM×12 — all fine. Anything longer (every 18 months, biennial) cannot be expressed: the row is rejected at append, never approximated, so do not stage one expecting the platform to find a near-enough cadence. - Two end conditions. Exactly one of
total_amount_cents,number_of_payments, oruntil_further_notice: true. A source that carries both a total and a count needs you to choose; the platform will not. - Totals over $999,999.99. A plan’s total can be at most $999,999.99.
A
total_amount_centsover99999999is rejected at append. Anumber_of_paymentswhose total comes to more is an invalid row at finalisation. first_datewithoutfirst_amount_cents. A one-off first payment needs its amount; the row is rejected without it.- Counting the first payment.
number_of_paymentscounts recurring payments only; a first payment is in addition, and the total isnumber_of_payments×recurring_amount_cents+first_amount_cents. A schedule of 11 collections whose first is a one-off $60 isfirst_date+first_amount_cents: 6000andnumber_of_payments: 10, not 11. - Expecting money to move. Nothing bills on approval. The plans are
drafts until each customer has a payment method, and the projection’s
creates_as: "draft"is the record of that. - Everything the customer import trips on — exact
declared_count, new keys for corrections, a frozen dataset, approval belonging to the merchant — applies unchanged.