# Payment Plans

Split a fixed total into installments, collect customer authorization, and charge each installment automatically.

---

## Overview

A payment plan is an installment agreement for a fixed `totalAmount` with one customer:

- 💰 **Fixed total** - Split into equal installments, a down payment plus installments, or a custom schedule
- 📅 **Payment frequency** - One-time, weekly, bi-weekly, monthly, quarterly, or every N days
- ✍️ **Customer authorization** - The customer accepts the terms and provides a card on a hosted page
- 🔄 **Automatic retries** - Declined installments are retried; the plan defaults after the last attempt
- ⏰ **Late fees** - Optional fixed or percentage fee per day, week or month overdue

> **Note:** Payment plans are installment agreements, not subscription "price plans". For open-ended subscriptions use [Subscriptions](./subscriptions.md).

### Authentication

Payment plan management endpoints live under `/api/v1/billing/payment-plans`:

```http
Authorization: Bearer YOUR_JWT
X-Tenant-ID: your-tenant-id
```

- A user JWT is required and its tenant must match `X-Tenant-ID` (`401` otherwise). API keys are not accepted on these routes.
- `merchantId` is read from the request body on create and from the query string on list.
- Most errors are `{ "success": false, "error": "message" }` (a string). Invalid state transitions (pause, resume, cancel, activate) currently return `500`.

---

## Create Payment Plan

### Endpoint

```http
POST /api/v1/billing/payment-plans
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/billing/payment-plans \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "c1a7e5d2-3b4f-4e6a-9c8d-7f0b1a2e3d45",
    "planName": "Dental treatment plan",
    "customerName": "Jane Smith",
    "customerEmail": "jane@example.com",
    "customerPhone": "5551234567",
    "totalAmount": 1200.00,
    "timeframeType": "months_6",
    "paymentStructure": "fixed_down_equal",
    "structureConfig": { "down_payment_amount": 200, "num_remaining_payments": 5 },
    "paymentFrequency": "monthly",
    "firstPaymentDate": "2026-11-01",
    "preferredDayOfMonth": 1,
    "lateFeeType": "fixed",
    "lateFeeValue": 10,
    "lateFeeFrequency": "per_month"
  }'
```

**Response:** `201 Created`
```json
{
  "success": true,
  "data": {
    "id": "6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47",
    "planNumber": "PP-2026-00042",
    "planName": "Dental treatment plan",
    "merchantId": "c1a7e5d2-3b4f-4e6a-9c8d-7f0b1a2e3d45",
    "customerName": "Jane Smith",
    "customerEmail": "jane@example.com",
    "totalAmount": 1200.00,
    "remainingAmount": 1200.00,
    "currency": "USD",
    "timeframeType": "months_6",
    "paymentStructure": "fixed_down_equal",
    "paymentFrequency": "monthly",
    "totalPaymentsCount": 6,
    "nextPaymentDate": "2026-11-01",
    "nextPaymentAmount": 200.00,
    "status": "pending_approval",
    "createdAt": "2026-10-01T12:00:00.000Z"
  }
}
```

When `customerEmail` or `customerPhone` is present, a setup link (valid 7 days) is sent to the customer by email (or SMS when there is no email) so they can authorize the plan.

---

## Request Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `merchantId` | uuid | Yes | Merchant the plan belongs to |
| `planName` | string | Yes | Plan name |
| `customerName` | string | Yes | Customer name |
| `customerEmail` | string | Yes | Customer email |
| `customerPhone` | string | No | Customer phone |
| `customerId` | uuid | No | Existing customer. If omitted, a customer is found or created from the name/email |
| `totalAmount` | decimal | Yes | Total to collect (must be > 0) |
| `currency` | string | No | Default `USD` |
| `timeframeType` | string | Yes | `days_30`, `days_60`, `days_90`, `months_3`, `months_6`, `months_12`, `custom` |
| `customDurationDays` | integer | If `custom` | Plan length in days (≥ 1) |
| `paymentStructure` | string | Yes | `equal_split`, `fixed_down_equal`, `percent_down_equal`, `custom_schedule` |
| `structureConfig` | object | No | See below. Missing counts are derived from timeframe ÷ frequency |
| `paymentFrequency` | string | Yes | `one_time`, `weekly`, `bi_weekly` (`biweekly` accepted), `monthly`, `quarterly`, `custom_days` |
| `customFrequencyDays` | integer | If `custom_days` | Days between payments (≥ 1) |
| `firstPaymentDate` | date | Yes | Date of the first payment |
| `preferredDayOfMonth` | integer | No | 1-28 |
| `paymentMethodType` | string | No | `card` (default), `ach`, `manual` |
| `paymentMethodToken` | string | No | PCI vault token, if the card is already collected |
| `paymentMethodLast4` / `paymentMethodBrand` | string | No | Card display details |
| `lateFeeType` | string | No | `none` (default), `fixed`, `percent` |
| `lateFeeValue` | decimal | No | Fixed amount, or percent of the installment |
| `lateFeeFrequency` | string | No | `per_day` (default), `per_week`, `per_month` |
| `additionalFeeType` / `additionalFeeValue` | string / decimal | No | `none`, `fixed`, `percent` — stored on the plan |
| `defaultedAfterDays` | integer | No | Stored on the plan |
| `retryFailedPayments` | boolean | No | Default `true` |
| `sendPaymentReminders` | boolean | No | Default `true` |
| `reminderDaysBefore` / `reminderDaysAfter` | integer | No | Defaults `3` / `1` |
| `sendPaymentReceipts` | boolean | No | Default `true` |
| `activateImmediately` | boolean | No | `true` creates the plan as `active` without customer authorization (default `false` → `pending_approval`) |
| `description` | string | No | Stored as plan notes |

**`structureConfig` by structure:**

| Structure | Fields | Rules |
|-----------|--------|-------|
| `equal_split` | `num_payments` | At least 2 payments |
| `fixed_down_equal` | `down_payment_amount`, `num_remaining_payments` | Down payment > 0 and < total; ≥ 1 remaining payment |
| `percent_down_equal` | `down_payment_percent`, `num_remaining_payments` | Percent between 1 and 99; ≥ 1 remaining payment |
| `custom_schedule` | `payments` (array of amounts) | At least 2 payments; amounts must sum to `totalAmount` |

Validation failures return `400` with the messages joined in `error`.

---

## Preview Schedule

Calculate the installment schedule without saving anything. Takes the same schedule fields as create (`totalAmount`, `timeframeType`, `paymentStructure`, `structureConfig`, `paymentFrequency`, `firstPaymentDate`, ...).

### Endpoint

```http
POST /api/v1/billing/payment-plans/preview
```

**Response:**
```json
{
  "success": true,
  "data": {
    "payments": [
      { "paymentNumber": 1, "scheduledDate": "2026-11-01", "amount": 200.00, "isDownPayment": true, "label": "Down Payment" },
      { "paymentNumber": 2, "scheduledDate": "2026-12-01", "amount": 200.00, "isDownPayment": false, "label": "Payment 1 of 5" }
    ],
    "totalAmount": 1200.00,
    "numberOfPayments": 6,
    "startDate": "2026-11-01",
    "endDate": "2027-04-01",
    "timeframeDays": 180
  }
}
```

---

## Get Structure Suggestions

### Endpoint

```http
GET /api/v1/billing/payment-plans/suggestions
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `totalAmount` | decimal | Yes | Total amount |
| `timeframeType` | string | Yes | Timeframe (see above) |

Returns a list of `{ structure, config, description, previewPayments }` options (e.g. equal split, 50% down).

---

## List Payment Plans

### Endpoint

```http
GET /api/v1/billing/payment-plans
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Default `1` |
| `limit` | integer | Default `20` |
| `status` | string | One status (see Plan Statuses) |
| `merchantId` | uuid | Filter by merchant |
| `q` | string | Search customer name/email, plan name or plan number |

```bash
curl "https://uat.auxcore.net/api/v1/billing/payment-plans?status=active&limit=50" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": [
    {
      "id": "6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47",
      "planNumber": "PP-2026-00042",
      "planName": "Dental treatment plan",
      "customerName": "Jane Smith",
      "totalAmount": 1200.00,
      "remainingAmount": 1000.00,
      "totalPaidAmount": 200.00,
      "status": "active",
      "nextPaymentDate": "2026-12-01",
      "nextPaymentAmount": 200.00,
      "completedPayments": 1,
      "totalPayments": 6,
      "lateFeeType": "fixed",
      "lateFeeAmount": 10,
      "lateFeeFrequency": "per_month",
      "scheduledPayments": [
        { "id": "a4c2...", "paymentNumber": 1, "scheduledDate": "2026-11-01", "scheduledAmount": 200.00, "status": "paid", "lateFeeAmount": 0 }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 50
}
```

---

## Payment Plan Overview

### Endpoint

```http
GET /api/v1/billing/payment-plans/overview
```

**Response:**
```json
{ "success": true, "data": { "plans": 12 } }
```

---

## Get Payment Plan

### Endpoint

```http
GET /api/v1/billing/payment-plans/:id
```

```bash
curl https://uat.auxcore.net/api/v1/billing/payment-plans/6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

Returns the full plan with `scheduledPayments`. `404` if not found.

---

## Get Payment Schedule

### Endpoint

```http
GET /api/v1/billing/payment-plans/:id/schedule
```

Returns the scheduled installments ordered by date, each with `paymentNumber`, `scheduledDate`, `scheduledAmount`, `actualAmount`, `lateFeeAmount`, `status`, `attemptCount`, `nextRetryAt`, `failureReason`, `transactionId` and `isDownPayment`.

**Installment statuses:** `waiting`, `scheduled`, `next_charge`, `pending` (retry scheduled), `processing`, `paid`, `completed`, `failed`, `cancelled`, `skipped`, `refunded`.

---

## Get Plan History

### Endpoint

```http
GET /api/v1/billing/payment-plans/:id/history
```

Returns the activity log (created, activated, authorized, payment succeeded/failed, retry scheduled, paused, resumed, cancelled, completed, defaulted, payment method linked).

---

## Update Payment Plan

### Endpoint

```http
PUT /api/v1/billing/payment-plans/:id
```

```bash
curl -X PUT https://uat.auxcore.net/api/v1/billing/payment-plans/6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "planName": "Dental treatment plan (updated)",
    "customerEmail": "jane.smith@example.com",
    "description": "Updated notes"
  }'
```

Only `planName`, `customerName`, `customerEmail`, `customerPhone`, `description` (notes) and `metadata` are applied. Sending `totalAmount` or `paymentStructure` on a plan that is not `draft` returns `400`; the schedule itself cannot be changed through this endpoint.

> **Note:** There is no delete endpoint. Cancel a plan instead.

---

## Activate Payment Plan

Activate a `draft` or `pending_approval` plan without customer authorization.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/activate
```

---

## Authorize Payment Plan (Merchant)

Attach a card collected by the merchant and charge the first installment as a customer-initiated card-on-file transaction. Behaves like the public authorize endpoint below, but the body uses the `paymentMethod*` names only (no `pciToken` / `last4` / `cardBrand` aliases).

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/authorize
```

```json
{ "paymentMethodToken": "pci_tok_9f8e7d6c5b4a", "paymentMethodLast4": "1111", "paymentMethodBrand": "visa", "consentAccepted": true }
```

`consentAccepted` must be `true` (`400 CONSENT_REQUIRED`) and `paymentMethodToken` is required (`400 PAYMENT_REQUIRED`). A plan that is not `draft` or `pending_approval` returns `400` with a plain string `error` (no `INVALID_STATUS` code). Declines return `402 PAYMENT_DECLINED`.

---

## Pause Payment Plan

Only `active` plans can be paused.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/pause
```

---

## Resume Payment Plan

Only `paused` plans can be resumed.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/resume
```

---

## Cancel Payment Plan

Cancels the plan and every unpaid installment. `completed` or `cancelled` plans cannot be cancelled.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/cancel
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/billing/payment-plans/6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47/cancel \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Paid in full by check" }'
```

---

## Charge Next Installment

Charge the next due installment now (plus any late fee). The plan must be `active` and have a payment method. A decline returns `402`; other failures return `400`.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/charge-now
```

---

## Resend Authorization Link

Re-send the setup link (valid 7 days) for a `pending_approval` plan.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/resend-auth
```

**Response:**
```json
{ "success": true, "data": { "linkId": "lnk_7Hq2Xv9P", "setupLinkId": "lnk_7Hq2Xv9P" } }
```

---

## Link Payment Method

Replace the card on file. Does not charge the new card.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/link-payment-method
```

```json
{ "pciToken": "pci_tok_1a2b3c4d5e6f", "last4": "4242", "brand": "visa", "type": "card" }
```

`pciToken` is required (`400` otherwise).

---

## Send Payment Method Update Link

Email the customer a link (valid 24 hours) to update their card. Returns `400` if the plan has no customer email.

### Endpoint

```http
POST /api/v1/billing/payment-plans/:id/send-method-update-link
```

**Response:**
```json
{ "success": true, "data": { "token": "0f1e2d3c-4b5a-4968-8776-a5b4c3d2e1f0", "updateUrl": "https://uat.auxcore.net/update-payment-method?token=0f1e2d3c-4b5a-4968-8776-a5b4c3d2e1f0&type=plan&id=6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47" } }
```

---

## List Plan Templates

### Endpoint

```http
GET /api/v1/billing/payment-plan-templates
```

Returns active templates for the tenant, plus merchant-specific ones when `merchantId` is passed in the query string.

---

## Create Plan Template

### Endpoint

```http
POST /api/v1/billing/payment-plan-templates
```

```json
{
  "name": "6 months, 20% down",
  "merchantId": "c1a7e5d2-3b4f-4e6a-9c8d-7f0b1a2e3d45",
  "timeframeType": "months_6",
  "paymentStructure": "percent_down_equal",
  "structureConfig": { "down_payment_percent": 20, "num_remaining_payments": 5 },
  "paymentFrequency": "monthly",
  "preferredDayOfMonth": 1
}
```

---

## Public Plan Authorization

The customer opens the setup link and authorizes the plan on a hosted page. These two endpoints need **no API key**: the plan ID is the capability.

### Get Plan (Public)

```http
GET /api/v1/public/payment-plans/:planId
```

Returns the plan with its `scheduledPayments` (`id`, `paymentNumber`, `scheduledDate`, `scheduledAmount`, `status`, `isDownPayment`) and merchant branding (`merchantName`, `merchantLogo`, `primaryColor`, `displaySaveCard`). `404` if not found.

### Authorize Plan (Public)

```http
POST /api/v1/public/payment-plans/:planId/authorize
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/payment-plans/6e1b9c3a-2d4f-4a7e-8b5c-1f0a9d2e3c47/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "pciToken": "pci_tok_9f8e7d6c5b4a",
    "last4": "1111",
    "cardBrand": "visa",
    "customerName": "Jane Smith",
    "customerEmail": "jane@example.com",
    "consentAccepted": true
  }'
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `pciToken` | string | Yes | PCI vault card token from the hosted card fields (alias `paymentMethodToken`) |
| `consentAccepted` | boolean | Yes | Must be `true` |
| `last4` | string | No | Alias `paymentMethodLast4` |
| `cardBrand` | string | No | Alias `paymentMethodBrand` |
| `customerName`, `customerEmail`, `customerPhone` | string | No | Override the customer details on the plan |

What happens:
1. The token is promoted to a long-lived card-on-file token.
2. The first due installment (plus any late fee) is charged.
3. The plan becomes `active` and the first installment is marked paid.

| Status | Code | Meaning |
|--------|------|---------|
| 400 | `CONSENT_REQUIRED` | `consentAccepted` is not `true` |
| 400 | `PAYMENT_REQUIRED` | No card token |
| 400 | `INVALID_STATUS` | Plan is not `draft` or `pending_approval` |
| 402 | `PAYMENT_DECLINED` | The first installment was declined |
| 404 | `NOT_FOUND` | Plan not found |
| 502 | `TOKEN_PROMOTION_FAILED` | The card could not be stored for recurring use |

The gateway allows up to 50 seconds for this call.

---

## Plan Statuses

| Status | Description |
|--------|-------------|
| `draft` | Created, not yet sent |
| `pending_approval` | Waiting for customer authorization |
| `active` | Installments are being charged |
| `paused` | Charging suspended |
| `completed` | All installments paid |
| `cancelled` | Cancelled; unpaid installments cancelled |
| `defaulted` | An installment failed on its last retry attempt |

---

## Automatic Charging, Retries and Late Fees

- Due installments on `active` plans are charged hourly. Plans that are not yet authorized are never charged.
- A declined installment is retried every `retryIntervalDays` (default **3**) up to `maxRetryAttempts` (default **3**) attempts while `retryFailedPayments` is on. After the last failed attempt the plan becomes `defaulted`.
- Late fees start the day after the due date and accrue per full interval: `per_day` (each day), `per_week` (each 7 days) or `per_month` (each month). A `percent` fee is a percentage of the installment. The fee is added to the amount charged and recorded on the installment as `lateFeeAmount`.

---

## Best Practices

### ✅ DO:
- **Preview first** - Call the preview endpoint before creating a plan
- **Collect authorization** - Let the customer accept the terms through the setup link
- **Use a down payment** - Reduces risk on longer plans
- **Watch for `defaulted` plans** - Send a method update link and charge again

### ❌ DON'T:
- **Don't use `activateImmediately` without consent** - The customer has not accepted the terms
- **Don't change amounts after creation** - Cancel and create a new plan instead

---

## Next Steps

- [Create subscriptions →](./subscriptions.md)
- [Handle failed payments →](./dunning.md)
- [Manage billing cycles →](./billing-cycles.md)

---

**Need help?** Contact support@auxcore.net
