> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Purchase lifecycle management

> Cancel, reactivate, activate plans, and switch between plans using the TypeScript SDK.

## Overview

After a customer completes checkout, the SDK provides helpers to manage the full purchase lifecycle:

* **Cancel renewal** — stop auto-renewal; access continues until the period ends
* **Reactivate renewal** — undo a pending cancellation while still in the active period
* **Activate plan** — activate a product for a customer on a specific plan without checkout (free units or credit balance)
* **Plan switching** — activating a different plan automatically expires the current purchase and creates a new one

All helpers follow the same pattern: pass the incoming `Request` object and a body, get back either a data object or a `NextResponse` error.

## Cancel renewal

Cancels auto-renewal on a purchase. The customer keeps access until the current billing period ends.

<CodeGroup>
  ```typescript Next.js helper theme={null}
  import { cancelRenewal } from '@solvapay/next'

  export async function POST(request: NextRequest) {
    const { purchaseRef, reason } = await request.json()
    const result = await cancelRenewal(request, { purchaseRef, reason })
    return result instanceof NextResponse ? result : NextResponse.json(result)
  }
  ```

  ```typescript Server SDK theme={null}
  import { cancelPurchaseCore } from '@solvapay/server'

  const result = await cancelPurchaseCore(request, {
    purchaseRef: 'pur_...',
    reason: 'Too expensive',
  })
  ```
</CodeGroup>

| Parameter     | Type     | Required | Description                      |
| ------------- | -------- | -------- | -------------------------------- |
| `purchaseRef` | `string` | Yes      | The purchase reference to cancel |
| `reason`      | `string` | No       | Optional cancellation reason     |

The purchase remains `active` with `cancelledAt` set. When the billing period ends, the purchase transitions to `expired`.

## Reactivate renewal

Undoes a pending cancellation, restoring auto-renewal. Only works while the purchase is still active (before the period ends).

<CodeGroup>
  ```typescript Next.js helper theme={null}
  import { reactivateRenewal } from '@solvapay/next'

  export async function POST(request: NextRequest) {
    const { purchaseRef } = await request.json()
    const result = await reactivateRenewal(request, { purchaseRef })
    return result instanceof NextResponse ? result : NextResponse.json(result)
  }
  ```

  ```typescript Server SDK theme={null}
  import { reactivatePurchaseCore } from '@solvapay/server'

  const result = await reactivatePurchaseCore(request, {
    purchaseRef: 'pur_...',
  })
  ```
</CodeGroup>

| Parameter     | Type     | Required | Description                          |
| ------------- | -------- | -------- | ------------------------------------ |
| `purchaseRef` | `string` | Yes      | The purchase reference to reactivate |

**Preconditions:**

* Purchase status must be `active`
* `cancelledAt` must be set (cancellation is pending)
* `endDate` must not have passed

On success, `cancelledAt` is cleared and `autoRenew` is restored. A `purchase.updated` webhook fires.

## Activate plan

Activates a product for a customer on a specific plan without going through checkout. Useful for free plans, credit-based activation, or programmatic plan assignment.

Usage-based plans (metered, no billing cycle) activate immediately. A zero balance is not an activation gate — empty-wallet access is enforced later as `topup_required` at usage time. Free plans activate immediately. Paid recurring, hybrid, or one-time plans return `payment_required` so the customer can complete checkout.

<CodeGroup>
  ```typescript Next.js helper theme={null}
  import { activatePlan } from '@solvapay/next'

  export async function POST(request: NextRequest) {
    const { productRef, planRef } = await request.json()
    const result = await activatePlan(request, { productRef, planRef })
    return result instanceof NextResponse ? result : NextResponse.json(result)
  }
  ```

  ```typescript Server SDK theme={null}
  import { activatePlanCore } from '@solvapay/server'

  const result = await activatePlanCore(request, {
    productRef: 'prd_...',
    planRef: 'pln_...',
  })
  ```
</CodeGroup>

| Parameter    | Type     | Required | Description             |
| ------------ | -------- | -------- | ----------------------- |
| `productRef` | `string` | Yes      | The product to activate |
| `planRef`    | `string` | Yes      | The plan to activate on |

### Response statuses

| Status             | Meaning                                                                              |
| ------------------ | ------------------------------------------------------------------------------------ |
| `activated`        | Purchase created successfully                                                        |
| `already_active`   | Customer already has an active purchase on this plan                                 |
| `topup_required`   | Customer needs more credits — response includes `creditBalance` and `creditsPerUnit` |
| `payment_required` | Plan requires payment — response includes `checkoutUrl` and `checkoutSessionId`      |
| `invalid`          | Invalid product or plan reference                                                    |

```typescript theme={null}
const result = await activatePlan(request, { productRef, planRef })

if (!(result instanceof NextResponse)) {
  switch (result.status) {
    case 'activated':
      // Purchase created — result.purchaseRef is available
      break
    case 'topup_required':
      // Show top-up UI with result.creditBalance and result.creditsPerUnit
      break
    case 'payment_required':
      // Redirect to result.checkoutUrl
      break
  }
}
```

## Plan switching

When a customer already has an active purchase on a product and you call `activatePlan` with a **different** `planRef`, the SDK automatically:

1. Expires the existing purchase (fires `purchase.expired` webhook)
2. Creates a new purchase on the requested plan (fires `purchase.created` webhook)

No special API call is needed — `activatePlan` handles the switch.

```typescript theme={null}
// Customer is on pln_basic, switch to pln_pro
const result = await activatePlan(request, {
  productRef: 'prd_myapi',
  planRef: 'pln_pro',
})
// result.status === 'activated' if the switch succeeded
```

<Warning>
  Plan switching immediately expires the old purchase. If the old plan was a paid recurring plan, consider whether you need to handle proration or credits on your side.
</Warning>

## Automatic top-ups (auto-recharge)

When `activatePlan` returns `topup_required`, the customer is out of credits and
needs to buy more before they can continue. Two things belong on that surface:

1. A **top-up** UI so they can add credits now.
2. An optional **auto-recharge** toggle so the balance refills automatically next time.

The customer-facing components (`TopupForm`, `AutoRecharge`) and hooks
(`useTopup`, `useAutoRecharge`) live in `@solvapay/react` — see
[Credit Top-Ups & Auto-Recharge](/sdks/typescript/guides/react#credit-top-ups--auto-recharge)
in the React guide. This section covers the server-side helpers that back them; the dedicated
[Auto-recharge guide](/sdks/typescript/guides/auto-recharge) walks through the full stack,
including the monthly spend cap and the `@solvapay/server` 2.0.0 migration.

### Configuration helpers

Auto-recharge config is read and written through three helpers, exposed as
`GET` / `PUT` / `DELETE` on a single `/api/auto-recharge` route:

<CodeGroup>
  ```typescript Next.js helper theme={null}
  import { disableAutoRecharge, getAutoRecharge, saveAutoRecharge } from '@solvapay/next'

  export const GET = (request: NextRequest) => getAutoRecharge(request)
  export const PUT = (request: NextRequest) => saveAutoRecharge(request)
  export const DELETE = (request: NextRequest) => disableAutoRecharge(request)
  ```

  ```typescript Server SDK theme={null}
  import {
    getAutoRechargeCore,
    saveAutoRechargeCore,
    disableAutoRechargeCore,
  } from '@solvapay/server'

  const { config, display } = await getAutoRechargeCore(request, { customerRef })
  ```
</CodeGroup>

### Top-up payment helpers

Top-ups reuse the payment-intent flow. `createTopupPaymentIntent` accepts an
optional `autoRecharge` payload so auto-recharge can be armed in the **same**
charge as the initial card payment (no separate card-setup step), and
`processTopupPaymentIntent` confirms the credit landed:

<CodeGroup>
  ```typescript Next.js helper theme={null}
  import { createTopupPaymentIntent, processTopupPaymentIntent } from '@solvapay/next'

  // POST /api/create-topup-payment-intent
  export async function POST(request: NextRequest) {
    const { amount, currency, autoRecharge } = await request.json()
    return createTopupPaymentIntent(request, { amount, currency, autoRecharge })
  }

  // POST /api/process-topup-payment
  export async function POST(request: NextRequest) {
    const { paymentIntentId } = await request.json()
    return processTopupPaymentIntent(request, { paymentIntentId })
  }
  ```

  ```typescript Server SDK theme={null}
  import { createTopupPaymentIntentCore, processTopupPaymentIntentCore } from '@solvapay/server'

  const intent = await createTopupPaymentIntentCore(request, {
    amount: 2000, // minor units
    currency: 'USD',
    autoRecharge, // optional AutoRechargeInput
  })
  ```
</CodeGroup>

### AutoRechargeInput parameters

`saveAutoRecharge` (and the `autoRecharge` field on `createTopupPaymentIntent`)
accept an `AutoRechargeInput`:

| Parameter              | Type        | Required     | Description                                                                                                       |
| ---------------------- | ----------- | ------------ | ----------------------------------------------------------------------------------------------------------------- |
| `enabled`              | `boolean`   | Yes          | Whether auto-recharge is on                                                                                       |
| `triggerType`          | `'balance'` | Yes          | Only balance-based triggering is supported                                                                        |
| `thresholdAmountMajor` | `number`    | When enabled | Balance level (display-currency major units) that triggers a recharge. Must be `> 0`                              |
| `topupAmountMajor`     | `number`    | When enabled | Fixed recharge amount. Must be `>= thresholdAmountMajor`                                                          |
| `maxMonthlySpendMajor` | `number`    | No           | Optional monthly spend cap in display-currency major units. Resets each UTC calendar month; blank means unlimited |
| `currency`             | `string`    | Yes          | ISO 4217 currency, must be a supported top-up currency                                                            |

`SaveAutoRechargeInput` also accepts `deferSetupIntent?: boolean` — set it to
stage the config without creating a SetupIntent (used by the combined top-up +
auto-recharge flow). Validation enforces both amounts `<= 10,000` major units and
a per-currency Stripe minimum on the top-up amount.

### Routes to wire

| Route                                   | Helper                                                         | Purpose                                                            |
| --------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
| `GET/PUT/DELETE /api/auto-recharge`     | `getAutoRecharge` / `saveAutoRecharge` / `disableAutoRecharge` | Read, set, and disable the config                                  |
| `POST /api/create-topup-payment-intent` | `createTopupPaymentIntent`                                     | Create the top-up payment intent (optionally arming auto-recharge) |
| `POST /api/process-topup-payment`       | `processTopupPaymentIntent`                                    | Confirm the credit landed after Stripe confirmation                |

### Behavior notes

* **Credits mint on charge success.** Credits are booked when the off-session charge succeeds; the webhook is an idempotent backstop, not the primary path.
* **Deferred setup stages the config.** With `deferSetupIntent` / `deferCardSetup`, the config is `pending_setup` and is **not** armed until the card is saved on the top-up charge and the status flips to `active`.
* **Live FX, no stored credit threshold.** The threshold is stored in display-currency minor units and re-resolved to credits at trigger time — there's no persisted `thresholdCredits`.
* **Monthly spend cap.** An optional `maxMonthlySpendMajor` limits how much auto-recharge spend is allowed per UTC calendar month. The config stays `active` when the cap is hit — charges resume automatically next month. The `<AutoRecharge>` component exposes the cap under **Advanced** and shows current-period spend on the summary card; read `monthlySpendMinor` / `monthlySpendPeriod` on the config when building a custom UI.

<Warning>
  `autoRecharge.triggered: true` on a usage/debit response means an off-session
  charge was **initiated** — it does not mean credits were booked inline in that
  same response. Off-session **declines are delivered via the
  `customer.credit.auto_topup_failed` webhook**, not in the usage/debit response.
  Pair the webhook with `useAutoRecharge().config.status === 'failed'` to prompt
  the customer to update their card.
</Warning>

## Complete Next.js example

```
app/api/
├── cancel-renewal/route.ts
├── reactivate-renewal/route.ts
├── activate-plan/route.ts
├── auto-recharge/route.ts
├── create-topup-payment-intent/route.ts
├── process-topup-payment/route.ts
└── ...
```

```typescript theme={null}
// app/api/cancel-renewal/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { cancelRenewal } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { purchaseRef, reason } = await request.json()
  const result = await cancelRenewal(request, { purchaseRef, reason })
  return result instanceof NextResponse ? result : NextResponse.json(result)
}
```

```typescript theme={null}
// app/api/reactivate-renewal/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { reactivateRenewal } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { purchaseRef } = await request.json()
  const result = await reactivateRenewal(request, { purchaseRef })
  return result instanceof NextResponse ? result : NextResponse.json(result)
}
```

```typescript theme={null}
// app/api/activate-plan/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { activatePlan } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { productRef, planRef } = await request.json()
  const result = await activatePlan(request, { productRef, planRef })
  return result instanceof NextResponse ? result : NextResponse.json(result)
}
```

```typescript theme={null}
// app/api/auto-recharge/route.ts
import { NextRequest } from 'next/server'
import { disableAutoRecharge, getAutoRecharge, saveAutoRecharge } from '@solvapay/next'

export const GET = (request: NextRequest) => getAutoRecharge(request)
export const PUT = (request: NextRequest) => saveAutoRecharge(request)
export const DELETE = (request: NextRequest) => disableAutoRecharge(request)
```

```typescript theme={null}
// app/api/create-topup-payment-intent/route.ts
import { NextRequest } from 'next/server'
import { createTopupPaymentIntent } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { amount, currency, autoRecharge } = await request.json()
  return createTopupPaymentIntent(request, { amount, currency, autoRecharge })
}
```

```typescript theme={null}
// app/api/process-topup-payment/route.ts
import { NextRequest } from 'next/server'
import { processTopupPaymentIntent } from '@solvapay/next'

export async function POST(request: NextRequest) {
  const { paymentIntentId } = await request.json()
  return processTopupPaymentIntent(request, { paymentIntentId })
}
```

## Next steps

* [Auto-recharge](/sdks/typescript/guides/auto-recharge) — the full auto-recharge stack, monthly spend cap semantics, and the 2.0.0 migration
* [Credit Top-Ups & Auto-Recharge](/sdks/typescript/guides/react#credit-top-ups--auto-recharge) — the React components and hooks (`TopupForm`, `AutoRecharge`, `useTopup`, `useAutoRecharge`) that drive the top-up UI
* [Webhooks](/webhooks) — handle `purchase.updated`, `purchase.expired`, and `purchase.created` events from lifecycle changes, plus `customer.credit.auto_topup_failed` for off-session declines
* [Billing](/plans/billing) — understand billing cycles, renewal processing, and purchase states
* [Next.js guide](/sdks/typescript/guides/nextjs) — full Next.js integration walkthrough
