> ## 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.

# Stripe Migration Tool Integration Guide

> Integrate the Stripe migration tool into your Java project to move expiring Stripe subscriptions to Waffo and verify inquiry, cancellation, and Sandbox behavior.

This guide assumes you have read [Stripe Migration Tool](/docs/en/developer-docs/tools-and-references/references/stripe-adapter) and confirmed this migration path suits your project. It covers what to change first, then the rules behind it.

## Prerequisites

* You have a subscription agreement with Waffo and hold the Sandbox API key, RSA key pair, and merchant id
* Your project runs `stripe-java` 24.11.0 or later
* You have a publicly reachable HTTPS endpoint for Waffo notifications
* The currencies you plan to route are within your contract; verify with `paymethodconfig/inquiry`

## Step 1 Install the dependency

<Tabs>
  <Tab title="Maven">
    ```xml theme={null}
    <dependency>
        <groupId>com.waffo</groupId>
        <artifactId>waffo-java-stripe</artifactId>
        <version>0.2.0</version>
    </dependency>
    ```
  </Tab>

  <Tab title="Gradle">
    ```groovy theme={null}
    implementation 'com.waffo:waffo-java-stripe:0.2.0'
    ```
  </Tab>
</Tabs>

**Leave your existing `stripe-java` version alone.** It is a provided dependency; the adapter will not upgrade or downgrade it. `waffo-java` is pulled in transitively, so you do not declare it separately.

<Note>
  Check [Maven Central](https://central.sonatype.com/artifact/com.waffo/waffo-java-stripe) for the current version and confirm it resolves with `mvn dependency:get -Dartifact=com.waffo:waffo-java-stripe:<version>` before writing it into your `pom.xml`.
</Note>

## Step 2 Build the routing client

Replace `new StripeClient(key)` with `WaffoStripe.client(...)`. The adapter takes Waffo routing configuration only — **it holds no Stripe credential of its own**.

```java theme={null}
import com.stripe.Stripe;
import com.stripe.StripeClient;
import com.waffo.stripe.WaffoStripe;
import com.waffo.stripe.config.WaffoConfig;

// 1. Your Waffo credentials, built with waffo-java's own config class
com.waffo.types.config.WaffoConfig waffoJavaConfig =
        com.waffo.types.config.WaffoConfig.builder()
                .apiKey(System.getenv("WAFFO_API_KEY"))
                .privateKey(System.getenv("WAFFO_PRIVATE_KEY"))       // RSA private key, base64
                .waffoPublicKey(System.getenv("WAFFO_PUBLIC_KEY"))    // Waffo public key, base64
                .merchantId(System.getenv("WAFFO_MERCHANT_ID"))
                .environment(com.waffo.types.config.Environment.SANDBOX)
                .build();

// 2. Routing configuration
WaffoConfig routing = WaffoConfig.builder()
        .waffoConfig(waffoJavaConfig)
        .notifyUrl("https://your-app.example/webhooks/waffo")
        .onUnsupported(WaffoConfig.OnUnsupported.FAIL_LOUD)   // recommended while migrating, see below
        .build();

// 3. Supply the Stripe key the normal stripe-java way; it is used unchanged
//    for pass-through and fallback calls
Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY");

StripeClient client = WaffoStripe.client(routing);
```

The adapter stamps every request to Waffo with an `X-Waffo-Client: waffo-stripe-java/<version>` identifier header. You neither need to nor should wrap the transport layer to forge it yourself.

Each parameter's meaning, values, and default are in the [configuration reference](#configuration-reference) below. One choice is worth calling out here:

<Tip>
  **Use `FAIL_LOUD` while migrating.** The default `FALLBACK` silently forwards unroutable requests to Stripe, which is exactly when you most need to see which subscriptions did not move. Start with `FAIL_LOUD` to surface every case, work through them, then switch to `FALLBACK` as a production safety net.
</Tip>

If your project already has a `StripeClient` configured with timeouts and a proxy, `WaffoStripe.client(routing, existingClient)` preserves all of it — see [client construction and the Stripe credential](#client-construction-and-the-stripe-credential).

## Step 3 Tag the subscriptions to migrate

Add one `metadata` line to your existing parameter builder and leave everything else alone.

```java theme={null}
SessionCreateParams params = SessionCreateParams.builder()
        .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
        .setSuccessUrl("https://your-app.example/subscription/success")
        .setCancelUrl("https://your-app.example/subscription/cancel")
        .addLineItem(SessionCreateParams.LineItem.builder()
                .setPrice("price_123")
                .setQuantity(1L)
                .build())
        .putMetadata("source", "waffo")        // the only line you add
        .build();

RequestOptions options = RequestOptions.builder()
        .setIdempotencyKey(persistedSubscriptionRequest)   // persisted before the call
        .build();

Session session = client.checkout().sessions().create(params, options);
redirect(session.getUrl());   // Waffo cashier URL when routed, Stripe URL on fallback
```

The example intentionally omits `uiMode`. Stripe defaults to a hosted Checkout, and the migration tool also treats an omitted value as hosted. This keeps the code compatible with `stripe-java` 24.11.x, 32.x, and 33.x. Do not substitute `HOSTED_PAGE` on 32.x or 33.x: its serialized `hosted_page` value is classified as a non-hosted Checkout.

### Move expiring Stripe subscriptions to Waffo

Read the end of the current paid period from the original Stripe subscription and call it `handoffAt`. Use your existing Stripe flow to stop renewal at `handoffAt`; the migration tool does not modify the original Stripe subscription.

On the Waffo-routed request, use the same time as `billing_cycle_anchor` and explicitly set `proration_behavior=none`:

```java theme={null}
long handoffAt = stripeSubscription.getCurrentPeriodEnd();

SessionCreateParams params = SessionCreateParams.builder()
        .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
        .setSuccessUrl("https://your-app.example/subscription/success")
        .setCancelUrl("https://your-app.example/subscription/cancel")
        .addLineItem(SessionCreateParams.LineItem.builder()
                .setPrice("price_123")
                .setQuantity(1L)
                .build())
        .setSubscriptionData(SessionCreateParams.SubscriptionData.builder()
                .setBillingCycleAnchor(handoffAt)
                .setProrationBehavior(
                        SessionCreateParams.SubscriptionData.ProrationBehavior.NONE)
                .build())
        .putMetadata("source", "waffo")
        .build();
```

| Time                      | Behavior                                                                                                                   |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Before `handoffAt`        | The customer opens the Waffo cashier and completes card entry and any required 3DS challenge; no first charge is collected |
| Inquiry while waiting     | `status=active`; `billing_cycle_anchor` and `current_period_end` equal `handoffAt`; `metadata.waffo_current_period=0`      |
| At `handoffAt`            | Waffo initiates the first charge automatically; the customer does not need to return                                       |
| Cancel before `handoffAt` | `Subscription.cancel("wsub_…")` immediately cancels the Waffo subscription and prevents the scheduled first charge         |

<Note>
  `billing_cycle_anchor` maps exactly to Waffo `startTime`. Waffo's backend validates the maximum scheduling window; the migration tool does not hardcode a 365- or 366-day limit. Do not combine it with `trial_end` or `trial_period_days`; those combinations are treated as mapping failures.
</Note>

<Warning>
  **The idempotency key must be 32 characters or fewer. Generate and persist it before the call, then reuse it on every retry.**

  The adapter uses this key as Waffo's `subscriptionRequest` to confirm create results and prevent duplicate subscriptions. Do not pass a business order id longer than 32 characters. The current version converts a longer key into an irreversible 32-character digest, so a Webhook cannot recover the original value from it. If your existing order id is longer, generate a separate stable correlation key of at most 32 characters and persist its relationship to the order id.

  Do not rely on an idempotency key generated automatically by `stripe-java`. The adapter intercepts the request before Stripe's transport layer generates that key, so it never becomes Waffo's `subscriptionRequest`. If you do not pass a key explicitly in `RequestOptions`, you also cannot persist and reuse it on the next call.
</Warning>

After a successful create:

* The returned `Session.id` starts with `wcs_`; `client.checkout().sessions().retrieve("wcs_…")` retrieves it.
* `session.getSubscription()` returns the corresponding `wsub_…` subscription id; `client.subscriptions().retrieve("wsub_…")` routes back to Waffo automatically.
* After creation, persist the `wsub_…` subscription id with your business order id. Use that subscription id to find the business record when processing Webhooks; do not try to reverse the idempotency key.
* Native `sub_…` and `cs_…` ids still go to Stripe. The two id namespaces never collide.

## Step 4 Translate Webhook notifications

Call `handle(...)` at the `notifyUrl` endpoint you configured. The SDK verifies and parses the notification, translates its event, and generates the acknowledgment Waffo uses to confirm delivery.

```java theme={null}
import com.stripe.exception.StripeException;
import com.stripe.model.Event;
import com.waffo.stripe.net.WaffoStripeWebhooks;
import com.waffo.stripe.net.WaffoStripeWebhookResult;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;

// Built once from the same waffo-java config; it verifies the signature itself
WaffoStripeWebhooks webhooks = new WaffoStripeWebhooks(waffoJavaConfig);

@PostMapping(value = "/webhooks/waffo", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> onWaffo(@RequestBody String body,
                                      @RequestHeader("X-SIGNATURE") String signature) throws StripeException {

    WaffoStripeWebhookResult result = webhooks.handle(body, signature);   // verify, translate, and generate the acknowledgment

    // Record subscription payment attempts and retries here; keep this out of
    // one-time payment fulfillment
    if (result.getPaymentNotification() != null) {
        subscriptionPaymentHandler.handle(result.getPaymentNotification().getResult());
    }

    Event event = result.getEvent();
    if (event != null) {
        existingStripeWebhookDispatcher.dispatch(event);   // your existing Stripe dispatch logic
    }

    // The SDK is Web-framework neutral; map its acknowledgment into Spring here
    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(result.getResponseBody());
}
```

### Return the acknowledgment generated by the SDK

The `WaffoStripeWebhookResult` returned by `handle(...)` already contains the acknowledgment body. The SDK stays Web-framework neutral, so it does not return Spring's `ResponseEntity` directly. In Spring, map these two values as shown below; use the equivalent mapping for another Web framework:

| Requirement                      | Detail                              |
| -------------------------------- | ----------------------------------- |
| Response body                    | Return `getResponseBody()` directly |
| `Content-Type: application/json` | It cannot be `text/plain`           |

This response tells Waffo that your endpoint received the notification. If you replace it with `"ok"`, Waffo cannot recognize a successful result and delivers the same notification again.

When signature validation fails, **do not apply any business effect**. Log a security event, then reconcile any state that needs recovery through the subscription inquiry API.

### Event mapping

| Waffo notification                         | Translated Stripe event                                                           |
| ------------------------------------------ | --------------------------------------------------------------------------------- |
| `SUBSCRIPTION_STATUS_NOTIFICATION`         | `customer.subscription.created` / `updated` / `deleted`                           |
| `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | `invoice.paid` / `invoice.payment_failed`, for the first period and every renewal |
| `REFUND_NOTIFICATION`                      | `charge.refunded`, or `refund.updated` when the refund fails                      |

Translated events expose their data object through your usual `event.getDataObjectDeserializer().getObject()`, exactly like native Stripe events.

The following are **not translated**, and `getEvent()` returns `null`:

* `PAYMENT_NOTIFICATION` — the period-changed notification already produces `invoice.paid` / `invoice.payment_failed`, so translating this one too would double-count. It is exposed separately via `getPaymentNotification()`; use `paymentInfo.productName` to tell subscription billing apart from one-time payments.
* `SUBSCRIPTION_CHANGE_NOTIFICATION` — subscription upgrades and downgrades, out of first-phase scope.
* Non-terminal refund notifications.

<Warning>
  **Projects that fulfill on `checkout.session.completed` must be reworked.** The adapter does not translate that event. Move subscription activation and entitlement grants to `customer.subscription.created` and `invoice.paid`, and use business-level idempotency to prevent double granting. This is the most commonly missed part of a migration.
</Warning>

<Note>
  The legacy `translate(body, signature)` method is retained for source compatibility, but it only returns the translated `Event` and does not expose the acknowledgment response above. **New integrations must use `handle(...)`.**
</Note>

## Step 5 Integrate immediate cancellation

The migration tool supports immediate Waffo subscription cancellation through Stripe's default cancel call. It does not simulate period-end cancellation, scheduled cancellation, or subscription updates.

Here is how the two differ:

|                             | Stripe                                          | Waffo                                     |
| --------------------------- | ----------------------------------------------- | ----------------------------------------- |
| Cancel immediately          | Supported                                       | Supported                                 |
| Cancel at period end        | `cancel_at_period_end=true`                     | **No equivalent switch**; immediate only  |
| Cancel at a given time      | `cancel_at`                                     | Not supported                             |
| Reversible after cancelling | A period-end cancel can be undone before expiry | Cancellation is terminal                  |
| Where you call it           | Stripe SDK                                      | `client.subscriptions().cancel("wsub_…")` |

The default call invokes Waffo `subscription/cancel`, then retrieves the same subscription to confirm a terminal cancelled state. If the cancel result is unknown, the migration tool recovers only by retrieving the same `wsub_…`; it never forwards the operation to Stripe. Cancelling while the subscription is waiting for `handoffAt` prevents the scheduled first charge.

<Warning>
  Cancel options with additional billing semantics, including `invoice_now=true` or `prorate=true`, fail immediately. Waffo also has no equivalent to Stripe's period-end cancellation. Keep the flow on Stripe if it depends on `cancel_at_period_end`, scheduled cancellation, cancellation reversal, or `Subscription.update("wsub_…")`.
</Warning>

## Integration checklist

### Dependency and configuration

* The dependency version was confirmed resolvable from Maven Central, not copied from documentation
* `stripe-java` is 24.11.0 or later
* The client is built with `WaffoStripe.client(...)` and Sandbox configuration is wired in

### Code changes

* Target create requests carry `metadata.source=waffo`, with the idempotency key persisted beforehand
* The Webhook endpoint uses `handle(...)` and returns the response body generated by the SDK
* `PAYMENT_NOTIFICATION` has its own subscription-aware handling, separate from one-time payments
* Fulfillment has moved off `checkout.session.completed` to `customer.subscription.created` and `invoice.paid`
* The project can retrieve and immediately cancel `wsub_` subscriptions and does not depend on unsupported capabilities such as period-end cancellation
* Expiring subscriptions use the same `handoffAt` to stop Stripe renewal and set the Waffo `billing_cycle_anchor`

### Verification

* Every fallback surfaced by `FAIL_LOUD` during migration has been reviewed and resolved
* The project's own build and tests pass
* The full Sandbox flow passes (see the next section)

## Sandbox verification

Verification must be driven through **your own project's HTTP endpoints**; the adapter's internal tests are not a substitute. Cover:

| Item                    | What to confirm                                                                                           |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| Create                  | A tagged request really routed to Waffo, returning a `wcs_` session and a Waffo cashier URL               |
| Payment                 | A real cashier payment completed in a browser                                                             |
| Inquiry                 | The `wsub_` subscription retrieves correctly with the right status mapping                                |
| Waiting for handoff     | The customer completed card entry and any required 3DS; inquiry fields show that period 1 has not started |
| Automatic first charge  | Waffo charges automatically at `handoffAt` without asking the customer to return                          |
| Renewal                 | The renewal notification arrives and is handled correctly                                                 |
| Cancel before start     | An awaiting `wsub_…` is cancelled immediately and no first charge is created                              |
| Cancellation capability | Default immediate cancellation succeeds; flows that require period-end cancellation remain on Stripe      |
| Fallback                | Requests hitting a fallback condition really went to Stripe, with the correct reason code in `metadata`   |
| Pass-through            | Untagged requests behave exactly as before the migration                                                  |
| Webhook                 | Response body and `Content-Type` satisfy the protocol                                                     |

***

What follows are the adapter's behavioral rules and parameter details. Consult them when integration produces an unexpected result.

## Configuration reference

Integration involves three groups of settings, coming from two different `WaffoConfig` classes (same name, different packages — keep them apart).

### 1. Routing config: `com.waffo.stripe.config.WaffoConfig`

The adapter's own configuration. It decides which requests go to Waffo, where notifications land, and what happens when routing fails. **Three parameters, no other switches.**

| Parameter       | Type                                 | Required                        | Default    | Meaning                                                                                                                                                                                        |
| --------------- | ------------------------------------ | ------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `waffoConfig`   | `com.waffo.types.config.WaffoConfig` | Required to route subscriptions | `null`     | Waffo credentials for the routing target. Left `null`, the adapter degrades to pure Stripe pass-through and a `source=waffo` tag falls back with `waffo_not_configured`                        |
| `notifyUrl`     | `String`                             | Required to route subscriptions | none       | Where Waffo posts subscription notifications. Must be publicly reachable. Stripe's session params have no equivalent field, so it lives here and applies to every subscription routed to Waffo |
| `onUnsupported` | `OnUnsupported` enum                 | No                              | `FALLBACK` | What happens when a create cannot be routed to Waffo; values below                                                                                                                             |

### 2. `OnUnsupported` enum values

| Value                | Behavior                                                                                                                                                                                                                                   | When to use it                                                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `FALLBACK` (default) | Forwards the create to Stripe to complete normally, stamping `waffo_routing=stripe_fallback` and `waffo_fallback_reason` into the returned object's `metadata`, plus `waffo_fallback_code` when Waffo returned a definitive rejection code | Production. The customer can always pay; the cost is that you learn about unrouted subscriptions after the fact, from `metadata`          |
| `FAIL_LOUD`          | Does not forward to Stripe; throws a `StripeException` carrying the fallback reason                                                                                                                                                        | Migration and integration testing. Surfaces every unroutable case immediately so you can work through them before switching to `FALLBACK` |

<Warning>
  **This parameter has a limited scope.** It only governs the five cases where a create can be judged unroutable either before dispatch or on a definitive rejection: a red-line, a clean Waffo rejection, an unsupported payment method, a field mapping failure, and no Waffo client configured.

  It does **not** affect how idempotency conflicts and unknown network states are handled — those are handled by separate logic, see [cases that never fall back, to protect the customer from double charges](#cases-that-never-fall-back-to-protect-the-customer-from-double-charges).
</Warning>

### 3. Waffo credentials: `com.waffo.types.config.WaffoConfig`

This is `waffo-java`'s config class. You hand it to the `waffoConfig` parameter above and the adapter builds the Waffo client from it.

| Parameter        | Type               | Required | Meaning                                                                                     |
| ---------------- | ------------------ | -------- | ------------------------------------------------------------------------------------------- |
| `apiKey`         | `String`           | Yes      | The API key Waffo issued you                                                                |
| `privateKey`     | `String`           | Yes      | Your RSA private key, base64-encoded, used to sign requests                                 |
| `waffoPublicKey` | `String`           | Yes      | Waffo's RSA public key, base64-encoded, used to verify response and notification signatures |
| `merchantId`     | `String`           | Yes      | Your merchant id                                                                            |
| `environment`    | `Environment` enum | Yes      | `SANDBOX` or `PRODUCTION`; determines which environment requests go to                      |
| `connectTimeout` | `int`              | No       | Connect timeout                                                                             |
| `readTimeout`    | `int`              | No       | Read timeout                                                                                |

Three ways to construct it — pick one:

<CodeGroup>
  ```java Builder (explicit) theme={null}
  com.waffo.types.config.WaffoConfig cfg =
          com.waffo.types.config.WaffoConfig.builder()
                  .apiKey(System.getenv("WAFFO_API_KEY"))
                  .privateKey(System.getenv("WAFFO_PRIVATE_KEY"))
                  .waffoPublicKey(System.getenv("WAFFO_PUBLIC_KEY"))
                  .merchantId(System.getenv("WAFFO_MERCHANT_ID"))
                  .environment(com.waffo.types.config.Environment.SANDBOX)
                  .build();
  ```

  ```java Environment variables theme={null}
  // Reads WAFFO_API_KEY / WAFFO_PRIVATE_KEY / WAFFO_PUBLIC_KEY
  //     / WAFFO_MERCHANT_ID / WAFFO_ENVIRONMENT
  com.waffo.types.config.WaffoConfig cfg =
          com.waffo.types.config.WaffoConfig.fromEnv();
  ```

  ```java Properties theme={null}
  // Reads waffo.api-key / waffo.private-key / waffo.waffo-public-key
  //     / waffo.merchant-id / waffo.environment
  // springEnvironment is an injected org.springframework.core.env.Environment
  com.waffo.types.config.WaffoConfig cfg =
          com.waffo.types.config.WaffoConfig.fromProperties(springEnvironment::getProperty);
  ```
</CodeGroup>

### Client construction and the Stripe credential

`WaffoStripe.client(...)` has three overloads. They differ **only in which Stripe credential is used for pass-through and fallback**; routing to Waffo behaves identically in all three.

| Constructor                          | Stripe credential for pass-through and fallback                                                              |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `client(WaffoConfig)`                | The global `Stripe.apiKey`, or a per-request key                                                             |
| `client(WaffoConfig, StripeClient)`  | Reuses your existing `StripeClient`'s client-level key, HTTP client, timeouts, and proxy settings            |
| `client(WaffoConfig, String apiKey)` | The supplied key becomes the client-level credential, equivalent to passing `new StripeClient(apiKey)` above |

Credential precedence: **per-request `RequestOptions` key > client-level > global**.

If the second overload cannot read the client-level credential from your `stripe-java` version, client initialization fails. Upgrade to a supported version or use the overload that accepts an API key explicitly.

### Request-level parameters

| Parameter               | Where you pass it     | Required                   | Meaning                                                                                                                                                                                |
| ----------------------- | --------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metadata.source=waffo` | `SessionCreateParams` | Required to route to Waffo | The routing tag. Requests without it pass through to Stripe unchanged                                                                                                                  |
| `idempotencyKey`        | `RequestOptions`      | Integration requirement    | At most 32 characters. Generate and persist it before the call, then reuse it on retry. If the business order id is longer, use a separate stable correlation key and save the mapping |

## Unsupported Stripe usage

The first phase covers only a subset of subscription Checkout. The table below lists every unsupported usage.

Rows that say *Falls back to Stripe* do not affect payment: the adapter forwards the create to Stripe, the customer pays as usual, and the reason code lands in the returned object's `metadata.waffo_fallback_reason` alongside `waffo_routing=stripe_fallback`. Those two names and `waffo_fallback_code` are reserved by the adapter — do not use them for your own metadata. The rest either throw or silently stop working, and need code changes.

<Note>
  Every row below needs to be checked by hand against your own code — a scanner can only give you leads; it cannot determine currency, payment method, or dynamically assembled parameters. See [scanning for which ones apply to you](#scanning-for-which-ones-apply-to-you).
</Note>

| Your Stripe usage                                                                                         | Consequence and what to do                                                                                                                                                                                         | Result                                 |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| You sell a one-time product, not a subscription                                                           | Falls back to Stripe. One-time payments should never carry the tag; drop `source=waffo`                                                                                                                            | `not_subscription_mode`                |
| The cashier is embedded in your own page instead of redirecting                                           | Falls back to Stripe. Waffo needs the redirect cashier; just remove the `uiMode` setting (Stripe defaults to it), or accept Stripe                                                                                 | `non_hosted_ui`                        |
| One subscription bundles several products or plans                                                        | Falls back to Stripe. A Waffo subscription is a single amount. Split it up, or accept Stripe                                                                                                                       | `multi_item`                           |
| The price information is incomplete                                                                       | Falls back to Stripe. Supply a price; at least one of `price` or `price_data` is required                                                                                                                          | `missing_price`                        |
| A coupon or promotion code is applied                                                                     | Falls back to Stripe. Waffo subscriptions have no coupon input. Fold the discount into the price, or accept Stripe                                                                                                 | `has_discount`                         |
| There is a trial and no card is collected up front                                                        | Falls back to Stripe. Waffo must collect a card first. Set `payment_method_collection` to `always`                                                                                                                 | `trial_without_upfront_card`           |
| Usage-based billing — pay for what you use                                                                | Falls back to Stripe. Out of first-phase scope                                                                                                                                                                     | `metered`                              |
| Tiered pricing — cheaper per unit at higher volume                                                        | Falls back to Stripe. Out of first-phase scope                                                                                                                                                                     | `tiered`                               |
| The subscription runs in phases, e.g. one price for three periods then another                            | Falls back to Stripe. Cannot be expressed as a fixed Waffo period; out of first-phase scope                                                                                                                        | `has_schedule`                         |
| The settlement currency is not in your Waffo contract                                                     | Falls back to Stripe. Use `paymethodconfig/inquiry` to confirm which currencies your contract covers                                                                                                               | `currency_mismatch`                    |
| The payment method the customer picked cannot recur on Waffo                                              | Falls back to Stripe. Waffo supports cards plus Alipay, WeChat Pay, GrabPay, KakaoPay, NaverPay, and PIX; mixing the card family with named wallets, or including anything Waffo does not support, also falls back | `unsupported_payment_method`           |
| The price is not recurring, the amount is customer-entered, or the price lookup failed                    | Falls back to Stripe. Check the price object. The adapter falls back rather than send Waffo a malformed request                                                                                                    | `mapping_error`                        |
| Tagged, but Waffo credentials are not configured yet                                                      | Falls back to Stripe. Check whether `waffoConfig(...)` received `null`                                                                                                                                             | `waffo_not_configured`                 |
| Waffo cleanly rejected this one                                                                           | Falls back to Stripe. Investigate via `metadata.waffo_fallback_code`; most often the subscription product is not enabled yet                                                                                       | `waffo_rejected`                       |
| Updating a subscription: `Subscription.update("wsub_…")`                                                  | Not supported by the adapter                                                                                                                                                                                       | Unsupported                            |
| Cancelling with `invoice_now=true`, `prorate=true`, or other additional billing semantics                 | The migration tool supports only default immediate cancellation                                                                                                                                                    | Unsupported                            |
| Cancel at period end: `cancel_at_period_end` / `cancel_at`                                                | Waffo has no equivalent capability. Keep subscription flows that depend on it on Stripe                                                                                                                            | Unsupported                            |
| Changing the period, proration, pausing, or trial length                                                  | Waffo's subscription update only changes the amount (`amount`, `trialPeriodAmount`, `scheduledAmounts`); none of these dimensions are available                                                                    | Semantic mismatch                      |
| Upgrade/downgrade via Stripe's in-place proration                                                         | A Waffo plan change is re-creating — it carries a new product and may send the customer through the cashier again, so the result is not equivalent                                                                 | Semantic mismatch                      |
| Operating on items of a Waffo subscription: `subscriptionItems.create/list(subscription="wsub_…")`        | Waffo does not produce `si_…` item ids, so there is no corresponding object                                                                                                                                        | `InvalidRequestException`              |
| Building a schedule from a Waffo subscription: `subscriptionSchedules.create(from_subscription="wsub_…")` | It would hand Stripe control of a Waffo subscription's billing cadence, so it is rejected                                                                                                                          | `InvalidRequestException`              |
| Fulfilling on `checkout.session.completed`                                                                | Silently stops working: that event is no longer produced after migration. Switch to `customer.subscription.created` and `invoice.paid`, see [event mapping](#event-mapping)                                        | No exception; the code just never runs |

Conversely, existing `si_…`, `sub_sched_…`, and `sub_…` ids all indicate Stripe ownership, and operations on them **pass through unchanged**.

<Tip>
  Keep `onUnsupported=FAIL_LOUD` throughout migration. The *Falls back to Stripe* cases then surface as exceptions so you can review them one by one. Switch to `FALLBACK` as a production safeguard only after every case is accounted for. See the [configuration reference](#configuration-reference).
</Tip>

### Scanning for which ones apply to you

You do not have to read through all your code by hand. The [AI migration skill](/docs/en/developer-docs/tools-and-references/references/stripe-adapter#migrate-automatically-with-the-ai-skill) ships a scanner that sorts every Stripe call in your project into these classifications:

| Scan result                        | Maps to the table above                                           | What you do                                            |
| ---------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------ |
| `PASS_THROUGH`                     | Not in the table; stays on Stripe                                 | Nothing                                                |
| `ROUTED_LIKELY`                    | Matched none of the rows, but that is **not** proof it will route | Confirm by testing in Sandbox                          |
| `FALLBACK`                         | Matched a *Falls back to Stripe* row                              | Accept it, or adjust the parameters                    |
| `UNSUPPORTED`                      | Matched one of the remaining rows                                 | You must change code                                   |
| `REVIEW`                           | The scanner cannot determine the source                           | Trace it until you can classify it                     |
| `BLOCKED_PENDING_OWNERSHIP_REVIEW` | Create-chain ownership is unsettled                               | Settle ownership first; do not tag anything until then |

What the scanner cannot settle, and you have to trace yourself, is mainly:

* **Params assembled in another file, a factory method, or your own wrapper** — trace the real values that reach the create call, then check them against the table above.
* **The origin of the id in an update/cancel** — follow the business flow to see whether it is a `wsub_` or a `sub_`: the migration tool supports retrieving and immediately cancelling `wsub_`, but not updating it; `sub_` passes through to Stripe.
* **`metadata` or event names built from enums or constants** — a static scan cannot enumerate them; confirm by hand that the tag is really applied.

<Warning>
  If your project contains both candidate subscription creates and `SubscriptionItem` or `SubscriptionSchedule` calls, first establish which create chain those calls belong to. A chain that depends on multiple items, proration, or schedule phases should stay on Stripe as a whole. Until ownership is settled, do not tag the candidate create with `source=waffo`.
</Warning>

<Note>
  The scanner works from regular expressions and file context, not a Java syntax tree, so **its output is an inventory, not an acceptance verdict**. `ROUTED_LIKELY` deserves particular care: it only means no obvious red-line was found. At create time, the adapter pre-checks the currency against your contract when that configuration is available, and the Waffo create API validates it otherwise; payment methods only go through a mapping pre-filter, and whether they are covered by your contract is likewise validated by the Waffo create API. Dynamically assembled parameters only settle at runtime. The real verdict comes from [Sandbox verification](#sandbox-verification).
</Note>

## Cases that never fall back, to protect the customer from double charges

In some situations Waffo may already have persisted the subscription; forwarding to Stripe would then create a second one and charge the customer twice. So in the situations below the adapter will **never** fall back to Stripe, whatever `onUnsupported` is set to:

**Idempotency conflict and unknown network state.** In both cases Waffo may already have created the subscription. The adapter re-queries using the original request's idempotency key:

* An existing subscription is found → its `wsub_` session is returned, equivalent to a successful create.
* The result cannot be confirmed → automatic fallback is not supported; query and confirm the final state.

**An explicit Waffo rejection.** Even here, the adapter first re-queries with the same idempotency key and falls back per the reason code **only after confirming that the subscription does not exist**. If the query finds an existing subscription, that subscription is routed instead; automatic fallback is not supported for transient or indeterminate results.

Both rules make the same trade-off: **pause the create and confirm its state rather than risk charging a customer twice.**

## Version compatibility and release certification

`stripe-java` is a provided dependency: the adapter pins no version, and your project decides.

|                  | Version                                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------------------- |
| Supported floor  | 24.11.0, the earliest release with the collection-point request API; earlier versions are unsupported |
| Verified ceiling | 33.x                                                                                                  |

Before each `waffo-java-stripe` release, the full deterministic test suite and a live Sandbox regression run against 14 pinned stable `stripe-java` versions, and everything must pass before publishing. Version history is in the repository [CHANGELOG](https://github.com/waffo-com/waffo-stripe/blob/main/CHANGELOG.md).

## Related resources

* [Stripe Migration Tool](/docs/en/developer-docs/tools-and-references/references/stripe-adapter) — what it does and whether it fits you
* [Webhook signature verification](/docs/en/developer-docs/webhook/signature-verification) — how Waffo signs notifications
* [Idempotency](/docs/en/developer-docs/core-concepts/idempotency) — Waffo's idempotency key design
* [Error codes](/docs/en/developer-docs/tools-and-references/developer-tools/error-codes) — look up Waffo error-code meanings
* [GitHub repository](https://github.com/waffo-com/waffo-stripe) — source and changelog
