Skip to main content
This guide assumes you have read Stripe Migration Tool 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

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.
Check Maven Central 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.

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.
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 below. One choice is worth calling out here:
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.
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.

Step 3 Tag the subscriptions to migrate

Add one metadata line to your existing parameter builder and leave everything else alone.
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:
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.
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.
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.

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

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.
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.
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(...).

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: 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.
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_…").

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

2. OnUnsupported enum values

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.

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. Three ways to construct it — pick one:

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

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.
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.
Conversely, existing si_…, sub_sched_…, and sub_… ids all indicate Stripe ownership, and operations on them pass through unchanged.
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.

Scanning for which ones apply to you

You do not have to read through all your code by hand. The AI migration skill ships a scanner that sorts every Stripe call in your project into these classifications: 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.
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.
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.

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