# Invoice Payments

Collect, record and track invoice payments, including partial payments and payment history.

---

## Overview

There are two ways money reaches an invoice:

- 💳 **Customer pays the pay link** - card, ACH or a saved card on the hosted pay page (`/api/v1/public/invoices/...`). The charge and the invoice update happen together.
- 📝 **Merchant records a payment** - money already collected elsewhere (portal collect, terminal, cash, check) is recorded against the invoice with `/payment` or `/external-payment`. These endpoints never charge anything.

Every payment is appended to the invoice's `paymentHistory`, and `amountPaid`, `amountDue` and `status` move with it:

- 📊 **Partial payments** - only when `allowPartialPayment` is `true` (optionally with a minimum payment)
- ✅ **Fully paid** - status becomes `paid` and `paidAt` is stamped when `amountDue` reaches 0

Admin endpoints use `Authorization: Bearer YOUR_TOKEN` + `X-Tenant-ID` (or `x-api-key`).

---

## Record Payment

Record money that has **already been collected** (full or partial). Do not send card data — this endpoint does not charge.

### Endpoint

```http
POST /api/v1/invoices/:invoiceId/payment
```

`POST /api/v1/invoices/:invoiceId/record-payment` is an alias.

```bash
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/payment \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500.00,
    "paymentMethod": "card",
    "transactionId": "TXN-20261001-8F3A2C",
    "notes": "Collected at the counter"
  }'
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `amount` | number | Yes | Amount collected |
| `paymentMethod` | string | Yes | Free text, e.g. `card`, `ach`, `cash` |
| `transactionId` | string | No | The transaction that collected the money |
| `notes` | string | No | Shown in the activity log |
| `isExternal` | boolean | No | Marks the entry as recorded outside AuxCORE |

**Response:** the updated invoice.

```json
{
  "success": true,
  "data": {
    "id": "0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10",
    "status": "partial",
    "totalAmount": "1000.00",
    "amountPaid": 500.00,
    "amountDue": 500.00,
    "paymentHistory": [
      {
        "transactionId": "TXN-20261001-8F3A2C",
        "amount": 500.00,
        "paymentMethod": "card",
        "paidAt": "2026-10-01T14:00:00.000Z",
        "notes": "Collected at the counter",
        "isExternal": false,
        "balanceAfter": 500.00,
        "actorId": "a4c2e9d1-7b3f-4e8a-9c21-5d6e7f8a9b0c",
        "actorEmail": "jane@merchant.example"
      }
    ]
  }
}
```

### Validation

- The invoice must be `sent`, `viewed`, `partial` or `overdue` (INV-014); otherwise `400`.
- An amount below `amountDue` is rejected when `allowPartialPayment` is `false`.
- `/payment` is a settlement endpoint and does **not** check the minimum payment rule or the remaining balance — run **Payment Preflight** first when you decide the amount yourself.

---

## Payment Preflight

Check an amount against the invoice's rules **before** charging it elsewhere (e.g. a saved card or ACH collect). Nothing is recorded.

### Endpoint

```http
POST /api/v1/invoices/:invoiceId/payment-preflight
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/payment-preflight \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 250.00 }'
```

**Response:**
```json
{
  "success": true,
  "data": { "amount": 250.00, "amountDue": 500.00, "minimumPayment": 125.00 }
}
```

`400 VALIDATION_ERROR` when the invoice is not collectable, the amount is not greater than zero, exceeds the remaining balance, is partial on a full-payment-only invoice, or is below the minimum payment.

---

## Record External Payment

Record merchant-reported money received outside AuxCORE.

### Endpoint

```http
POST /api/v1/invoices/:invoiceId/external-payment
```

### Request

Requires a user token with the `invoices.manage` permission (API-key calls are rejected with `403`).

```bash
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/external-payment \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500.00,
    "paymentMethod": "Check",
    "paymentDate": "2026-10-01",
    "recordedBy": "Jane Doe",
    "reference": "CHK-1024",
    "internalNote": "Deposited to bank"
  }'
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `amount` | Yes | Greater than 0 and not above the remaining balance |
| `paymentMethod` | Yes | One of the allowed methods below (exact spelling) |
| `paymentDate` | Yes | `YYYY-MM-DD` |
| `recordedBy` | No | Display label ("Received by") |
| `reference` | No | e.g. check number |
| `internalNote` | No | Internal note |

### Allowed Payment Methods

```text
Cash
ACH
Check
Wire Transfer
External Card
Other
```

### Behavior

- Updates `amountPaid`, `amountDue`, and invoice `status` using the existing invoice balance transition.
- Appends a `paymentHistory` item: `amount`, `paymentMethod`, `paidAt`, `isExternal: true`, `balanceAfter`, `recordedBy`, optional `notes` and actor fields.
- Combines `reference` and `internalNote` into `notes` (`"CHK-1024 · Deposited to bank"`).
- Stores `recordedBy` as a display-only label, and stamps `actorId` / `actorEmail` from the authenticated token — the request body can never set the audit actor.
- Stamps `paidAt` at midday UTC on the given date, so the date renders unchanged in every viewer timezone.
- Creates a matching transaction record (provider `EXT`) so the payment appears in transaction reporting. This is best-effort: if it fails, the invoice is still updated.
- Does not charge anything and sends no notifications, receipts or webhooks.

### Validation

- Rejects any invoice that is not collectable — only `sent`, `viewed`, `partial` and `overdue` are accepted (INV-014).
- Rejects no-balance invoices.
- Rejects invalid payment methods or dates.
- Rejects non-finite, zero, negative, or over-balance amounts.
- Rejects partial amounts when `allowPartialPayment` is `false`, unless the invoice is already partial.
- Enforces the invoice's minimum payment rule.
- Requires `paymentDate` in `YYYY-MM-DD` format and not before the invoice issue date. The future-date bound allows one day past UTC today, so a merchant east of UTC can record the current local date.

### Response

Returns the updated invoice.

```json
{
  "success": true,
  "data": {
    "id": "0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10",
    "status": "partial",
    "amountPaid": 500.00,
    "amountDue": 1000.00,
    "paymentHistory": [
      {
        "amount": 500.00,
        "paymentMethod": "Check",
        "paidAt": "2026-10-01T12:00:00.000Z",
        "notes": "CHK-1024 · Deposited to bank",
        "isExternal": true,
        "balanceAfter": 1000.00,
        "recordedBy": "Jane Doe",
        "actorId": "a4c2e9d1-7b3f-4e8a-9c21-5d6e7f8a9b0c",
        "actorEmail": "jane@merchant.example"
      }
    ]
  }
}
```

---

## Payment History

There is no separate history endpoint. `GET /api/v1/invoices/:invoiceId` returns `paymentHistory` (one entry per payment) together with `amountPaid`, `amountDue`, `status`, `transactionId` (primary transaction) and `paidAt`.

---

## Payment Status

Payment progress is reflected in the invoice `status`:

| Status | Description |
|--------|-------------|
| `sent` / `viewed` / `overdue` | No payments received yet |
| `partial` | Some payments received |
| `pending` | An ACH payment is processing |
| `paid` | Fully paid |

---

## Customer Payments (Pay Link)

The hosted pay page uses these public endpoints. They need no API key: possession of the invoice's `linkId` authorises them. A full edit rotates the `linkId`; the old one returns `410 LINK_ROTATED`.

### Get Invoice for Payment

```http
GET /api/v1/public/invoices/link/:linkId
```

Returns the invoice plus merchant branding, `allowedPaymentMethods` (from `metadata.acceptCards` / `metadata.acceptAch`), `hasSavedCard` and `hasSavedAch`. The first open moves a `sent` invoice to `viewed`.

`GET /api/v1/public/invoices/link/:linkId/fee-preview` returns the customer-facing fee for an amount.

### Pay by Card

```http
POST /api/v1/public/invoices/:linkId/pay
```

```json
{
  "pciToken": "pci_tok_9f8e7d6c5b4a3210",
  "amount": 250.00,
  "email": "john@example.com",
  "customerName": "John Doe",
  "billingAddress": "100 Main St",
  "billingCity": "Austin",
  "billingState": "TX",
  "billingPostalCode": "78701",
  "billingCountry": "US"
}
```

- `pciToken` (`pci_tok_…`, from the AuxCORE SDK hosted fields) or `cardToken` is required. Raw card fields (`cardNumber`, `CardNumber`, `CVV`) are rejected with `400 VALIDATION_ERROR` in every environment.
- Optional `last4` / `cardBrand` are shown on the receipt; when missing they are looked up from the PCI vault.
- `amount` is only honoured when the invoice allows partial payments (capped at `amountDue`, and at least the minimum payment); otherwise the full balance is charged.
- The invoice must be `sent`, `viewed`, `partial` or `overdue`, and card payments must be enabled for it.

**Response:** `{ "success": true, "data": { "invoice": { ... }, "transaction": { ... } } }`

### Pay by ACH

```http
POST /api/v1/public/invoices/:linkId/pay-ach
```

Body: `accountHolderName`, `routingNumber`, `accountNumber`, `accountType`, `customerType`, `businessName`, `email`, `phone`, billing address fields, `consentAccepted`, `eSignature`, optional `amount`, `customerName` and `saveAch`. `routingNumber` must be 9 digits, `accountNumber` 4–17 digits, `accountType` `checking` or `savings`, `customerType` `personal` or `business` (`businessName` required for business); `consentAccepted` and `eSignature` are required. Field errors return `400` with `error.fields`. While the ACH transaction is processing the invoice is `pending`.

### Other Pay-Link Endpoints

| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/api/v1/public/invoices/:linkId/send-receipt` | Send a receipt: `{ "channel": "email" \| "sms", "recipient": "...", "transactionId": "..." }` |
| `PATCH` | `/api/v1/public/invoices/:linkId/next-payment` | Payer moves the next automatic instalment: `{ "nextPaymentDate": "YYYY-MM-DD" }` (after tomorrow, not after `dueDate`; only when `allowRescheduleNextPayment`) |
| `POST` | `/api/v1/public/invoices/:linkId/saved-card/request-otp` | Start saved-card verification (OTP) |
| `POST` | `/api/v1/public/invoices/:linkId/saved-card/verify-otp` | Verify the OTP |
| `POST` | `/api/v1/public/invoices/:linkId/saved-card/resend-otp` | Resend the OTP |
| `POST` | `/api/v1/public/invoices/:linkId/pay-with-saved-card` | Charge a verified saved card |
| `PUT` | `/api/v1/public/invoices/:linkId/saved-card/:paymentMethodId/set-default` | Make a saved card the default |
| `DELETE` | `/api/v1/public/invoices/:linkId/saved-card/:paymentMethodId` | Remove a saved card |

The saved-card OTP endpoints are limited to 10 requests per 15 minutes.

---

## Payment Reminders

```http
POST /api/v1/invoices/:invoiceId/send-reminder
```

Sends a reminder email now. See [Sending & Delivery](./sending.md).

> **Note:** a configurable automatic reminder schedule is not available via API.

---

## Failed Payments

A declined card payment on the pay link returns `400` with `success: false` and a message such as `"Payment declined: Insufficient funds"`, and the invoice is left unchanged so the customer can try again. Nothing is recorded in `paymentHistory`.

---

## Webhooks

When the invoice has a `webhookUrl`, a successful pay-link card or ACH payment POSTs `invoice.paid` to it:

```json
{
  "event": "invoice.paid",
  "invoice": { "id": "0b8e6a3c-...", "status": "partial", "amountPaid": 250.00, "amountDue": 250.00 },
  "transaction": { "transactionId": "TXN-20261002-9C1D4E", "amount": 250.00 }
}
```

The event is also sent for partial payments — check `invoice.status`. Payments recorded with `/payment` or `/external-payment` do not send it.

---

## Best Practices

### ✅ DO:
- **Allow partial payments** - Increases payment rate
- **Preflight before charging** - when your integration decides the amount
- **Record every off-platform payment** with `/external-payment`
- **Enable reminders** - Reduce overdue invoices
- **Offer multiple methods** - Card and ACH

### ❌ DON'T:
- **Don't send card data to `/payment`** - it records, it does not charge
- **Don't record more than the balance** - `/payment` does not stop you
- **Don't record the same payment twice** - pass `transactionId`

---

## Common Scenarios

### Scenario 1: Payment Plan

Customer wants to pay a $500 invoice in 5 installments (the invoice needs `allowPartialPayment: true`):

```bash
# Payment 1, after collecting $100
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-.../payment \
  -H "Authorization: Bearer YOUR_TOKEN" -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{"amount": 100.00, "paymentMethod": "card", "transactionId": "TXN-20261001-1A2B3C"}'

# Continue until paid...
```

### Scenario 2: External Payment

Customer paid by check outside AuxCORE:

```bash
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-.../external-payment \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500.00,
    "paymentMethod": "Check",
    "paymentDate": "2026-10-01",
    "reference": "CHK-12345",
    "internalNote": "Deposited to bank"
  }'
```

---

## Next Steps

- [Create invoices →](./invoices.md)
- [Send invoices →](./sending.md)
- [Invoice appearance →](./templates.md)

---

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