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.

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


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

FieldTypeDescription
idstringUnique plan identifier
namestringPlan display name
descriptionstringPlan description
amountnumberRecurring payment amount
currencystringISO 4217 currency code
recurrencynumberBilling interval - number of units
recurrencyUnit"MONTH"Billing interval unit
recurringCyclesnumber | nullTotal number of billing cycles. null for indefinite
trialPeriodnumber | nullTrial period in days. null if no trial
oneTimeFeenumber | nullOne-time fee charged at signup. null if none
discountAmountnumber | nullFixed discount amount applied
discountPercentagenumber | nullPercentage discount applied
discountCyclesnumber | nullNumber of cycles the discount applies to
alreadySubscribedbooleanWhether this customer is already subscribed to this plan
isLivebooleanWhether the plan is in live mode
createdstringISO 8601 creation timestamp

SubscriptionPlanGroup

FieldTypeDescription
idstringUnique plan group identifier
namestringPlan group display name
currencystringCurrency for plans in this group
plansSubscriptionPlan[]List of plans within this group
createdstringISO 8601 creation timestamp

SubscriptionStatus - the status of a customer's subscription:

StatusDescription
NEWSubscription created, not yet active
TRIALCustomer is in the trial period
INCOMPLETEFirst payment not yet completed
ACTIVESubscription is active and billing normally
PAST_DUEPayment failed - subscription at risk
PENDING_CANCELLATIONCancellation requested, still active until end of cycle
CANCELLEDSubscription cancelled
ENDEDAll recurring cycles completed
TERMINATEDSubscription terminated early
PAUSEDSubscription 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.

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

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

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

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

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

// SubscriptionPlan[]
Note

The planGroupId is found in the MoneyHash dashboard under Plan Groups. Each plan group contains plans with the same currency and billing configuration.

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.

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
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
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
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
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
Note

selectSubscriptionPlan 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 SDK Architecture and Build Your Own Card Form for the full payment flow.

Note

The intent details of a subscription payment also carry subscriptionInfo - the subscription's id, its plan, and its current status (the lifecycle values above). Read it from any IntentDetails (including getIntentDetails) to show the subscription state in-app; your backend should still confirm via webhooks.


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.

// 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 });
let response = try await moneyHash.getSubscriptionPlanGroups(
    limit: 10,
    offset: 0,
    currency: "AED" // optional filter
)

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

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

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

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

The default pagination limit is 10. Use offset and limit to control the page size, and hasNext / hasPrevious to build pagination controls in your UI.

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.

const intentDetails = await moneyHash.renderSubscriptionEmbed({
  intentId: "<intent-id>",
  selector: "#subscription-container",
});
moneyHash.renderSubscriptionEmbed(
    on: self, // the presenting UIViewController
    intentId: "<intent-id>",
    embedStyle: nil
) { result in
    // Result<IntentDetails, Error>
}
moneyHash.renderSubscriptionEmbed(
    intentId = "<intent-id>",
    embedStyle = null,
    launcher = intentLauncher // ActivityResultLauncher<IntentCreationParams>
)
final intentDetails = await moneyHash.renderSubscriptionEmbed(
  "<intent-id>",
  null, // embedStyle
);
const intentDetails = await moneyHash.renderSubscriptionEmbed(
  "<intent-id>",
);

Handling

  • Respect 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.

Did this page help you?