---
updatedAt: 2026-08-13T16:03:40.000Z
agentTools:
  projectIndex: https://docs.moneyhash.io/llms.txt
---

# Subscription Plans

Subscription Plans let customers subscribe to recurring payment plans managed through MoneyHash. Your frontend retrieves the available plans with the SDK, presents them to the customer, and subscribes them to the selected plan - which returns a payment intent to complete the first billing cycle. From then on, MoneyHash bills the remaining cycles automatically. This page covers the full flow on every platform - Web (JavaScript), iOS, Android, Flutter, and React Native.

## How it works

You configure **plan groups** in the MoneyHash dashboard - each group holds plans that share a currency, and each plan carries its own billing interval, trial period, discounts, and cycle count. Your app's job is exactly three calls: fetch the plans for a customer, render your pricing UI, and subscribe the pick. The subscription's **first cycle is a normal payment intent** - you complete it with the standard payment flow you already have - and every later cycle is billed by MoneyHash without your code involved.

<Image src="https://files.readme.io/2d676bfedea9e71164a879ce4ca1ca14fea75155f76b04272f52ea1b15798e16-advanced-topics_subscription-plans_overview_diagram.svg" align="center" width="85%" />

Once created, a subscription moves through a lifecycle your backend should track via webhooks:

<Image src="https://files.readme.io/1f717f69a8a7424a985e783c9a6d582cc8dfb5cc906d831a5b2fc6db90b6c3bc-advanced-topics_subscription-plans_lifecycle_diagram.svg" align="center" width="80%" />

***

## Prerequisites

* SDK initialized with your account's **public API key**
* Subscription plans and plan groups configured in the MoneyHash dashboard under **Plan Groups**
* A **MoneyHash customer** must exist - subscription plans are always associated with a customer record

## Type reference

The same shapes on every platform - the mobile models mirror these fields.

**SubscriptionPlan**

| Field                | Type           | Description                                              |
| -------------------- | -------------- | -------------------------------------------------------- |
| `id`                 | string         | Unique plan identifier                                   |
| `name`               | string         | Plan display name                                        |
| `description`        | string         | Plan description                                         |
| `amount`             | number         | Recurring payment amount                                 |
| `currency`           | string         | ISO 4217 currency code                                   |
| `recurrency`         | number         | Billing interval - number of units                       |
| `recurrencyUnit`     | `"MONTH"`      | Billing interval unit                                    |
| `recurringCycles`    | number \| null | Total number of billing cycles. `null` for indefinite    |
| `trialPeriod`        | number \| null | Trial period in days. `null` if no trial                 |
| `oneTimeFee`         | number \| null | One-time fee charged at signup. `null` if none           |
| `discountAmount`     | number \| null | Fixed discount amount applied                            |
| `discountPercentage` | number \| null | Percentage discount applied                              |
| `discountCycles`     | number \| null | Number of cycles the discount applies to                 |
| `alreadySubscribed`  | boolean        | Whether this customer is already subscribed to this plan |
| `isLive`             | boolean        | Whether the plan is in live mode                         |
| `created`            | string         | ISO 8601 creation timestamp                              |

**SubscriptionPlanGroup**

| Field      | Type                | Description                      |
| ---------- | ------------------- | -------------------------------- |
| `id`       | string              | Unique plan group identifier     |
| `name`     | string              | Plan group display name          |
| `currency` | string              | Currency for plans in this group |
| `plans`    | SubscriptionPlan\[] | List of plans within this group  |
| `created`  | string              | ISO 8601 creation timestamp      |

**SubscriptionStatus** - the status of a customer's subscription:

| Status                 | Description                                             |
| ---------------------- | ------------------------------------------------------- |
| `NEW`                  | Subscription created, not yet active                    |
| `TRIAL`                | Customer is in the trial period                         |
| `INCOMPLETE`           | First payment not yet completed                         |
| `ACTIVE`               | Subscription is active and billing normally             |
| `PAST_DUE`             | Payment failed - subscription at risk                   |
| `PENDING_CANCELLATION` | Cancellation requested, still active until end of cycle |
| `CANCELLED`            | Subscription cancelled                                  |
| `ENDED`                | All recurring cycles completed                          |
| `TERMINATED`           | Subscription terminated early                           |
| `PAUSED`               | Subscription temporarily paused                         |

## Step 1 - Retrieve subscription plans for a customer

Get the list of available plans within a plan group for a specific customer. The `alreadySubscribed` flag on each plan tells you whether the customer is already on it - use it to disable or highlight those plans in your UI.

```javascript JavaScript
const subscriptionPlans = await moneyHash.getSubscriptionPlans({
  planGroupId: "<plan-group-id>",
  customerId: "<customer-id>",
});

// SubscriptionPlan[]
```
```swift iOS
let subscriptionPlans = try await moneyHash.getSubscriptionPlans(
    planGroupId: "<plan-group-id>",
    customerId: "<customer-id>"
)

// [SubscriptionPlan]
```
```kotlin Android
val subscriptionPlans = moneyHash.getSubscriptionPlans(
    planGroupId = "<plan-group-id>",
    customerId = "<customer-id>"
)

// List<SubscriptionPlan>
```
```dart Flutter
final subscriptionPlans = await moneyHash.getSubscriptionPlans(
  "<plan-group-id>",
  "<customer-id>",
);

// List<SubscriptionPlan>
```
```typescript React Native
const subscriptionPlans = await moneyHash.getSubscriptionPlans({
  planGroupId: "<plan-group-id>",
  customerId: "<customer-id>",
});

// SubscriptionPlan[]
```

<HTMLBlock>{`
<style>
  .mh-callout { border-radius: 0 8px 8px 0; padding: 14px 16px; margin-bottom: 16px; }
  .mh-callout .mh-title { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  .mh-callout .mh-title span { font-size: 13px; font-weight: 500; }
  .mh-callout p { font-size: 13px; margin: 0; }
  .mh-callout code { font-size: 12px; }
  .mh-info { background: #E6F1FB; border-left: 3px solid #378ADD; }
  .mh-info .mh-title span, .mh-info p, .mh-info code { color: #0C447C; }
  @media (prefers-color-scheme: dark) {
    .mh-info { background: #042C53; border-left-color: #378ADD; }
    .mh-info .mh-title span, .mh-info p, .mh-info code { color: #B5D4F4; }
  }
  [data-color-mode="dark"] .mh-info { background: #042C53; border-left-color: #378ADD; }
  [data-color-mode="dark"] .mh-info .mh-title span, [data-color-mode="dark"] .mh-info p, [data-color-mode="dark"] .mh-info code { color: #B5D4F4; }
</style>
<div class="mh-callout mh-info">
  <div class="mh-title">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2ZM8 7V11M8 5.5V5.49333" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    <span><strong>Note</strong></span>
  </div>
  <p>The <code>planGroupId</code> is found in the MoneyHash dashboard under <strong>Plan Groups</strong>. Each plan group contains plans with the same currency and billing configuration.</p>
</div>
`}</HTMLBlock>

## Step 2 - Subscribe a customer to a plan

Once the customer selects a plan, call `selectSubscriptionPlan` to subscribe them. It returns a **payment intent** - proceed through the standard payment flow to complete the first billing cycle.

```javascript JavaScript
const intentDetails = await moneyHash.selectSubscriptionPlan({
  planGroupId: "<plan-group-id>",
  customerId: "<customer-id>",
  planId: "<selected-plan-id>",
});

// intentDetails contains the payment intent —
// proceed with the standard payment flow from here
```
```swift iOS
let intentDetails = try await moneyHash.selectSubscriptionPlan(
    planGroupId: "<plan-group-id>",
    customerId: "<customer-id>",
    planId: "<selected-plan-id>"
)

// intentDetails contains the payment intent —
// proceed with the standard payment flow from here
```
```kotlin Android
val intentDetails = moneyHash.selectSubscriptionPlan(
    planGroupId = "<plan-group-id>",
    customerId = "<customer-id>",
    planId = "<selected-plan-id>"
)

// intentDetails contains the payment intent —
// proceed with the standard payment flow from here
```
```dart Flutter
final intentDetails = await moneyHash.selectSubscriptionPlan(
  "<plan-group-id>",
  "<customer-id>",
  "<selected-plan-id>",
);

// intentDetails contains the payment intent —
// proceed with the standard payment flow from here
```
```typescript React Native
const intentDetails = await moneyHash.selectSubscriptionPlan({
  planGroupId: "<plan-group-id>",
  customerId: "<customer-id>",
  planId: "<selected-plan-id>",
});

// intentDetails contains the payment intent —
// proceed with the standard payment flow from here
```

<HTMLBlock>{`
<style>
  .mh-callout { border-radius: 0 8px 8px 0; padding: 14px 16px; margin-bottom: 16px; }
  .mh-callout .mh-title { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  .mh-callout .mh-title span { font-size: 13px; font-weight: 500; }
  .mh-callout p { font-size: 13px; margin: 0; }
  .mh-callout code { font-size: 12px; }
  .mh-callout a { color: inherit; text-decoration: underline; }
  .mh-info { background: #E6F1FB; border-left: 3px solid #378ADD; }
  .mh-info .mh-title span, .mh-info p, .mh-info code { color: #0C447C; }
  .mh-info a { color: #0C447C; }
  @media (prefers-color-scheme: dark) {
    .mh-info { background: #042C53; border-left-color: #378ADD; }
    .mh-info .mh-title span, .mh-info p, .mh-info code { color: #B5D4F4; }
    .mh-info a { color: #B5D4F4; }
  }
  [data-color-mode="dark"] .mh-info { background: #042C53; border-left-color: #378ADD; }
  [data-color-mode="dark"] .mh-info .mh-title span, [data-color-mode="dark"] .mh-info p, [data-color-mode="dark"] .mh-info code { color: #B5D4F4; }
  [data-color-mode="dark"] .mh-info a { color: #B5D4F4; }
</style>
<div class="mh-callout mh-info">
  <div class="mh-title">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2ZM8 7V11M8 5.5V5.49333" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    <span><strong>Note</strong></span>
  </div>
  <p><code>selectSubscriptionPlan</code> returns a payment intent to complete the first payment cycle. Handle it the same way as a regular payment intent - collect card details, submit, and handle webhooks. See <a href="/docs/sdk-architecture">SDK Architecture</a> and <a href="/docs/build-card-form">Build Your Own Card Form</a> for the full payment flow.</p>
</div>
`}</HTMLBlock>

<HTMLBlock>{`
<style>
  .mh-callout { border-radius: 0 8px 8px 0; padding: 14px 16px; margin-bottom: 16px; }
  .mh-callout .mh-title { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  .mh-callout .mh-title span { font-size: 13px; font-weight: 500; }
  .mh-callout p { font-size: 13px; margin: 0; }
  .mh-callout code { font-size: 12px; }
  .mh-info { background: #E6F1FB; border-left: 3px solid #378ADD; }
  .mh-info .mh-title span, .mh-info p, .mh-info code { color: #0C447C; }
  @media (prefers-color-scheme: dark) {
    .mh-info { background: #042C53; border-left-color: #378ADD; }
    .mh-info .mh-title span, .mh-info p, .mh-info code { color: #B5D4F4; }
  }
  [data-color-mode="dark"] .mh-info { background: #042C53; border-left-color: #378ADD; }
  [data-color-mode="dark"] .mh-info .mh-title span, [data-color-mode="dark"] .mh-info p, [data-color-mode="dark"] .mh-info code { color: #B5D4F4; }
</style>
<div class="mh-callout mh-info">
  <div class="mh-title">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2ZM8 7V11M8 5.5V5.49333" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    <span><strong>Note</strong></span>
  </div>
  <p>The intent details of a subscription payment also carry <code>subscriptionInfo</code> - the subscription's <code>id</code>, its <code>plan</code>, and its current <code>status</code> (the lifecycle values above). Read it from any <code>IntentDetails</code> (including <code>getIntentDetails</code>) to show the subscription state in-app; your backend should still confirm via webhooks.</p>
</div>
`}</HTMLBlock>

***

## Retrieve plan groups

To let customers browse the available subscription tiers, use `getSubscriptionPlanGroups`. Results can be filtered by currency and paginated - the response carries `planGroups`, `count`, `hasNext`, and `hasPrevious`.

```javascript JavaScript
// All plan groups
const { planGroups, count, hasNext, hasPrevious } =
  await moneyHash.getSubscriptionPlanGroups();

// Filter by currency
const { planGroups } = await moneyHash.getSubscriptionPlanGroups({
  currency: "AED",
});

// With pagination
const { planGroups, hasNext, hasPrevious } =
  await moneyHash.getSubscriptionPlanGroups({ offset: 0, limit: 10 });
```
```swift iOS
let response = try await moneyHash.getSubscriptionPlanGroups(
    limit: 10,
    offset: 0,
    currency: "AED" // optional filter
)

// response.planGroups · response.count
// response.hasNext · response.hasPrevious
```
```kotlin Android
val response = moneyHash.getSubscriptionPlanGroups(
    limit = 10,
    offset = 0,
    currency = "AED" // optional filter
)

// response.planGroups · response.count
// response.hasNext · response.hasPrevious
```
```dart Flutter
final response = await moneyHash.getSubscriptionPlanGroups(
  10,     // limit (default 10)
  0,      // offset
  "AED",  // currency — optional filter
);

// response.planGroups · response.count
// response.hasNext · response.hasPrevious
```
```typescript React Native
const response = await moneyHash.getSubscriptionPlanGroups({
  limit: 10,
  offset: 0,
  currency: "AED", // optional filter
});

// response.planGroups · response.count
// response.hasNext · response.hasPrevious
```

<HTMLBlock>{`
<style>
  .mh-callout { border-radius: 0 8px 8px 0; padding: 14px 16px; margin-bottom: 16px; }
  .mh-callout .mh-title { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  .mh-callout .mh-title span { font-size: 13px; font-weight: 500; }
  .mh-callout p { font-size: 13px; margin: 0; }
  .mh-callout code { font-size: 12px; }
  .mh-info { background: #E6F1FB; border-left: 3px solid #378ADD; }
  .mh-info .mh-title span, .mh-info p, .mh-info code { color: #0C447C; }
  @media (prefers-color-scheme: dark) {
    .mh-info { background: #042C53; border-left-color: #378ADD; }
    .mh-info .mh-title span, .mh-info p, .mh-info code { color: #B5D4F4; }
  }
  [data-color-mode="dark"] .mh-info { background: #042C53; border-left-color: #378ADD; }
  [data-color-mode="dark"] .mh-info .mh-title span, [data-color-mode="dark"] .mh-info p, [data-color-mode="dark"] .mh-info code { color: #B5D4F4; }
</style>
<div class="mh-callout mh-info">
  <div class="mh-title">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2ZM8 7V11M8 5.5V5.49333" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    <span><strong>Note</strong></span>
  </div>
  <p>The default pagination limit is 10. Use <code>offset</code> and <code>limit</code> to control the page size, and <code>hasNext</code> / <code>hasPrevious</code> to build pagination controls in your UI.</p>
</div>
`}</HTMLBlock>

## Alternative - the subscription embed

Instead of building the payment step yourself, you can hand the intent from `selectSubscriptionPlan` to the MoneyHash-hosted **subscription embed** - it renders the payment UI end-to-end and resolves with the final intent details.

```javascript JavaScript
const intentDetails = await moneyHash.renderSubscriptionEmbed({
  intentId: "<intent-id>",
  selector: "#subscription-container",
});
```
```swift iOS
moneyHash.renderSubscriptionEmbed(
    on: self, // the presenting UIViewController
    intentId: "<intent-id>",
    embedStyle: nil
) { result in
    // Result<IntentDetails, Error>
}
```
```kotlin Android
moneyHash.renderSubscriptionEmbed(
    intentId = "<intent-id>",
    embedStyle = null,
    launcher = intentLauncher // ActivityResultLauncher<IntentCreationParams>
)
```
```dart Flutter
final intentDetails = await moneyHash.renderSubscriptionEmbed(
  "<intent-id>",
  null, // embedStyle
);
```
```typescript React Native
const intentDetails = await moneyHash.renderSubscriptionEmbed(
  "<intent-id>",
);
```

***

## Handling&#x20;

* **Respect&#x20;**`alreadySubscribed`**.** Disable or restyle plans the customer is already on - subscribing twice to the same plan is a support ticket waiting to happen.
* **The first cycle is just a payment.** Everything from the payment pages applies - intent states, card collection, 3-D Secure, webhooks. The subscription only turns `ACTIVE` once that payment completes; until then it sits in `INCOMPLETE`.
* **Track the lifecycle server-side.** `PAST_DUE` is your dunning trigger; `PENDING_CANCELLATION` means access continues until the cycle ends; `ENDED` fires when `recurringCycles` runs out - none of these should be inferred client-side.
* **Trials and discounts are plan attributes.** `trialPeriod`, `discountAmount` / `discountPercentage`, and `discountCycles` come from the dashboard configuration - render them, don't compute them.
* **Groups are single-currency.** Filter plan groups by the customer's currency (`getSubscriptionPlanGroups({ currency })`) rather than mixing tiers across currencies in one screen.