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

# Overview

> Create and manage recurring billing plans backed by tokenized card payments — with configurable retry policies and webhook notifications on every cycle.

Subscription Merchant API v2.0 enables recurring billing via credit card. Create a plan, direct the customer through a **one-time card linking flow**, and Singapay automatically charges the saved card on every cycle — no customer action required after the initial setup.

<Info>
  This is a **Money In** operation. Each cycle charge transfers funds from the customer's tokenized card to your Singapay Payment Gateway account.
</Info>

<CardGroup cols={4}>
  <Card title="API Version" icon="code">
    v2.0
  </Card>

  <Card title="Base Path" icon="route">
    `/api/v2.0/recurring/plans`
  </Card>

  <Card title="Auth" icon="key">
    Bearer Token (JWT)
  </Card>

  <Card title="Channel" icon="credit-card">
    Credit Card (recurring)
  </Card>
</CardGroup>

***

## Subscription lifecycle

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Customer
    participant M as Merchant System
    participant S as SingaPay API
    participant PP as Payment Provider

    M->>S: POST /recurring/plans
    Note over M,S: Schedule, amount (or items), customer details
    S-->>M: Plan created + payment_link_url (status: pending_card_linking)

    M->>C: Redirect to payment_link_url
    C->>S: Enter card details (one-time card linking)

    S->>PP: Initial charge + card tokenization
    PP-->>S: Charge result + card token
    S->>S: Plan status → active
    S-->>M: Webhook: subscription.cycle.payment_success

    loop Each scheduled cycle
        S->>PP: Auto-charge (tokenized card, no customer action)
        PP-->>S: Charge result
        S-->>M: Webhook: payment_success or payment_failed
        Note over S,M: Retry block included on failure
    end
```

***

## Plan status lifecycle

| Status                 | Description                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| `pending_card_linking` | Plan created, awaiting customer to complete card linking via `payment_link_url`.                     |
| `pending_payment`      | Card linked, awaiting scheduled start (`schedule.start_time` is in the future).                      |
| `active`               | Subscription running — cycles auto-charged on schedule.                                              |
| `paused`               | Merchant-initiated pause from the dashboard. No charges while paused.                                |
| `suspended`            | Auto-suspended after retries exhausted (when `failed_payment_action = stop_plan`).                   |
| `cancelled`            | Cancelled by merchant or auto-cancelled on hard failure during initial card linking. Terminal state. |
| `completed`            | All scheduled cycles charged (only for plans with `total_interval` set).                             |

***

## Initial card linking — auto-cancel on hard failure

<Warning>
  When a plan is created with `charge_immediately = true` and the customer's first card-linking attempt is **rejected by the issuer or acquirer**, the platform **auto-cancels the plan** immediately.

  There is no saved card to retry against, so retrying would never succeed.
</Warning>

When an auto-cancel occurs:

* The bill for that cycle moves to `cancelled`
* The plan transitions from `pending_payment` / `pending_card_linking` directly to `cancelled`
* The active payment link is expired — the customer cannot complete a charge against the cancelled plan
* A `subscription.plan.status_changed` webhook fires with `plan.metadata.cancellation_reason = initial_linking_failed`

<Note>
  Plans created with `charge_immediately = false` are **not** affected by this rule. To re-attempt after a hard failure, create a new plan.
</Note>

***

## Amount vs. items

A plan is either **amount-only** or **itemized** — never both. This is enforced on both Create and Update.

<CardGroup cols={2}>
  <Card title="Amount-only" icon="dollar-sign">
    Send `amount`. The entire recurring charge is a single line item — simple and straightforward.
  </Card>

  <Card title="Itemized" icon="list">
    Send `items[]`. The per-cycle charge is the sum of `quantity × unit_price` across all items.
  </Card>
</CardGroup>

<Warning>
  Sending both `amount` and `items` in the same request returns a `422` validation error. On Update, the patch shape must match the plan's existing type — mixing returns `409` with response code `SP102`.
</Warning>

***

## Retry policy

When a cycle charge fails, Singapay automatically retries based on the plan's configured policy.

| Field                                | Type / Range                   | Description                                                                                                                  |
| ------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `retry_policy.max_attempts`          | integer, 1–5                   | Automatic retry attempts after the initial charge fails.                                                                     |
| `retry_policy.interval_days`         | integer, 1–7                   | Days to wait between consecutive retry attempts.                                                                             |
| `retry_policy.failed_payment_action` | `continue_plan` \| `stop_plan` | Behavior after retries exhausted. `continue_plan` keeps the subscription running on the next cycle; `stop_plan` suspends it. |

***

## Error reference

<AccordionGroup>
  <Accordion title="HTTP status codes" icon="circle-exclamation">
    | Code  | Description                                                   |
    | ----- | ------------------------------------------------------------- |
    | `200` | Success (show, update, cancel)                                |
    | `201` | Created (create plan)                                         |
    | `400` | Bad Request — validation or business rule error               |
    | `401` | Unauthorized — invalid or missing token                       |
    | `404` | Plan or account not found                                     |
    | `409` | Plan cannot be updated or already cancelled                   |
    | `422` | Validation error — missing field, amount/items conflict, etc. |
    | `500` | Internal server error                                         |
  </Accordion>

  <Accordion title="Singapay response codes" icon="code">
    | Code    | Description                                          | HTTP Status |
    | ------- | ---------------------------------------------------- | ----------- |
    | `SP000` | Success                                              | 200 / 201   |
    | `SP002` | General failure (internal error)                     | 500         |
    | `SP020` | Merchant account not found                           | 404         |
    | `SP100` | Subscription plan not found                          | 404         |
    | `SP101` | Subscription plan already cancelled                  | 409         |
    | `SP102` | Subscription plan cannot be updated in current state | 409         |
  </Accordion>
</AccordionGroup>

***

## Important notes

<AccordionGroup>
  <Accordion title="Card-only channel" icon="credit-card">
    Subscriptions run exclusively on credit card recurring. The initial card linking uses one-time 3DS; all subsequent cycle charges use the tokenized card silently — no customer action needed.
  </Accordion>

  <Accordion title="Minimum amount" icon="circle-minus">
    The per-cycle charge must clear the card channel minimum. Plans below this floor are rejected on both Create and Update with a `422` validation error.
  </Accordion>

  <Accordion title="Plan identifiers" icon="fingerprint">
    `subscription_id` is a globally unique merchant-supplied identifier across all plans for the merchant. `merchant_reff_no` is an optional, non-unique label for grouping and reporting — echoed back on every cycle webhook.
  </Accordion>

  <Accordion title="Upgrade semantics (PATCH)" icon="arrow-up-right-dots">
    A PATCH that changes `amount` or `items` triggers the **full upgrade flow** — the existing plan is closed and a new plan is created with `parent_plan_id` pointing at it, plus an optional proration and a fresh payment link.

    A PATCH that only changes `name`, `merchant_reff_no`, or `metadata` **patches the plan in place** without creating a new plan.
  </Accordion>

  <Accordion title="IP whitelisting required" icon="shield">
    All Merchant API routes require IP whitelisting. Ensure your server IP is registered for the merchant account before making any API calls.
  </Accordion>
</AccordionGroup>

***

## Available endpoints

<CardGroup cols={2}>
  <Card title="Create Recurring Plan" icon="plus" href="./create-plan">
    Register a new plan. Returns a `payment_link_url` for the customer's one-time card linking.
  </Card>

  <Card title="Get Recurring Plan" icon="eye" href="./get-plan">
    Fetch current plan state — status, schedule progress, next payment date, retry policy, and lineage.
  </Card>

  <Card title="Update or Upgrade Plan" icon="pen" href="./update-plan">
    Patch cosmetic fields in place, or change amount / items to trigger the upgrade flow with a new plan and optional proration.
  </Card>

  <Card title="Cancel Plan" icon="ban" href="./cancel-plan">
    Stop a plan immediately. Active plans go through the lifecycle service; pending plans are cancelled outright.
  </Card>
</CardGroup>

<Note>
  All endpoints require authentication. See [Authentication](/api-reference/authentication).
</Note>

***

## Changelog

| Date       | Description                                                                                                                 |
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| 2026-04-20 | Initial documentation — Subscription Merchant API v2.0 (create, show, update/upgrade, cancel) + Subscription Cycle webhook. |
