Payment plans
Create and run a schedule from your code: deposit, installments and balance, paid by link or charged to the traveler's card. Preview, creation, edits, pause, automatic debit and test mode.
Checked against the API code on October 30, 2026
Overview
A payment plan splits an alternative's amount into a deposit, intermediate installments and a balance. It has a single payment URL: the traveler pays each installment there in turn, or the card they saved is charged automatically on each date.
In the API, a plan is a payment link of kind: "PLAN". On that link, amountCents is the installment payable now, never the total. The total and each installment are on the plan.
| Method | Path | Scope |
|---|---|---|
POST | /v1/bookings/:id/payment-plans/preview | payment-plans:read |
POST | /v1/bookings/:id/payment-plans | payment-plans:write |
GET | /v1/bookings/:id/payment-plans | payment-plans:read |
GET | /v1/payment-plans/:id | payment-plans:read |
GET | /v1/payment-plan-templates | payment-plans:read |
GET | /v1/bookings/:id/payment-plan-suggestion | payment-plans:read |
PATCH | /v1/payment-plans/:id | payment-plans:write |
POST | /v1/payment-plans/:id/pause | payment-plans:write |
POST | /v1/payment-plans/:id/resume | payment-plans:write |
POST | /v1/payment-plans/:id/cancel | payment-plans:write |
POST | /v1/payment-plans/:id/installments/:installmentId/mark-paid | payment-plans:write |
POST | /v1/payment-plans/:id/installments/:installmentId/resend-link | payment-plans:write |
GET | /v1/payment-plans/:id/events | payment-plans:read, test key |
A plan opens on an alternative. Only one can be open at a time, and not next to an active reusable link. A plan has 2 to 7 payments: the deposit, then 1 to 6 payments, the balance included.
Create a plan
Preview a plan
/v1/bookings/:id/payment-plans/previewScopepayment-plans:readSame body as creation. Nothing is written. The response gives the plan creation would make, and everything that would refuse it: valid is true when creation would succeed, short of a concurrent change. It is a POST, so it counts against the write limit.
{
"success": true,
"data": {
"preview": {
"alternative": { "id": "b3a1f0e2-6d4c-4e8a-9b7f-1c2d3e4f5a6b", "ezusAlternativeId": "ALT-1" },
"template": { "id": "4d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d", "name": "Standard" },
"amountCents": 480000,
"currency": "EUR",
"availableCents": 480000,
"collectionMode": "AUTO_CHARGE",
"rules": { "depositPercent": 30, "paymentsAfterDeposit": 2, "balanceOffsetDays": 45 },
"installments": [
{ "amountCents": 144000, "dueDate": "2026-04-21" },
{ "amountCents": 168000, "dueDate": "2026-05-09" },
{ "amountCents": 168000, "dueDate": "2026-05-26" }
],
"valid": true,
"errors": [],
"warnings": []
}
}
}
errors mixes the schedule's codes and the alternative's state. sequence, when present, names the installment at fault (1 is the deposit).
| Code | Meaning |
|---|---|
SUM_MISMATCH | The installments do not add up to amountCents |
DATES_NOT_INCREASING | Two installments are not in date order |
FIRST_DUE_DATE_IN_PAST | The deposit falls before today (Paris time) |
LAST_DUE_DATE_NOT_BEFORE_DEPARTURE | The balance falls on the departure day or after |
INSTALLMENT_BELOW_MINIMUM | An installment is under 1.00 |
INSTALLMENT_COUNT_OUT_OF_RANGE | Fewer than 2 or more than 7 payments |
EXCEEDS_AVAILABLE | The plan exceeds what is left to collect on the alternative |
DEPARTURE_DATE_REQUIRED | The alternative has no departure date |
NOTHING_TO_COLLECT | Nothing is left to collect |
PAYMENT_PLAN_ALREADY_ACTIVE | A plan is already open on the alternative |
REUSABLE_LINK_ACTIVE | A reusable link is active on the alternative |
AUTO_CHARGE_EMAIL_REQUIRED | Automatic debit without the client's email |
AUTO_CHARGE_REQUIRES_CARD | Automatic debit without card in allowedMethods |
warnings can hold INSTALLMENTS_TOO_CLOSE: accepted, but two payments are less than 14 days apart.
Create the plan
/v1/bookings/:id/payment-plansScopepayment-plans:writeCreates the plan and its link on an alternative of the booking. Returns 201 with the plan. The agency's Stripe account must be active (403 STRIPE_NOT_ACTIVE otherwise).
The schedule comes from one source:
templateId: one of the agency's templates (see List templates). Its rules, collection mode and options apply to the booking.rulesalone: the server computes the installments. The deposit isdepositPercent% of the amount, rounded to the unit, due today. The balance is duebalanceOffsetDaysdays before departure. Intermediate installments are spread between the two, and the balance absorbs the rounding.rules+installments: your own dates and amounts, revalidated with the same rules.
| Field | Type | Required | Rules |
|---|---|---|---|
templateId | uuid | one or the other | One of the agency's templates |
rules.depositPercent | integer | one or the other | 1 to 99 |
rules.paymentsAfterDeposit | integer | with rules | 1 to 6, the balance included |
rules.balanceOffsetDays | integer | with rules | 1 to 365 days before departure |
installments | array | no | { amountCents, dueDate } in order, paymentsAfterDeposit + 1 entries. Requires rules |
amountCents | integer | no | The total to schedule. Defaults to what is left to collect on the alternative, or to the sum of installments when you send them |
collectionMode | string | no | LINK or AUTO_CHARGE. Defaults to the template's, otherwise LINK |
remindersEnabled | boolean | no | D-7 and D-1 reminders before an installment paid by link. Defaults to the template's, otherwise false |
allowEarlyPayment | boolean | no | Defaults to the template's, otherwise true |
allowedMethods | array | no | "card" and/or "sepa_debit". ["card"] by default. SEPA applies in EUR only and never with automatic debit |
label | string | no | Up to 255. Defaults to the booking's reference, or its name |
checkoutFieldKeys | array | no | As for a payment link |
alternativeId / ezusAlternativeId | string | no | Target alternative, as for a link. Defaults to the main alternative |
The body is strict: an unknown field returns 422 VALIDATION_ERROR instead of being ignored. templateId and rules together return 422 PLAN_SOURCE_AMBIGUOUS, neither of them 422 PLAN_SOURCE_REQUIRED.
curl -X POST https://api.ezuspay.app/v1/bookings/7c9e6679-7425-40de-944b-e07fc1f90ae7/payment-plans \
-H "x-api-key: $EZP_KEY" -H "Content-Type: application/json" \
-d '{
"ezusAlternativeId": "ALT-1",
"templateId": "4d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
"collectionMode": "AUTO_CHARGE"
}'
{
"success": true,
"data": {
"paymentPlan": {
"id": "9b2e4c1a-3f5d-4e6a-8b7c-0d1e2f3a4b5c",
"status": "PENDING",
"collectionMode": "AUTO_CHARGE",
"totalAmountCents": 480000,
"paidCents": 0,
"installments": [
{ "id": "1a2b3c4d-0000-4000-8000-000000000001", "sequence": 1, "amountCents": 144000, "dueDate": "2026-04-21", "status": "SCHEDULED", "paidAt": null, "collectionMode": "AUTO_CHARGE" },
{ "id": "1a2b3c4d-0000-4000-8000-000000000002", "sequence": 2, "amountCents": 168000, "dueDate": "2026-05-09", "status": "SCHEDULED", "paidAt": null, "collectionMode": "AUTO_CHARGE" },
{ "id": "1a2b3c4d-0000-4000-8000-000000000003", "sequence": 3, "amountCents": 168000, "dueDate": "2026-05-26", "status": "SCHEDULED", "paidAt": null, "collectionMode": "AUTO_CHARGE" }
],
"label": "2026-0042",
"currency": "EUR",
"allowedMethods": ["card"],
"allowEarlyPayment": true,
"remindersEnabled": true,
"rules": { "name": "Standard", "depositPercent": 30, "paymentsAfterDeposit": 2, "balanceOffsetDays": 45 },
"templateId": "4d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
"attentionReason": null,
"autoCharge": { "active": false, "cardBrand": null, "cardLast4": null },
"createdVia": "API",
"paymentLink": { "id": "d290f1ee-6c54-4b01-90e6-d701748f0851", "checkoutUrl": "https://app.ezuspay.io/lnk/voyages-horizon/8K3QZ7M2XR4T" },
"booking": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "reference": "2026-0042", "ezusProjectId": "PRJ-1029" },
"alternative": { "id": "b3a1f0e2-6d4c-4e8a-9b7f-1c2d3e4f5a6b", "ezusAlternativeId": "ALT-1" },
"createdAt": "2026-04-21T09:15:00.000Z",
"updatedAt": "2026-04-21T09:15:00.000Z"
}
}
}
| Field | Values |
|---|---|
status | PENDING (deposit not paid yet), ACTIVE, PAUSED, COMPLETED, CANCELLED |
paidCents | Installments settled, outside Stripe included |
installments[].status | SCHEDULED, PROCESSING (money on its way), PAID, FAILED, ACTION_REQUIRED, ON_HOLD, CANCELLED |
installments[].collectionMode | Can be LINK on an AUTO_CHARGE plan: that installment will be paid by link |
autoCharge.active | true when the traveler consented, their card is saved and the plan is open |
attentionReason | Why the plan needs the agency, otherwise null: PAYMENT_FAILED, INSTALLMENT_UNPAID, NOTICE_UNDELIVERABLE, ORPHAN_PAYMENT, OVERPAID, TOTAL_NOT_ABSORBED, INSTALLMENT_REFUNDED |
createdVia | APP or API |
paymentLink.checkoutUrl | The URL to send the traveler, or to open in the embedded checkout |
Replay after a timeout
There is no Idempotency-Key header, but creation is safe to replay: a second attempt on the same alternative returns 409 PAYMENT_PLAN_ALREADY_ACTIVE, with the id of the plan already open.
{
"success": false,
"error": {
"code": "PAYMENT_PLAN_ALREADY_ACTIVE",
"message": "This alternative already has an open payment plan",
"details": { "planId": "9b2e4c1a-3f5d-4e6a-8b7c-0d1e2f3a4b5c" }
}
}
Read that plan with GET /v1/payment-plans/:id instead of creating another one.
Automatic debit
With collectionMode: "AUTO_CHARGE", the traveler's card is charged on each date, after a notice. Only the traveler can turn it on: when paying the deposit on the plan's page, they click a button that names the authorization, for example "Pay €1,440.00 and activate the payment schedule". The API can neither give that consent nor save a card.
- The client's email is required, to notify them before each charge (
422 AUTO_CHARGE_EMAIL_REQUIRED). For an Ezus booking, fix it in Ezus, then callPOST /v1/bookings/:id/resync. - Card only.
allowedMethodsmust includecard. SEPA is removed. - After consent,
autoCharge.activebecomestrue, with the card's brand and last 4 digits, and thepayment_plan.auto_charge_activatedwebhook is sent. - The traveler can decline by choosing "Pay the deposit only" ("Pay this installment only" after the deposit). The following installments then go to the link.
- Changing the amount or the date of an installment on a consented plan takes it off the debit: it will be paid by link, and the traveler is told. The other installments stay on the debit.
- A declined charge gets at most one retry three days later, announced to the traveler. An unusable card, or a bank asking the traveler to authenticate, moves the installment to the link.
- The traveler can turn the debit off from the plan's page, with "Turn off automatic payments". The agency can also cancel the plan.
- On a completed or cancelled plan,
autoCharge.activeisfalse. While the plan is paused, it staystrue: the debit will resume.
Traveler emails
Ezus Pay writes to the traveler, whoever created the plan: a notice before each charge, the link on the due date, a follow-up three days after an unpaid installment, declined charges, changed amounts or dates, cancellation. If your system also sends its own reminders, the traveler will get both.
| Can be turned off | |
|---|---|
| D-7 and D-1 reminders before an installment paid by link | Yes, with remindersEnabled: false |
| Link on the due date, follow-up three days later | No |
| Notice before a charge | No: it is the promise made to the traveler when they consented |
| Declined charge, changed amount or date, cancellation | No: they announce a change of money |
In test mode, no email reaches the traveler. See Test mode.
Read plans
List a booking's plans
/v1/bookings/:id/payment-plansScopepayment-plans:readEvery plan of the booking, across alternatives and statuses, newest first, in the shape above. Returns 404 BOOKING_NOT_FOUND for a booking outside your agency.
Retrieve a plan
/v1/payment-plans/:idScopepayment-plans:readOne plan, in the shape above. Returns 404 PAYMENT_PLAN_NOT_FOUND when the plan is not the key's agency's.
Plan links also show in GET /v1/bookings/:id/payment-links and GET /v1/payment-links/:id, with a plan summary. See the reference.
List templates
/v1/payment-plan-templatesScopepayment-plans:readThe agency's plan templates, the default first. They are managed in Settings → Payment plans. The API does not change them.
{
"success": true,
"data": {
"templates": [
{
"id": "4d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
"name": "Standard",
"isDefault": true,
"depositPercent": 30,
"paymentsAfterDeposit": 2,
"balanceOffsetDays": 45,
"collectionMode": "LINK",
"remindersEnabled": true,
"allowEarlyPayment": true,
"suggestMinAmountCents": null,
"suggestMinDaysBeforeDeparture": null
}
]
}
}
suggestMinAmountCents (in the agency's currency) and suggestMinDaysBeforeDeparture are the template's suggestion conditions, null when there are none.
Retrieve the suggested plan
/v1/bookings/:id/payment-plan-suggestionScopepayment-plans:readThe plan the agency's templates suggest on an alternative, computed on what is left to collect. The main alternative by default, or ?alternativeId= or ?ezusAlternativeId= for another one, never both. The template chosen is the most specific one whose conditions hold, otherwise the default template if its schedule is valid.
{
"success": true,
"data": {
"suggestion": {
"template": { "id": "4d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d", "name": "Standard" },
"alternative": { "id": "b3a1f0e2-6d4c-4e8a-9b7f-1c2d3e4f5a6b", "ezusAlternativeId": "ALT-1" },
"amountCents": 480000,
"currency": "EUR",
"installments": [
{ "amountCents": 144000, "dueDate": "2026-04-21" },
{ "amountCents": 168000, "dueDate": "2026-05-09" },
{ "amountCents": 168000, "dueDate": "2026-05-26" }
],
"matched": true
}
}
}
suggestion is null when no plan could be created or would help: a plan already open, an active reusable link, nothing left to collect, no departure date, a cancelled booking. matched is false when no template's conditions hold and the default template is offered. To create that plan, send its templateId.
Run a plan
Each action returns the plan as it stands after it. A plan that moved between your read and your action (a payment just arrived) returns 409 PAYMENT_PLAN_CHANGED: read it again, then replay. While a payment of an installment is on its way, edits and cancellation return 409 INSTALLMENT_PROCESSING.
Edit installments
/v1/payment-plans/:idScopepayment-plans:writeChanges the dates and amounts of the unpaid installments. Send all of them, in order, by id. Their number does not change: to change it, cancel the plan and create another one. The total follows.
| Field | Type | Rules |
|---|---|---|
installments | array | { id, amountCents, dueDate } of each unpaid installment, in order |
label | string | Up to 255 |
allowEarlyPayment | boolean | |
remindersEnabled | boolean |
curl -X PATCH https://api.ezuspay.app/v1/payment-plans/9b2e4c1a-3f5d-4e6a-8b7c-0d1e2f3a4b5c \
-H "x-api-key: $EZP_KEY" -H "Content-Type: application/json" \
-d '{
"installments": [
{ "id": "1a2b3c4d-0000-4000-8000-000000000002", "amountCents": 158000, "dueDate": "2026-05-12" },
{ "id": "1a2b3c4d-0000-4000-8000-000000000003", "amountCents": 178000, "dueDate": "2026-05-26" }
]
}'
On a plan whose traveler consented to automatic debit, an installment whose amount or date changes goes to the link. See Automatic debit.
Pause and resume
/v1/payment-plans/:id/pauseScopepayment-plans:writeNo notice, charge or automatic link goes out. The link stays payable, and the traveler is not told. No body.
/v1/payment-plans/:id/resumeScopepayment-plans:writeEach installment whose date passed during the pause needs a new date, otherwise 422 RESUME_DECISIONS_REQUIRED. With no installment overdue, the body can be empty. The traveler receives the dates in force.
{
"decisions": [
{ "installmentId": "1a2b3c4d-0000-4000-8000-000000000002", "action": "reschedule", "dueDate": "2026-06-01" }
]
}
On a consented plan, a rescheduled installment goes to the link.
Cancel
/v1/payment-plans/:id/cancelScopepayment-plans:writeCancels the unpaid installments and the link, detaches the card from the automatic debit and tells the traveler. What was paid stays paid: a refund goes through POST /v1/transactions/:id/refunds. No body.
Mark an installment paid
/v1/payment-plans/:id/installments/:installmentId/mark-paidScopepayment-plans:writeFor a payment received outside Stripe: a manual payment of the installment's exact amount is recorded, and the plan moves on. The next installment only (409 INSTALLMENT_NOT_NEXT otherwise). No fee, no email to the traveler.
| Field | Type | Rules |
|---|---|---|
method | string | BANK_TRANSFER, CASH, CHECK or OTHER |
paidAt | date | YYYY-MM-DD, today or earlier |
note | string | Up to 1,000 characters, for example the transfer reference |
Send the link again
/v1/payment-plans/:id/installments/:installmentId/resend-linkScopepayment-plans:writeSends the traveler the link of the next installment. Not twice within 10 minutes (429 LINK_RECENTLY_SENT). The response adds emailSent, which is false in test mode. The plan is abbreviated in the example below.
{ "success": true, "data": { "paymentPlan": { "id": "9b2e4c1a-3f5d-4e6a-8b7c-0d1e2f3a4b5c" }, "emailSent": true } }
Test mode
With an ezpay_test_ key, plans work as in production, automatic debit included, on the test Stripe account. No email reaches the traveler: each email is kept in the plan's journal.
Read the journal
/v1/payment-plans/:id/eventsScopepayment-plans:readThe plan's journal, oldest first. Available with a test key only (403 TEST_MODE_ONLY with a production key).
{
"success": true,
"data": {
"events": [
{ "id": "…", "kind": "CREATED", "occurredAt": "2026-04-21T09:15:00.000Z", "installmentId": null, "actor": "API", "webhook": "payment_plan.created", "email": null },
{
"id": "…",
"kind": "NOTICE_SENT",
"occurredAt": "2026-05-06T06:00:00.000Z",
"installmentId": "1a2b3c4d-0000-4000-8000-000000000002",
"actor": "SYSTEM",
"webhook": null,
"email": { "to": "jean@example.com", "subject": "Prélèvement de 1 680,00 € le 9 mai 2026 — Voyages Horizon", "text": "…" }
}
]
}
}
actorisAPP(an agency member),API(a key) orSYSTEM(the charge scheduler, Stripe, the traveler).webhookis the event this entry sends to your endpoints, ornull.emailis the email that would have gone out in production, in the traveler's language.
Errors
On top of the common errors:
| Error | When |
|---|---|
404 BOOKING_NOT_FOUND, 404 ALTERNATIVE_NOT_FOUND | Booking or alternative outside your agency |
404 PAYMENT_PLAN_NOT_FOUND | Unknown plan, or another agency's |
404 TEMPLATE_NOT_FOUND, 404 INSTALLMENT_NOT_FOUND | Unknown template or installment |
403 STRIPE_NOT_ACTIVE | The agency's Stripe account cannot collect |
403 TEST_MODE_ONLY | The journal is read with a test key |
422 PLAN_SOURCE_AMBIGUOUS, 422 PLAN_SOURCE_REQUIRED | templateId and rules together, or neither |
422 NOTHING_TO_COLLECT | Nothing is left to collect on the alternative |
422 DEPARTURE_DATE_REQUIRED | The alternative has no departure date |
422 INVALID_PAYMENT_PLAN_SCHEDULE | Invalid schedule (sum, dates, number of installments) |
422 AMOUNT_EXCEEDS_AVAILABLE | The plan exceeds what is left to collect |
422 AUTO_CHARGE_EMAIL_REQUIRED, 422 AUTO_CHARGE_REQUIRES_CARD | Automatic debit not possible |
422 NO_PAYMENT_METHOD_FOR_CURRENCY | No payment method available in this currency |
422 RESUME_DECISIONS_REQUIRED | An installment overdue during the pause has no new date |
422 NO_TRAVELER_EMAIL | No traveler email to send the link to |
409 PAYMENT_PLAN_ALREADY_ACTIVE | A plan is already open (error.details.planId) |
409 REUSABLE_LINK_ACTIVE | A reusable link is active on the alternative |
409 PAYMENT_PLAN_CHANGED | The plan moved since you read it: read it again, then replay |
409 INSTALLMENT_PROCESSING | A payment of the installment is on its way |
409 INSTALLMENT_NOT_NEXT | Only the next installment can be settled or sent again |
409 INSTALLMENT_NOT_PAYABLE | The installment is already paid or cancelled |
409 PAYMENT_PLAN_NOT_OPEN | The plan is already completed or cancelled |
409 PAYMENT_PLAN_ALREADY_PAUSED, 409 PAYMENT_PLAN_NOT_PAUSED | Pause or resume with no effect |
429 LINK_RECENTLY_SENT | The link went out less than 10 minutes ago |
To follow a plan without reading it again, subscribe your endpoints to the payment_plan.* and installment.* events. See Webhooks.