# Subscriptions

Create and manage recurring billing schedules ("recurring payments") with automatic payment processing.

---

## Overview

The Recurring API enables you to:

- 🔁 **Create recurring schedules** - Weekly, bi-weekly, semi-monthly, monthly, quarterly, annually, or a one-time scheduled charge
- 💳 **Automatic billing** - Due payments are charged by an hourly scheduler
- ⏸️ **Pause & resume** - Temporarily stop a schedule
- ⏭️ **Skip payments** - Move the next charge forward by one period
- 🔄 **Auto-retry failed payments** - Declined charges are retried automatically
- 📧 **Customer notifications** - Setup links, success, failure and dunning emails
- 📊 **Schedule & history** - Per-payment schedule rows and a full activity log

### Authentication

All `/api/v1/recurring` endpoints are admin endpoints:

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

An API key (`x-api-key`) is also accepted on these routes; the merchant is then taken from the key.

- The merchant is taken from the JWT (or API key). Tenant-level users without a merchant in their token must pass `merchantId` (in the body for writes, in the query string for reads).
- The `recurring` feature must be enabled for the tenant/merchant; otherwise the gateway answers `404 NOT_FOUND`.
- Errors use `{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "..." } }` (`400 VALIDATION_ERROR`, `404 NOT_FOUND`, `401 UNAUTHORIZED`, `500 INTERNAL_ERROR`).

> **Note:** IDs are UUIDs. Field names are camelCase and mirror the stored record (`recurringAmount`, `period`, `recurringDate1`, ...).

---

## Create a Subscription

### Endpoint

```http
POST /api/v1/recurring
```

### Request

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "recurringAmount": 29.99,
    "period": "Monthly",
    "recurringDate1": "2026-11-01",
    "nextPaymentDate": "2026-11-01",
    "maximumOccurrence": "12",
    "suggestedMode": "CARD",
    "customerId": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7f1a2c34",
    "recurringUserName": "John Doe",
    "customerEmail": "john@example.com",
    "description": "Premium membership",
    "requestOrigin": "recurring",
    "sendNotifications": true,
    "children": [
      {
        "pciToken": "pci_tok_9f8e7d6c5b4a",
        "gatewayType": "TsysSierra",
        "isDefault": true
      }
    ],
    "metadata": { "plan": "premium" }
  }'
```

### Parameters

| Field | Type | Description | Required |
|-------|------|-------------|----------|
| `recurringAmount` | decimal | Amount charged each cycle | Yes |
| `period` | string | `Weekly`, `BiWeekly`, `SemiMonthly`, `Monthly`, `Quarterly`, `Annually`, `Custom` (case-sensitive) | Yes |
| `merchantId` | uuid | Only needed when the token/key carries no merchant | Conditional |
| `recurringDate1` | date | First charge date (`YYYY-MM-DD`). Cannot be in the past. Defaults to today | No |
| `recurringDate2` | date | End date. Cannot be in the past | No |
| `recurringDate3`, `recurringDate4` | date | Extra dates for custom schedules | No |
| `nextPaymentDate` | date | Date of the first automatic charge. See note below | No |
| `maximumOccurrence` | string | Number of payments (`1`-`999`) or `NoExpire` | No |
| `suggestedMode` | string | `CARD` or `ACH` — selects how the scheduler charges | Yes, for automatic charging |
| `children` | array | Payment methods (see below). Without one, the schedule cannot be charged | No |
| `customerId` | uuid | Customer record | No |
| `recurringUserName` | string | Customer display name | No |
| `customerEmail` | string | Customer email, used for notifications | No |
| `description` | string | Description (max 255) | No |
| `branchId` | uuid | Location (branch) the schedule belongs to | No |
| `requestOrigin` | string | `recurring`, `scheduled`, `virtual_terminal`, `invoice` | No |
| `status` | string | Initial status, e.g. `pending` while waiting for customer authorization. Defaults to `active` (`scheduled` when `requestOrigin` is `scheduled`) | No |
| `planType` | string | Label, e.g. `Subscription`, `Payment Plan`, `Scheduled Payment` | No |
| `gracePeriodDays` | integer | Stored for display | No |
| `lateFeeType` / `lateFeeAmount` | string / decimal | `none`, `fixed`, `percent` — stored only, not applied to recurring charges | No |
| `planFeeType` / `planFeeAmount` | string / decimal | `none`, `fixed`, `percent` — stored only | No |
| `sendNotifications` | boolean | Email the customer on creation (needs `customerEmail`) | No |
| `templateInvoiceId` | uuid | Template invoice for recurring invoices (`requestOrigin: invoice`) | No |
| `metadata` | object | Custom key-value pairs | No |

**`children[]` (payment method):**

| Field | Type | Description |
|-------|------|-------------|
| `pciToken` | string | PCI vault card token; promoted to a long-lived card-on-file token before the schedule is saved |
| `gatewayToken` / `gatewayCustomerId` | string | Existing gateway card token / customer vault ID |
| `gatewayType` | string | Gateway the token belongs to |
| `accountToken` / `routingToken` / `accountType` | string | ACH account (`checking` or `savings`) or a saved bank-account vault token in `accountToken` |
| `isDefault` | boolean | The scheduler charges the default method |

> **Note:** If `nextPaymentDate` is omitted, the first automatic charge is scheduled **one period after** `recurringDate1` (the portal takes the first payment up front). Send `nextPaymentDate` equal to `recurringDate1` to have the scheduler take the first charge on the start date. One-time schedules (`requestOrigin: "scheduled"` or `maximumOccurrence: "1"`) are always charged on `recurringDate1`.

> **Note:** When `sendNotifications` is `true` and no payment method is supplied, AuxCORE emails the customer a setup/consent link (valid 7 days) so they can authorize a card; send `status: "pending"` in that case.

> **Note:** An identical request (same merchant, `recurringUserName`, `recurringAmount` and `period`) within 5 seconds returns the existing schedule instead of creating a duplicate.

### Response

`201 Created`

```json
{
  "success": true,
  "data": {
    "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61",
    "tenantId": "your-tenant-id",
    "merchantId": "c1a7e5d2-3b4f-4e6a-9c8d-7f0b1a2e3d45",
    "customerId": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7f1a2c34",
    "recurringAmount": 29.99,
    "period": "Monthly",
    "recurringDate1": "2026-11-01",
    "maximumOccurrence": "12",
    "suggestedMode": "CARD",
    "status": "active",
    "isRecurring": true,
    "nextPaymentDate": "2026-11-01",
    "successCount": 0,
    "failCount": 0,
    "requestOrigin": "recurring",
    "createdAt": "2026-10-01T16:00:00.000Z"
  }
}
```

---

## Subscription Periods

| Period | Cadence |
|--------|---------|
| `Weekly` | Every 7 days |
| `BiWeekly` | Every 14 days |
| `SemiMonthly` | Every 15 days |
| `Monthly` | Same day each month |
| `Quarterly` | Every 3 months |
| `Annually` | Same day each year |
| `Custom` | Used for one-time scheduled payments; if it recurs, the next charge is the same day next month |

---

## Create via Public API

Server-to-server integrations can create a schedule with an API key. The merchant is always taken from the key; any `merchantId` in the body is overwritten.

### Endpoint

```http
POST /api/v1/public/recurring
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/recurring \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recurringAmount": 29.99,
    "period": "Monthly",
    "recurringDate1": "2026-11-01",
    "nextPaymentDate": "2026-11-01",
    "suggestedMode": "CARD",
    "recurringUserName": "John Doe",
    "customerEmail": "john@example.com",
    "children": [{ "pciToken": "pci_tok_9f8e7d6c5b4a", "isDefault": true }]
  }'
```

Body and response are the same as Create a Subscription above. Authentication errors from the gateway use the public format (`{ "success": false, "error": "Invalid API key", "message": "..." }`).

---

## Get Subscription

### Endpoint

```http
GET /api/v1/recurring/:id
```

```bash
curl https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

Returns the full schedule record, including `children` (payment methods), counters (`successCount`, `failCount`, `failedPaymentCount`, `retryCount`, `totalCollectedAmount`), dates (`nextPaymentDate`, `lastPaymentDate`, `lastFailedPaymentDate`, `nextRetryAt`) and pause info (`pausedAt`, `pausedReason`). `404 NOT_FOUND` if it does not exist.

---

## List Subscriptions

### Endpoint

```http
GET /api/v1/recurring
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default `1`) |
| `perPage` | integer | Items per page (default `10`; `limit` is accepted as an alias) |
| `sort` | string | `asc` or `desc` (default `desc`) |
| `sortColumn` | string | `createdAt`, `recurringAmount`, `nextPaymentDate`, `period` (default `createdAt`) |
| `q` | string | Search by customer name |
| `customerId` | uuid | Filter by customer |
| `period` | string | Filter by period (e.g. `Monthly`) |
| `isRecurring` | boolean | `true` / `false` |
| `requestOrigin` | string | Filter by one origin |
| `requestOrigins` | string | Comma-separated list of origins |
| `branchId` | uuid | Filter by location |
| `merchantId` | uuid | Required only when the token carries no merchant |

```bash
curl "https://uat.auxcore.net/api/v1/recurring?period=Monthly&perPage=50" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "data": [ { "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61", "recurringAmount": 29.99, "period": "Monthly", "status": "active" } ],
    "total": 1,
    "page": 1,
    "perPage": 50,
    "totalPages": 1
  }
}
```

There is no `status` filter; filter on `status` client-side. Deleted schedules are excluded.

---

## Subscription Overview

Counts for dashboard badges.

### Endpoint

```http
GET /api/v1/recurring/overview
```

**Response:**
```json
{
  "success": true,
  "data": { "recurring": 42, "scheduled": 5, "failed": 3 }
}
```

`failed` counts schedules with at least one failed payment.

---

## Update Subscription

### Endpoint

```http
PUT /api/v1/recurring/:id
```

```bash
curl -X PUT https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "recurringAmount": 39.99,
    "description": "Premium plus"
  }'
```

**Updatable Fields:** `recurringAmount`, `period`, `recurringDate1`-`recurringDate4`, `maximumOccurrence`, `suggestedMode`, `isRecurring`, `customerId`, `recurringUserName`, `customerEmail`, `status`, `children`, `description`, `gracePeriodDays`, `lateFeeType`, `lateFeeAmount`, `planType`, `planFeeType`, `planFeeAmount`, `setupLinkId`.

The same date and `maximumOccurrence` validation as create applies. Changing only the amount updates the unpaid schedule rows in place; changing the period rebuilds them. Paid rows are kept.

---

## Pause Subscription

Temporarily pause a subscription without cancelling it. Cancelled or expired schedules cannot be paused (`400`).

### Endpoint

```http
POST /api/v1/recurring/:id/pause
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/pause \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer travelling" }'
```

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61",
    "status": "paused",
    "isRecurring": false,
    "pausedAt": "2026-10-01T16:30:00.000Z",
    "pausedReason": "Customer travelling"
  },
  "message": "Recurring subscription paused successfully"
}
```

---

## Resume Subscription

Resume a `paused`, `pending` or `cancelled` schedule. The status returns to `active` and `nextPaymentDate` is recalculated as one period from today. Any other status returns `400`.

### Endpoint

```http
POST /api/v1/recurring/:id/resume
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/resume \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

---

## Skip Next Payment

Move the next charge forward by one period. Only `active` schedules can skip (`400` otherwise). No request body.

### Endpoint

```http
POST /api/v1/recurring/:id/skip-next
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/skip-next \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61",
    "status": "active",
    "nextPaymentDate": "2026-12-01"
  },
  "message": "Next payment skipped successfully"
}
```

---

## Cancel Subscription

Cancel immediately. Future schedule rows are marked `cancelled`; the record stays visible. Cancelling an already cancelled schedule returns `400`. There is no "cancel at period end" option.

### Endpoint

```http
POST /api/v1/recurring/:id/cancel
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/cancel \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer request" }'
```

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61",
    "status": "cancelled",
    "isRecurring": false,
    "pausedReason": "Customer request"
  },
  "message": "Recurring subscription cancelled successfully"
}
```

---

## Delete Subscription

Soft-deletes the schedule (hidden from lists, never charged again).

### Endpoint

```http
DELETE /api/v1/recurring/:id
```

```bash
curl -X DELETE https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{ "success": true, "data": { "message": "Recurring payment deleted successfully" } }
```

---

## Subscription Statuses

| Status | Description |
|--------|-------------|
| `active` | Billing on schedule |
| `scheduled` | One-time scheduled payment waiting for its date |
| `pending` | Waiting for the customer to authorize a payment method (setup link) |
| `processing` | A charge is in progress (short-lived lock) |
| `paused` | Paused by a user, or automatically after retries were exhausted |
| `cancelled` | Cancelled — no future payments |
| `completed` | All planned payments collected |
| `expired` | Ended by date |

---

## Charge Now

Charge the next payment immediately. Fails with `400` for `pending` schedules, schedules without a default payment method or `suggestedMode`, a charge already in progress, or a charge attempted in the last 60 seconds. A decline returns `400 PROCESSING_ERROR`.

### Endpoint

```http
POST /api/v1/recurring/:id/charge-now
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/charge-now \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": { "success": true, "transactionId": "5b9e2d7a-1c3f-4a8e-b6d0-2f4c8a1e7b93" },
  "message": "Payment processed successfully"
}
```

---

## Payment History

Summary of collected and failed payments.

### Endpoint

```http
GET /api/v1/recurring/:id/payment-history
```

```bash
curl https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/payment-history \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "recurring": { "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61", "status": "active" },
    "totalSuccessful": 3,
    "totalFailed": 1,
    "lastSuccessfulPayment": "2026-10-01",
    "lastFailedPayment": "2026-09-01",
    "nextPayment": "2026-11-01",
    "status": "active"
  }
}
```

For per-payment rows use [Get Payment Schedule](./billing-cycles.md); for the activity log use `GET /api/v1/recurring/:id/history`.

---

## Activity History

Audit log of the schedule (created, updated, paused, resumed, cancelled, payment_succeeded, payment_failed, retry_scheduled, skipped, payment_method_linked, ...).

### Endpoint

```http
GET /api/v1/recurring/:id/history
```

```bash
curl https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/history \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

---

## Link Payment Method

Attach a new card to the schedule. A `pciToken` is promoted to a long-lived card-on-file token and becomes the default method. One of `pciToken` or `paymentMethodId` is required (`400` otherwise).

### Endpoint

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

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/link-payment-method \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{
    "pciToken": "pci_tok_1a2b3c4d5e6f",
    "gatewayType": "TsysSierra",
    "last4": "4242",
    "brand": "visa",
    "isDefault": true
  }'
```

| Field | Type | Description |
|-------|------|-------------|
| `pciToken` | string | PCI vault card token |
| `paymentMethodId` | uuid | Saved customer payment method (vault reference) |
| `gatewayType` | string | Defaults to `TsysSierra` |
| `last4`, `brand` | string | Display details for the activity log |
| `isDefault` | boolean | Defaults to `true` |

---

## Send Payment Method Update Link

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

### Endpoint

```http
POST /api/v1/recurring/: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=recurring&id=8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61"
  }
}
```

---

## Resend Authorization Link

Re-send the setup/consent link for a `pending` schedule (`400` for any other status). The new link is valid for 7 days.

### Endpoint

```http
POST /api/v1/recurring/:id/resend-auth
```

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

---

## Auto-Retry Logic

When a scheduled charge is declined, AuxCORE retries it automatically:

- Each schedule allows `maxRetryAttempts` retries (default **3**).
- After a decline, the next retry is scheduled **24 hours** later (`nextRetryAt`). Due charges are processed hourly and retries are also swept every 6 hours.
- A successful charge resets the failure counters and moves `nextPaymentDate` to the next period.
- When the retries are exhausted the schedule is set to `paused` (`pausedReason`: "Payment failed after 3 retry attempts") and a dunning email is sent if notifications are enabled.

[Learn more about dunning →](./dunning.md)

---

## Notifications

AuxCORE does not emit subscription-specific webhook events. Each charge creates an ordinary transaction (with `requestOrigin: "recurring"`), and customer emails are sent by the platform:

| Email | When |
|-------|------|
| Recurring created / setup link | On creation with `sendNotifications: true` |
| Payment successful | After each successful automatic charge |
| Payment failed | After each failed automatic charge |
| Dunning notice | When retries are exhausted (can be turned off per merchant — see [Dunning](./dunning.md)) |

---

## Best Practices

### ✅ DO:
- **Tokenize the card first** - Pass a `pciToken`, never raw card data
- **Set `suggestedMode`** - The scheduler cannot charge without it
- **Send `nextPaymentDate`** - When the first charge should happen on the start date
- **Use the setup link** - Create with `status: "pending"` when the customer must authorize the card
- **Watch the overview** - Follow up on schedules with failed payments

### ❌ DON'T:
- **Don't create duplicate subscriptions** - Search by `customerId` first
- **Don't cancel when a pause will do** - Paused schedules can be resumed
- **Don't ignore paused schedules** - A schedule paused after failed retries needs a new card

---

## Next Steps

- [Dunning Management →](./dunning.md)
- [Billing Cycles →](./billing-cycles.md)
- [Set up webhooks →](../getting-started/webhooks.md)

---

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