Skip to content

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_import
append_payment_plan_import_rows × N
finalize_payment_plan_import ──manifest──▶ Payment plan imports → Approve / Reject
get_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.

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

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

finalize_payment_plan_import
{ "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_plan once 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.

cancel_payment_plan_import
{ "payment_plan_import": "pim_4Qw8Rt2YuIoP6AsDfGhJkLzX" }

Things agents trip on

  • Lapsed dates. start_date is checked at finalise, not at append. A dataset staged on Friday and finalised on Monday can lose its weekend rows to lapsed_start_dates. Re-date and re-stage in a new import; nothing re-dates for you.
  • References never set on the customers. customer_reference resolves against Customer.reference. If the customer import did not carry the source id as reference, every plan row that names it is unresolved_customers. Fix the customers (or name them by customer_id) rather than guessing.
  • Cadences past the ceiling. frequency_type is W or M and frequency_period at most 52 weeks or 12 months. Fortnightly is W×2, quarterly M×3, annual M×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, or until_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_cents over 99999999 is rejected at append. A number_of_payments whose total comes to more is an invalid row at finalisation.
  • first_date without first_amount_cents. A one-off first payment needs its amount; the row is rejected without it.
  • Counting the first payment. number_of_payments counts recurring payments only; a first payment is in addition, and the total is number_of_payments × recurring_amount_cents + first_amount_cents. A schedule of 11 collections whose first is a one-off $60 is first_date + first_amount_cents: 6000 and number_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.