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.

MethodPathScope
POST/v1/bookings/:id/payment-plans/previewpayment-plans:read
POST/v1/bookings/:id/payment-planspayment-plans:write
GET/v1/bookings/:id/payment-planspayment-plans:read
GET/v1/payment-plans/:idpayment-plans:read
GET/v1/payment-plan-templatespayment-plans:read
GET/v1/bookings/:id/payment-plan-suggestionpayment-plans:read
PATCH/v1/payment-plans/:idpayment-plans:write
POST/v1/payment-plans/:id/pausepayment-plans:write
POST/v1/payment-plans/:id/resumepayment-plans:write
POST/v1/payment-plans/:id/cancelpayment-plans:write
POST/v1/payment-plans/:id/installments/:installmentId/mark-paidpayment-plans:write
POST/v1/payment-plans/:id/installments/:installmentId/resend-linkpayment-plans:write
GET/v1/payment-plans/:id/eventspayment-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

POST/v1/bookings/:id/payment-plans/previewScopepayment-plans:read

Same 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.

JSON
{
  "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).

CodeMeaning
SUM_MISMATCHThe installments do not add up to amountCents
DATES_NOT_INCREASINGTwo installments are not in date order
FIRST_DUE_DATE_IN_PASTThe deposit falls before today (Paris time)
LAST_DUE_DATE_NOT_BEFORE_DEPARTUREThe balance falls on the departure day or after
INSTALLMENT_BELOW_MINIMUMAn installment is under 1.00
INSTALLMENT_COUNT_OUT_OF_RANGEFewer than 2 or more than 7 payments
EXCEEDS_AVAILABLEThe plan exceeds what is left to collect on the alternative
DEPARTURE_DATE_REQUIREDThe alternative has no departure date
NOTHING_TO_COLLECTNothing is left to collect
PAYMENT_PLAN_ALREADY_ACTIVEA plan is already open on the alternative
REUSABLE_LINK_ACTIVEA reusable link is active on the alternative
AUTO_CHARGE_EMAIL_REQUIREDAutomatic debit without the client's email
AUTO_CHARGE_REQUIRES_CARDAutomatic debit without card in allowedMethods

warnings can hold INSTALLMENTS_TOO_CLOSE: accepted, but two payments are less than 14 days apart.

Create the plan

POST/v1/bookings/:id/payment-plansScopepayment-plans:write

Creates 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.
  • rules alone: the server computes the installments. The deposit is depositPercent % of the amount, rounded to the unit, due today. The balance is due balanceOffsetDays days 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.
FieldTypeRequiredRules
templateIduuidone or the otherOne of the agency's templates
rules.depositPercentintegerone or the other1 to 99
rules.paymentsAfterDepositintegerwith rules1 to 6, the balance included
rules.balanceOffsetDaysintegerwith rules1 to 365 days before departure
installmentsarrayno{ amountCents, dueDate } in order, paymentsAfterDeposit + 1 entries. Requires rules
amountCentsintegernoThe total to schedule. Defaults to what is left to collect on the alternative, or to the sum of installments when you send them
collectionModestringnoLINK or AUTO_CHARGE. Defaults to the template's, otherwise LINK
remindersEnabledbooleannoD-7 and D-1 reminders before an installment paid by link. Defaults to the template's, otherwise false
allowEarlyPaymentbooleannoDefaults to the template's, otherwise true
allowedMethodsarrayno"card" and/or "sepa_debit". ["card"] by default. SEPA applies in EUR only and never with automatic debit
labelstringnoUp to 255. Defaults to the booking's reference, or its name
checkoutFieldKeysarraynoAs for a payment link
alternativeId / ezusAlternativeIdstringnoTarget 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.

Shell
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"
  }'
JSON
{
  "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"
    }
  }
}
FieldValues
statusPENDING (deposit not paid yet), ACTIVE, PAUSED, COMPLETED, CANCELLED
paidCentsInstallments settled, outside Stripe included
installments[].statusSCHEDULED, PROCESSING (money on its way), PAID, FAILED, ACTION_REQUIRED, ON_HOLD, CANCELLED
installments[].collectionModeCan be LINK on an AUTO_CHARGE plan: that installment will be paid by link
autoCharge.activetrue when the traveler consented, their card is saved and the plan is open
attentionReasonWhy the plan needs the agency, otherwise null: PAYMENT_FAILED, INSTALLMENT_UNPAID, NOTICE_UNDELIVERABLE, ORPHAN_PAYMENT, OVERPAID, TOTAL_NOT_ABSORBED, INSTALLMENT_REFUNDED
createdViaAPP or API
paymentLink.checkoutUrlThe 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.

JSON
{
  "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 call POST /v1/bookings/:id/resync.
  • Card only. allowedMethods must include card. SEPA is removed.
  • After consent, autoCharge.active becomes true, with the card's brand and last 4 digits, and the payment_plan.auto_charge_activated webhook 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.active is false. While the plan is paused, it stays true: 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.

EmailCan be turned off
D-7 and D-1 reminders before an installment paid by linkYes, with remindersEnabled: false
Link on the due date, follow-up three days laterNo
Notice before a chargeNo: it is the promise made to the traveler when they consented
Declined charge, changed amount or date, cancellationNo: they announce a change of money

In test mode, no email reaches the traveler. See Test mode.

Read plans

List a booking's plans

GET/v1/bookings/:id/payment-plansScopepayment-plans:read

Every 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

GET/v1/payment-plans/:idScopepayment-plans:read

One 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

GET/v1/payment-plan-templatesScopepayment-plans:read

The agency's plan templates, the default first. They are managed in Settings → Payment plans. The API does not change them.

JSON
{
  "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

GET/v1/bookings/:id/payment-plan-suggestionScopepayment-plans:read

The 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.

JSON
{
  "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

PATCH/v1/payment-plans/:idScopepayment-plans:write

Changes 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.

FieldTypeRules
installmentsarray{ id, amountCents, dueDate } of each unpaid installment, in order
labelstringUp to 255
allowEarlyPaymentboolean
remindersEnabledboolean
Shell
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

POST/v1/payment-plans/:id/pauseScopepayment-plans:write

No notice, charge or automatic link goes out. The link stays payable, and the traveler is not told. No body.

POST/v1/payment-plans/:id/resumeScopepayment-plans:write

Each 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.

JSON
{
  "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

POST/v1/payment-plans/:id/cancelScopepayment-plans:write

Cancels 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

POST/v1/payment-plans/:id/installments/:installmentId/mark-paidScopepayment-plans:write

For 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.

FieldTypeRules
methodstringBANK_TRANSFER, CASH, CHECK or OTHER
paidAtdateYYYY-MM-DD, today or earlier
notestringUp to 1,000 characters, for example the transfer reference
POST/v1/payment-plans/:id/installments/:installmentId/resend-linkScopepayment-plans:write

Sends 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.

JSON
{ "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

GET/v1/payment-plans/:id/eventsScopepayment-plans:read

The plan's journal, oldest first. Available with a test key only (403 TEST_MODE_ONLY with a production key).

JSON
{
  "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": "…" }
      }
    ]
  }
}
  • actor is APP (an agency member), API (a key) or SYSTEM (the charge scheduler, Stripe, the traveler).
  • webhook is the event this entry sends to your endpoints, or null.
  • email is the email that would have gone out in production, in the traveler's language.

Errors

On top of the common errors:

ErrorWhen
404 BOOKING_NOT_FOUND, 404 ALTERNATIVE_NOT_FOUNDBooking or alternative outside your agency
404 PAYMENT_PLAN_NOT_FOUNDUnknown plan, or another agency's
404 TEMPLATE_NOT_FOUND, 404 INSTALLMENT_NOT_FOUNDUnknown template or installment
403 STRIPE_NOT_ACTIVEThe agency's Stripe account cannot collect
403 TEST_MODE_ONLYThe journal is read with a test key
422 PLAN_SOURCE_AMBIGUOUS, 422 PLAN_SOURCE_REQUIREDtemplateId and rules together, or neither
422 NOTHING_TO_COLLECTNothing is left to collect on the alternative
422 DEPARTURE_DATE_REQUIREDThe alternative has no departure date
422 INVALID_PAYMENT_PLAN_SCHEDULEInvalid schedule (sum, dates, number of installments)
422 AMOUNT_EXCEEDS_AVAILABLEThe plan exceeds what is left to collect
422 AUTO_CHARGE_EMAIL_REQUIRED, 422 AUTO_CHARGE_REQUIRES_CARDAutomatic debit not possible
422 NO_PAYMENT_METHOD_FOR_CURRENCYNo payment method available in this currency
422 RESUME_DECISIONS_REQUIREDAn installment overdue during the pause has no new date
422 NO_TRAVELER_EMAILNo traveler email to send the link to
409 PAYMENT_PLAN_ALREADY_ACTIVEA plan is already open (error.details.planId)
409 REUSABLE_LINK_ACTIVEA reusable link is active on the alternative
409 PAYMENT_PLAN_CHANGEDThe plan moved since you read it: read it again, then replay
409 INSTALLMENT_PROCESSINGA payment of the installment is on its way
409 INSTALLMENT_NOT_NEXTOnly the next installment can be settled or sent again
409 INSTALLMENT_NOT_PAYABLEThe installment is already paid or cancelled
409 PAYMENT_PLAN_NOT_OPENThe plan is already completed or cancelled
409 PAYMENT_PLAN_ALREADY_PAUSED, 409 PAYMENT_PLAN_NOT_PAUSEDPause or resume with no effect
429 LINK_RECENTLY_SENTThe 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.