Prerequisites
- You have a subscription agreement with Waffo and hold the Sandbox API key, RSA key pair, and merchant id
- Your project runs
stripe-java24.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
- Maven
- Gradle
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
Replacenew StripeClient(key) with WaffoStripe.client(...). The adapter takes Waffo routing configuration only — it holds no Stripe credential of its own.
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:
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 onemetadata line to your existing parameter builder and leave everything else alone.
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 ithandoffAt. 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 returned
Session.idstarts withwcs_;client.checkout().sessions().retrieve("wcs_…")retrieves it. session.getSubscription()returns the correspondingwsub_…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_…andcs_…ids still go to Stripe. The two id namespaces never collide.
Step 4 Translate Webhook notifications
Callhandle(...) 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
TheWaffoStripeWebhookResult 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 producesinvoice.paid/invoice.payment_failed, so translating this one too would double-count. It is exposed separately viagetPaymentNotification(); usepaymentInfo.productNameto tell subscription billing apart from one-time payments.SUBSCRIPTION_CHANGE_NOTIFICATION— subscription upgrades and downgrades, out of first-phase scope.- Non-terminal refund notifications.
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.
Integration checklist
Dependency and configuration
- The dependency version was confirmed resolvable from Maven Central, not copied from documentation
stripe-javais 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_NOTIFICATIONhas its own subscription-aware handling, separate from one-time payments- Fulfillment has moved off
checkout.session.completedtocustomer.subscription.createdandinvoice.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
handoffAtto stop Stripe renewal and set the Waffobilling_cycle_anchor
Verification
- Every fallback surfaced by
FAIL_LOUDduring 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 differentWaffoConfig 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
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’smetadata.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.
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 asub_: the migration tool supports retrieving and immediately cancellingwsub_, but not updating it;sub_passes through to Stripe. metadataor event names built from enums or constants — a static scan cannot enumerate them; confirm by hand that the tag is really applied.
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, whateveronUnsupported 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.
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.
Related resources
- Stripe Migration Tool — what it does and whether it fits you
- Webhook signature verification — how Waffo signs notifications
- Idempotency — Waffo’s idempotency key design
- Error codes — look up Waffo error-code meanings
- GitHub repository — source and changelog