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

# Subscription integration

> The main path for subscription (recurring billing) integration: from choosing a subscription model through sandbox acceptance.

This page describes the subscription (recurring billing) integration path for Waffo Subscription. After choosing this approach, work through the page in order to complete your integration. Each section covers only the decisions and gotchas for that step. Field definitions and enum values link to the relevant reference pages instead of being repeated here.

For one-time payments, see the [Direct API integration overview](/docs/en/developer-docs/integration/api/overview).

## Choose a subscription integration approach

Waffo supports two subscription integration approaches. First choose who will manage the subscription:

| Integration approach | Who manages the subscription | How payments are initiated | Next step |
| - | - | - | - |
| Waffo Subscription | Waffo | Waffo handles renewals based on payment-method capabilities | Continue on this page |
| Merchant-managed subscription | Merchant | Store a token and initiate MITs on your own schedule | Go to [Waffo Checkout card binding](/docs/en/developer-docs/integration/tokenization/checkout-card-binding) or [Merchant-side card binding](/docs/en/developer-docs/integration/tokenization/overview) |

Waffo Subscription provides one integration approach for merchants. A renewal may be executed by Waffo or by the payment channel. For the small capability differences between payment methods, see the [Subscription payment method comparison](/docs/en/developer-docs/tools-and-references/references/subscription-payment-methods).

A merchant-managed subscription does not use `/api/v1/subscription/*`. Your system owns the billing schedule, subscription status, failed-payment retries, plan changes, and cancellation. For each period, use a token obtained during card binding to initiate an MIT through `ONE_TIME_PAYMENT`. The card-binding docs already cover the token lifecycle, CIT verification, MIT request, and payment result confirmation, so those details are not repeated here.

After you choose Waffo Subscription, follow the integration path below:

```mermaid theme={null}
flowchart LR
    A[Choose model] --> B[Configure period and trial]
    B --> C[Create and authorize]
    C --> D[Listen for notifications]
    D --> E[Handle renewals and retries]
    E --> F[Sandbox and acceptance]
```

## Confirm three things before you integrate

| What to confirm | Where to confirm it |
| - | - |
| Whether your payment method supports subscriptions, and to what extent | [Subscription payment method comparison](/docs/en/developer-docs/tools-and-references/references/subscription-payment-methods) |
| Whether your business is payment-first or service-first | [Choose a subscription model](#choose-a-subscription-model) on this page |
| Whether your billing cycle can be expressed with `periodType` + `periodInterval` | [Configure the billing period and trial](#configure-the-billing-period-and-trial) on this page |

<Note>
  Whether subscriptions are available, and what each payment method actually supports, is ultimately determined by your contract configuration and the [`POST /api/v1/paymethodconfig/inquiry`](/docs/api-reference/pay-method-config-inquiry/pay-method-config-inquiry) response.
</Note>

## Choose a subscription model

Waffo offers two subscription models. The core difference is **how service and billing are handled after a renewal fails**. Choosing the wrong one leaves your entitlement switching and billing rhythm out of step with what you expected, so decide before you integrate.

| | Payment-first | Service-first |
| - | - | - |
| When the current charge fails | Entitlements are suspended | Service continues |
| Later periods | No further charges are created | Created as originally planned |
| Next-period baseline after a retry succeeds | Restarted from the retry success time | Original planned billing baseline is kept |
| Concurrent outstanding bills | Not allowed | Allowed |

Two questions to work out which one you are:

1. Say the second period was due on 1 July, kept failing, and finally succeeded on 3 July. During those two days, do you suspend the customer's entitlements (payment-first) or keep serving them (service-first)?
2. If one period's renewal fails for good, do you still want to charge for later periods? Stopping is payment-first; continuing is service-first.

<Info>
  Choose payment-first or service-first before integration and tell Waffo which model you want. Contact Waffo support if you need to confirm or change the model.
</Info>

PIX supports service-first only; every other payment method supports both. See the [Subscription payment method comparison](/docs/en/developer-docs/tools-and-references/references/subscription-payment-methods).

## Configure the billing period and trial

The period is expressed by combining `productInfo.periodType` with `productInfo.periodInterval`.

| periodType | Allowed periodInterval |
| - | - |
| `DAILY` | 1–365 |
| `WEEKLY` | 1–4 |
| `MONTHLY` | 1 or greater, no upper limit |

<Warning>
  **There is no `YEARLY` type.** Express an annual subscription as `MONTHLY` with `periodInterval: "12"` — likewise `3` for quarterly, `6` for semi-annual, and `24` for two years.
</Warning>

The remaining period fields:

* `numberOfPeriod` — total number of periods. Leave empty for an open-ended subscription.
* `trialPeriodAmount` — the amount charged per trial period. Must be 0 or greater and less than the regular amount. Leave empty for no trial; set it to `0` for a free trial.
* `numberOfTrialPeriod` — how many trial periods there are.
* `trialPeriodType` / `trialPeriodInterval` — the period type and interval for the trial. These may differ from the regular period (for example a weekly trial before monthly billing); leave them empty to inherit the regular values. PIX requires the trial frequency to match the regular frequency.
* `scheduledAmounts` — a per-period amount list whose elements are `{period, amount}`. Use it when amounts differ by period, such as a first-period discount or stepped pricing. Omit it when every period costs the same. This field is supported only by eligible Waffo-managed subscriptions; channel-managed subscriptions such as PIX and DANA do not support it.

For full field definitions, see [Create Subscription](/docs/api-reference/subscription-create/create-subscription).

## Create a subscription and handle authorization

Call [`POST /api/v1/subscription/create`](/docs/api-reference/subscription-create/create-subscription) to create a subscription. Beyond the period parameters, these fields cause the most trouble:

* `subscriptionRequest` — the subscription idempotency key, which you generate. Reuse the same value when retrying a create request; see [Idempotency](/docs/en/developer-docs/core-concepts/idempotency).
* `currency` and `amount` — subscriptions use `currency` and `amount`, **not** the `orderCurrency` and `orderAmount` used by one-time payments. Mixing up these field names is the most common integration error.
* `paymentInfo.payMethodType` — optional. When provided, it filters candidates by payment method type. When omitted, Waffo applies no type filter. Waffo shows all eligible payment methods under your merchant contract only when `paymentInfo.payMethodName` is also omitted.
* `userInfo.userEmail` — required. When you have no real email address, pass a unique fallback address derived from the user ID. Do not use placeholder values, and do not share one address across multiple users.
* `subscriptionManagementUrl` — required, and it must be an authenticated page (not a public URL). Pass a web address rather than a deep link, because deep links cannot be opened on desktop. Use your own subscription management page, or wrap the URL returned by [`POST /api/v1/subscription/manage`](/docs/api-reference/subscription-manage/subscription-manage).
* `notifyUrl` — your Webhook callback address; see [Listen for notifications](#listen-for-notifications) on this page.

### Handle the authorization redirect

When `subscriptionStatus` in the response is `AUTHORIZATION_REQUIRED`, the customer has to complete authorization and you must redirect them to the authorization page. The address lives in `subscriptionAction`, which is a **JSON string** — parse it first, then read `webUrl` from it:

```json theme={null}
{
  "code": "0",
  "msg": "Success",
  "data": {
    "subscriptionRequest": "sub_a1b2c3d4e5f6a1b2c3d4e5f6",
    "subscriptionId": "SUB20260805000001",
    "subscriptionStatus": "AUTHORIZATION_REQUIRED",
    "subscriptionAction": "{\"webUrl\":\"https://cashier.waffo.com/subscribe?token=xxx\"}"
  }
}
```

<Warning>
  Do not read properties off `subscriptionAction` as if it were an object — it is a string and must be JSON-parsed first. The same handling applies to the subscription inquiry, subscription change, and change inquiry responses.
</Warning>

## Subscription status and what you should do

A subscription has 8 statuses. For what to do in each one, see the [Payment lifecycle](/docs/en/developer-docs/core-concepts/payment-lifecycle#subscription-statuses); for which ones are final, see the [Data reference](/docs/en/developer-docs/tools-and-references/references/data-reference#subscription-statuses).

<Warning>
  Do not treat the synchronous create response as the final subscription status. Activation, cancellation, and closure all arrive via Webhook — or confirm them yourself with [`POST /api/v1/subscription/inquiry`](/docs/api-reference/subscription-inquiry/subscription-inquiry).
</Warning>

## Listen for notifications

Subscriptions involve three notifications; subscribe to the ones that match the granularity you care about. For when each one fires, what it suits, and the recommended combinations, see [Webhook event types](/docs/en/developer-docs/webhook/event-types#subscription-notification-selection-guide).

The key constraint: `SUBSCRIPTION_STATUS_NOTIFICATION` and `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` are both dispatched asynchronously and **arrive in no guaranteed order**, so never drive your business state machine off callback arrival order. For the correct approach (idempotent deduplication, and querying the final status after either callback arrives), see [Webhook best practices](/docs/en/developer-docs/webhook/best-practices).

## Renewal failures and retries

Who retries a failed renewal—Waffo or the payment channel—depends on the renewal management type.

For **Waffo-managed subscriptions**, Waffo retries the current period's charge automatically — you do not need to implement retries yourself:

* The retry policy is configured per billing cycle, and each tier covers a **maximum number of attempts** and a **retry interval**.
* The retry interval is measured in **days**. There is no hour-level retry interval configuration.
* Without customization, Waffo retries once per day after the current-period charge first fails, for up to five retry attempts. To customize the attempt limit or interval, contact your Waffo account manager or technical support; you cannot change this policy through API parameters.
* Your [subscription model](#choose-a-subscription-model) determines both the next-period baseline after a retry succeeds and whether later periods are still charged after retries are exhausted. See [Subscription payment method comparison](/docs/en/developer-docs/tools-and-references/references/subscription-payment-methods#how-waffo-managed-renewal-times-are-calculated) for the complete Waffo-managed renewal timing rules.

For **channel-managed subscriptions**, the payment channel applies its own retry rules. PIX and DANA use wallet-side retry rules and do not support merchant customization. See [Subscription payment method comparison](/docs/en/developer-docs/tools-and-references/references/subscription-payment-methods) for details.

<Tip>
  For Waffo-managed subscriptions, mind the notification granularity: **every** failed attempt during retries sends a `PAYMENT_NOTIFICATION`. If you only care about each period's final result and not the intermediate retries, subscribe to `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` instead.
</Tip>

## Change, update, and cancel

These are three different operations on three different endpoints — do not mix them up:

| What you want to do | Endpoint | Key points |
| - | - | - |
| Switch plans (upgrade / downgrade) | [`POST /api/v1/subscription/change`](/docs/api-reference/subscription-change/subscription-change) | Requires a new `subscriptionRequest`, the original `originSubscriptionRequest`, plus `remainingAmount` (the original subscription's remaining value credited to the new one) and `productInfoList` (the new plan). Use `startTime` to schedule when it takes effect, or leave it empty for immediate; pass `promotionInfo` to apply a promotion. May return `AUTHORIZATION_REQUIRED` — handle it as described in [Handle the authorization redirect](#handle-the-authorization-redirect). Sends `SUBSCRIPTION_CHANGE_NOTIFICATION` on completion. |
| Adjust amounts or collect a top-up | [`POST /api/v1/subscription/update`](/docs/api-reference/subscription-update/subscription-update) | Identify the subscription by `subscriptionRequest` or `subscriptionId`. Use `amount`, `productInfo.trialPeriodAmount`, or `productInfo.scheduledAmounts` for a standard amount update. Add `topupInfo` to create a current-period top-up order and apply the new amount to later periods after the top-up succeeds. |
| Cancel from the merchant side | [`POST /api/v1/subscription/cancel`](/docs/api-reference/subscription-cancel/subscription-cancel) | Pass `subscriptionId`, `merchantId`, and `requestedAt`. Every payment method supports merchant-side cancellation. |
| Give customers a management entry point | [`POST /api/v1/subscription/manage`](/docs/api-reference/subscription-manage/subscription-manage) | Returns the subscription management page URL. The URL is unavailable while the subscription is still processing or has failed, which returns `A0028`. |

### Common amount-update scenarios

`update` supports only `ACTIVE`, Waffo-managed subscriptions. Every request must include at least one of `amount`, `productInfo.trialPeriodAmount`, or `productInfo.scheduledAmounts`. Even when you add `topupInfo`, include the target amount to apply after the top-up succeeds. A direct amount update affects future charges; the current-period order already exists and cannot be changed directly.

| Scenario | What to send | When it takes effect |
| - | - | - |
| Set one amount for all later periods | Send `amount` | The new amount applies from the next period. If a period also appears in `scheduledAmounts`, that period-specific amount takes precedence. |
| Temporarily change one or more future periods | Send `productInfo.scheduledAmounts`, for example a discount for period 4 only | Every `period` must be later than the current period, with at most 10 entries. Unlisted periods continue to use `amount`. The submitted list replaces the stored list, so include any other future period-specific amounts you still need. |
| Adjust an unfinished trial | Send `productInfo.trialPeriodAmount` | Available only while the trial is still active; applies from the next period. |
| Add seats now and raise later renewal amounts | Send the new future `amount` together with `topupInfo` | Waffo creates an immediate top-up order for the current-period difference. The new `amount` applies from the next period only after that top-up succeeds. A closed or failed top-up does not change later amounts. |

Example request for a current-period seat add-on:

```json theme={null}
{
  "subscriptionId": "SUB20260920000001",
  "amount": "130.00",
  "topupInfo": {
    "topupRequest": "seat-topup-001",
    "topupAmount": "15.00",
    "description": "Add three seats for the current period"
  }
}
```

`topupAmount` is the current-period difference you calculate and must be greater than 0; `amount` is the new total for later periods after the top-up succeeds. `topupRequest` is the top-up idempotency key and must be reused when retrying the same request. When the response has `topupInfo.topupStatus` set to `AUTHORIZATION_REQUIRED`, parse `topupInfo.topupAction` as a JSON string and redirect the customer to its `webUrl`. A subscription can have only one top-up in progress at a time.

With some payment methods, customers can cancel on the payment method's own side (Apple Wallet, Google Pay, and the PayPay app, for example). That cancellation is reported back to Waffo and surfaces as `CHANNEL_CANCELLED` or `USER_CANCELLED`. For which payment methods support customer-side cancellation, see the [Subscription payment method comparison](/docs/en/developer-docs/tools-and-references/references/subscription-payment-methods).

## Sandbox and acceptance

<Steps>
  <Step title="First-period payment">
    Works the same as a one-time payment — complete it on the cashier page.
  </Step>

  <Step title="Simulate renewals">
    Call `POST /api/v1/subscription/manage` to get the management page URL, open it, and use the "simulate next payment success" and "simulate next payment failure" buttons to step through periods. For the procedure, see [Sandbox and testing](/docs/en/developer-docs/getting-started/sandbox) and the [Sandbox simulator](/docs/en/developer-docs/tools-and-references/developer-tools/sandbox-simulator).
  </Step>

  <Step title="Acceptance">
    Run the subscription acceptance cases from the [Integration acceptance criteria](/docs/en/developer-docs/tools-and-references/references/acceptance-criteria), fill in the subscription acceptance case template, and submit it to your Waffo technical integration group.
  </Step>
</Steps>

Before going live, confirm at minimum: both the success and failure paths for the first period, signature verification on the subscription status and subscription payment notifications, matching each period order by `subscriptionRequest` plus period number, and not closing subscriptions yourself on an Unknown status.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.