Collect recurring payments
Collect the same payment from a customer on a schedule, weekly, monthly, quarterly or yearly, without creating each debit yourself.
Use recurring plans for subscriptions, memberships, rent, instalments, or any charge that repeats. Each scheduled run creates a debit against the customer's payment method and deposits the funds into your bank account.
Prerequisites
- Getting your API KeyJetpay API Key
- Your Getting your bank account IDcompany bank account ID where funds will be deposited
- A Create a contactcontact for the customer
The customer pays with one of:
- A Tokenize a contact credit cardtokenized credit card on their contact
- A bank account on their contact, plus a signed Pre-Authorized Debit (PAD) agreement, which these endpoints call a mandate
- Nothing yet: Jetpay gives you a link where the customer adds a payment method and accepts the plan themselves
Flow overview
sequenceDiagram
autonumber
participant Platform
participant Jetpay
participant Customer
Platform->>Jetpay: POST /recurring-plans
alt Card on file
Jetpay-->>Platform: Plan created (status: active)
else Bank account
Jetpay-->>Platform: Plan created (status: pending_mandate_upload)
Platform->>Jetpay: PUT /recurring-plans/{id}/mandate
Jetpay-->>Platform: Plan updated (status: pending_mandate_review)
else No payment method
Jetpay-->>Platform: Plan created (status: pending_customer_acceptance)
Platform->>Customer: Share the acceptance link
Customer->>Jetpay: Add payment method and accept
end
Jetpay-->>Platform: Webhook (payments.recurringPlans.collectionEnabled)
loop Every scheduled date
Jetpay->>Jetpay: Create debit
Jetpay-->>Platform: Webhook (payments.transactions.debits.*)
endImplementation details
Create a plan
Endpoint: POST /recurring-plans
This example charges a card on file every month, starting on the date you choose.
curl https://extapi.demo.jetpay.baselinepayments.com/recurring-plans \
-X POST \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_123",
"credit_card_id": "cc_456",
"to_bank_account_id": "<your_bank_account_id>",
"amount": 49.99,
"frequency": "monthly",
"start_date": "2026-10-15T00:00:00Z",
"statement": "Monthly membership"
}'To collect from a bank account instead, send from_bank_account_id in place of credit_card_id. To let the customer choose, leave both out.
If the amount will change over time, add "plan_type": "variable". Otherwise the plan is fixed and always collects the same amount.
Handle the response
A new plan returns HTTP 201. A card plan is active right away:
{
"identifier": "plan_789",
"amount": 49.99,
"status": "active",
"expiry_reason": null,
"plan_type": "fixed",
"frequency": "monthly",
"contact_id": "contact_123",
"to_bank_account_id": "your_bank_account_id",
"from_bank_account_id": null,
"customer_credit_card_id": "cc_456",
"start_date": "2026-10-15T00:00:00Z",
"end_date": null,
"statement": "Monthly membership",
"note": null,
"link": "<acceptance_url>",
"created_at": "2026-09-29T14:02:11Z"
}Store the returned identifier. You need it to manage the plan later.
A bank account plan comes back as pending_mandate_upload, and a plan with no payment method comes back as pending_customer_acceptance. The next two steps cover each case.
Upload the mandate (bank account plans)
Endpoint: PUT /recurring-plans/{identifier}/mandate
Upload the PAD agreement your customer signed, as a PDF.
curl https://extapi.demo.jetpay.baselinepayments.com/recurring-plans/plan_789/mandate \
-X PUT \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-F "[email protected];type=application/pdf"The plan moves to pending_mandate_review and can start collecting. Jetpay then reviews the mandate, and you receive payments.recurringPlans.mandate.approved or payments.recurringPlans.mandate.rejected.
Share the acceptance link (no payment method)
Send the plan's link to your customer. Jetpay also emails it to them. On the hosted page, they add a card or bank account and accept the plan, and it becomes active.
The customer must accept before the plan's start date.
Update a plan
Endpoint: PATCH /recurring-plans/{identifier}
Change the statement, note or end date on any plan, or the amount on a variable plan. Send only the fields you want to change.
curl https://extapi.demo.jetpay.baselinepayments.com/recurring-plans/plan_789 \
-X PATCH \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"amount": 59.99
}'The change applies from the next payment onward.
Change the payment method
Swap the card or bank account on an active plan, for example when a customer's card expires.
curl https://extapi.demo.jetpay.baselinepayments.com/recurring-plans/plan_789/payment-method \
-X POST \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"credit_card_id": "cc_999"
}'A new card takes effect right away, on the same schedule. A new bank account puts the plan back to pending_mandate_upload, and payments pause until you upload a mandate for it.
Cancel a plan
Endpoint: POST /recurring-plans/{identifier}/cancel
Stop all future payments.
curl https://extapi.demo.jetpay.baselinepayments.com/recurring-plans/plan_789/cancel \
-X POST \
-H "Authorization: Bearer <YOUR_API_TOKEN>"The plan comes back with "status": "cancelled". Cancelling is final.
Find your plans
Use GET /recurring-plans to list plans, filtered by contact, status or plan type. For example, this finds every plan waiting on a mandate:
curl "https://extapi.demo.jetpay.baselinepayments.com/recurring-plans?status=pending_mandate_upload" \
-H "Authorization: Bearer <YOUR_API_TOKEN>"To fetch one plan, use GET /recurring-plans/{identifier}.
Track status updates via events/webhooks
As a plan moves through its lifecycle, an Event is created for each update. Each scheduled payment is a regular debit, so it also produces the usual debit events. The most efficient way to track these updates in your system is to register for webhooks. If you are not yet familiar with our webhooks system, please review the Events and webhooksEvents and webhooks guide.
{
"recurringPlanId": "plan_789",
"status": "active",
"planType": "fixed",
"frequency": "monthly",
"amount": "49.99",
"startDate": "2026-10-15T00:00:00Z",
"endDate": null,
"expiryReason": null,
"contactId": "contact_123",
"toBankAccountId": "your_bank_account_id",
"fromBankAccountId": null,
"customerCreditCardId": "cc_456",
"statement": "Monthly membership",
"description": null
}See the Recurring Plans reference for request fields and error codes, and the Events reference for every recurring plan event.
Handling failures
The plan can't be created. Nothing was set up, so fix the cause and try again:
- 400: the request is invalid. A common cause is a start date earlier than tomorrow.
- 404: the contact, bank account or card wasn't found. The card or bank account must belong to the contact on the plan.
- 422: a bank account isn't ready to use yet, or the payment provider rejected the customer's bank account.
The request times out or returns a 5xx. Creating a plan doesn't support an Idempotency-Key, so a retry can create a second plan. List the contact's plans first, and only create a new one if it isn't there.
A scheduled payment fails. You receive a debit event with the failed state, and Jetpay emails both you and the customer. The plan stays active and isn't retried automatically: the next attempt is the next scheduled date. Follow up with the customer, and swap the payment method if theirs is no longer valid.
The mandate is rejected. You receive payments.recurringPlans.mandate.rejected with a reason that is safe to show your customer. The plan goes back to pending_mandate_upload and stops collecting. Get a corrected mandate signed and upload it.
The plan expires. You receive payments.recurringPlans.expired. This happens when a plan never activates in time: the customer didn't accept, no mandate was uploaded, or the mandate wasn't reviewed in time (a delay on our side). An expired plan can't be reactivated, so create a new one.
Testing
Use the demo environment to run each flow before going live:
- Create a card plan starting tomorrow and confirm you receive payments.recurringPlans.created and payments.recurringPlans.collectionEnabled.
- Create a bank account plan, upload any PDF as the mandate, and confirm you receive payments.recurringPlans.mandate.uploaded.
- Create a plan with no payment method, open its link, and accept it as the customer would.
- Update the amount on a variable plan and confirm payments.recurringPlans.updated shows the change.
- Cancel a plan and confirm a second cancel is refused with 409.
Payments run on the plan's schedule and can't be triggered early, so you won't see debit events before the start date.
Best practices
- Store the plan identifier. It's how you match plan events to your records.
- Upload mandates promptly. A plan only has until its second scheduled payment to get a mandate on file, and that deadline doesn't move if a mandate is rejected.
- Use variable plans for changing amounts. A fixed plan can't change its amount after creation.
- Treat collectionEnabled as a state, not a one-off. It fires again each time a plan becomes able to collect, for example after a new mandate is uploaded.
- Watch the debit events. Plan events tell you when a plan changes. Debit events tell you whether each payment succeeded.
- Check before you retry a create. List the contact's plans so you don't set up the same plan twice.
FAQ
When is the first payment collected? On the first scheduled date on or after the plan's start date. The start date has to be tomorrow or later.
Can a bank account plan collect before its mandate is approved? Yes. Once the mandate is uploaded, the plan can collect while Jetpay reviews it. If the mandate is later rejected, future payments stop.
When does a change to the amount take effect? From the next payment that hasn't been created yet. Payments already created aren't changed.
Does cancelling stop a payment that's already in progress? No. Cancelling stops future payments only.
What happens when a plan reaches its end date? It stops collecting. Its status stays active.
Can I restart a cancelled or expired plan? No. Create a new plan for the customer.
Does my customer get notified? Yes. Jetpay emails the customer when the plan is set up, a reminder before each payment, after each payment, and when the plan is cancelled, unless customer emails are turned off for your account.
Is the customer surcharge applied to card plans? Yes. If your account charges a customer surcharge, it's added to each card payment. Bank account payments have no surcharge.