The first time a team wires up Juspay in Flutter, the demo usually works within a day. Then QA finds an order where the user paid, the bank debited the money, and the app showed "payment failed" because the SDK returned backpressed after the UPI app closed. That gap, between what the SDK reports and what actually happened to the money, is where most of the work in a Juspay Flutter integration lives.
Here's how we'd structure it: the server creates the order and session, the app runs Juspay's HyperSDK, and the backend confirms status before anything is fulfilled. The server half is the same for a React Native app or a web checkout, so this is useful even if Flutter isn't your stack.
The three moving parts
Juspay's flow has three actors, and keeping their jobs separate is most of the design:
- Your backend calls Juspay's Session API (server to server, a POST to
/session, with sandbox and production hosts). It sends order details, gets back an SDK payload and a payment link, and Juspay creates the order on its side. Your API key lives here and nowhere else. - The app runs HyperSDK through the
hypersdkflutterplugin. It boots the SDK, hands it the payload from your server, and shows the payment screens. - Juspay processes the payment and notifies your backend through webhooks. Your backend can also ask it directly through the Order Status API.
The app is the least trustworthy of the three. It can be killed mid-payment, lose network on the bank page, or be tampered with. So it starts payments and displays results. It never decides them.
Setting up the Juspay Flutter integration on both platforms
The hypersdkflutter README covers setup, and the platform steps are where people lose an afternoon:
- Android: add your
clientIdto the rootbuild.gradle(Groovy or Kotlin DSL), and makeMainActivityextendFlutterFragmentActivity, notFlutterActivity. The SDK renders in fragments and fails without it. - iOS: add the post-install hook to your Podfile that references HyperSDK's
Fuse.rb, and createios/MerchantConfig.txtwith your client ID.
In Dart, the lifecycle is initiate(), then process(), then terminate() when you're done. initiate() boots the Hyper engine and takes the static parameters for the session; it's slow enough that you should call it early, when the user enters the checkout flow, not when they tap Pay. Check isInitialised() before calling it again.
When the user taps Pay, your app calls your backend, which calls the Session API and returns the SDK payload. Pass that payload straight into process(). Don't build it on the client: the payload carries the amount and order ID, and those must come from your server.
On Android, back presses need handling. The README asks you to wrap the parent widget in WillPopScope and call hyperSDK.onBackPress() in onWillPop. On newer Flutter versions, where WillPopScope is deprecated in favour of PopScope, check the current README for the recommended pattern rather than guessing.
Reading process_result without trusting it
The SDK sends events back through a callback. You'll see hide_loader (stop your spinner) and process_result, which carries a status such as charged, backpressed, user_aborted, authorizing, api_failure or another error state.
Treat every one of those as a hint about what to show, not as the outcome:
charged: show "confirming your payment" and ask your backend. It'll almost always say paid, but the backend is what says so.backpressedoruser_aborted: don't show "failed". Show "we're checking", then ask your backend. UPI is the classic case: the user approves in their UPI app, comes back, presses back, and the money has already moved.authorizing(ornew): show a pending screen and poll your backend for a minute or so. Then tell the user you'll notify them.- Errors: still ask the backend once. A network error on the device says nothing about the bank's side.
Juspay's own docs say the same thing: for the ambiguous states, check order status server to server.
Confirm status on the backend: webhooks plus Order Status API
Juspay recommends using both webhooks and the Order Status API, and we agree. Each covers the other's failure mode.
Webhooks arrive without you asking, usually fast, and cover the case where the user closed the app. Configure an HTTPS endpoint in the Juspay dashboard, authenticate incoming calls as Juspay's docs describe, and make the handler idempotent. The same event can arrive twice, and an order event can race your app's status poll.
The Order Status API is your pull-based check. Call it when the app asks about an order, when a webhook arrives (to confirm the details rather than trusting the body alone), and from a scheduled job that sweeps orders stuck in a pending state.
When either path concludes the order is charged, do one thing atomically: move the order from pending to paid with a conditional update, compare the charged amount to your stored amount, and only then trigger fulfilment. If the amount doesn't match, park the order for a person to look at.
If you redirect a web checkout back to your site, the redirect URL carries an order_id, status and an HMAC-SHA256 signature you can verify with your response key. That's fine for picking which screen to show. Still confirm with the Order Status API before shipping anything.
What web and React Native teams can reuse
Juspay ships SDKs for React Native and web as well, with the same initiate and process model. The backend in this post doesn't change at all: session creation, webhook handler, status check, sweeper job. If you build it once with clean boundaries, adding a second client later is a front-end task.
Quick pre-launch list:
- API key only on the server. The app only ever sees the SDK payload.
MainActivityextendsFlutterFragmentActivity;MerchantConfig.txtexists on iOS.- No screen says "failed" until the backend agrees.
- Webhook handler is idempotent and verifies with Order Status.
- A sweeper resolves orders stuck in pending.
- Tested on a real Android phone with a UPI app, including pressing back after approving.
We've built payment flows with multiple Indian gateways for a production ed-tech platform, including reconciliation after payment. If you're adding Juspay to a Flutter or React Native app and want someone who writes the code to review the flow, orithLabs can help through our mobile development work. The first call is free, and no pitch is required to book it.