Browser Return Flow
Unified browser payment flow for subscriptions, orders, and future gateways, including the shared payment result page contract and frontend implementation guide.
Browser Return Flow
Audience: Frontend developers, backend developers, QA Scope: Shared payment return flow,
/payments/resultcontract, subscription and order scenarios, gateway-agnostic behavior
Why This Exists
The browser should follow one payment return path regardless of:
- the feature being paid for: subscription, order, content, donation
- the gateway used: eSewa today, connectIPS / Fonepay / Khalti later
- the gateway callback format: POST form fields, GET query params, encoded payloads
The frontend should not verify payments directly. The backend owns verification and then redirects the browser to a single payment result page.
Final Shape
Frontend feature page
-> Backend initiate API
-> Gateway checkout page
-> Backend callback/redirect endpoint
-> Shared payment result page
-> Frontend next destinationThis means the payment result page is a single reusable screen for all payment scopes and all gateways.
End-to-End Sequence
Backend Responsibilities
1. Initiation
During payment initiation the backend:
- creates a pending payment record
- stores the final frontend destination in
payment.metadata.redirectUrl - generates backend-owned callback URLs:
/api/payments/redirect/:paymentId/success/api/payments/redirect/:paymentId/failure
- passes those callback URLs to the gateway instead of sending the gateway directly back to the frontend
This logic lives in:
2. Redirect Verification
When the gateway returns, the backend:
- loads the payment record using
paymentId - parses the gateway return payload
- verifies the payment
- marks the payment as completed or failed
- emits domain events
- redirects to the single payment result page
For eSewa, the backend also decodes the base64 data payload and verifies the signature plus the status API response.
Frontend Responsibilities
The frontend should do only two payment-specific things:
- initiate payment and submit the gateway form/redirect
- implement the shared
/payments/resultpage when a frontend is available
It should not call a verify endpoint after the user returns from the gateway.
Shared Result Page Contract
The backend redirects the browser to a single result page:
${PAYMENT_RESULT_PAGE_URL}If PAYMENT_RESULT_PAGE_URL is configured, the browser is redirected there.
If PAYMENT_RESULT_PAGE_URL is not configured, the backend uses its temporary fallback page:
${API_PUBLIC_BASE_URL}/api/payments/resultThis backend fallback page is intended for local development and backend-only environments where no frontend result screen exists yet.
Query Parameters Sent To The Result Page
| Param | Meaning | Example |
|---|---|---|
payment_status | Final verified status | completed |
payment_id | Internal payment row ID | 57 |
reference_type | What the payment was for | subscription |
reference_id | ID of the paid entity | 1a2b3c... |
next | Final frontend destination after showing result UI | https://app.example.com/subscriptions/1a2b3c |
subscription_id | Present when reference_type=subscription | 1a2b3c... |
order_id | Present when reference_type=order | 128 |
Result Page Behavior
The result page should:
- read the query params
- show a gateway-agnostic success/failure UI
- decide how long to remain visible
- redirect to
next
Recommended Frontend Implementation
import { useEffect, useMemo } from "react";
import { useSearchParams, useRouter } from "next/navigation";
export default function PaymentResultPage() {
const params = useSearchParams();
const router = useRouter();
const status = params.get("payment_status");
const next = params.get("next");
const referenceType = params.get("reference_type");
const referenceId = params.get("reference_id");
const isSuccess = status === "completed";
const title = useMemo(() => {
if (isSuccess) return "Payment successful";
return "Payment failed";
}, [isSuccess]);
const description = useMemo(() => {
if (referenceType === "subscription" && isSuccess) {
return "Your subscription has been activated.";
}
if (referenceType === "order" && isSuccess) {
return "Your order payment has been confirmed.";
}
if (referenceType === "subscription") {
return "We could not complete your subscription payment.";
}
if (referenceType === "order") {
return "We could not complete your order payment.";
}
return isSuccess
? "Your payment has been confirmed."
: "Your payment could not be completed.";
}, [isSuccess, referenceType]);
useEffect(() => {
if (!next) return;
const timeout = window.setTimeout(() => {
router.replace(next);
}, isSuccess ? 1800 : 2500);
return () => window.clearTimeout(timeout);
}, [router, next, isSuccess]);
return (
<main>
<h1>{title}</h1>
<p>{description}</p>
<p>Reference: {referenceId}</p>
{next ? <p>Redirecting...</p> : null}
</main>
);
}Temporary Backend Fallback Result Page
When no frontend result page exists, the API serves a temporary HTML result page at:
/api/payments/resultThat page:
- shows success or failure
- displays the payment and reference IDs
- shows the
nextURL - automatically redirects the browser to
nextafter a short delay
This lets the entire payment flow remain usable before the frontend implements /payments/result.
Subscription Scenario
Sequence
What The Frontend Sends
The subscription purchase request should use returnUrl as the final destination after payment flow completes, for example:
https://app.example.com/subscriptions/3e8ce1d8-...The frontend is not telling eSewa where to return. It is telling the backend where the user should finally land after backend verification finishes.
What The User Sees
- Subscription purchase screen
- eSewa checkout screen
- shared payment result page
- subscription detail page
Order Scenario
Sequence
Recommended Final Destination
For orders, returnUrl should usually be:
https://app.example.com/orders/:orderIdThe shared result page can then show a neutral message like:
- "Payment successful. Redirecting to your order..."
- "Payment failed. Redirecting back to your order..."
eSewa-Specific Notes In This Unified Model
eSewa currently returns:
- success: encoded
datapayload - failure: browser redirect to failure URL
In the new backend-owned flow:
- eSewa returns to the API, not the frontend
- the API decodes
data - the API verifies signature and status
- the API redirects to the shared result page
So even though eSewa has a special encoded success response, the frontend still sees the same generic result-page contract as every other gateway.
Future Gateway Compatibility
This flow is designed to survive gateway differences.
eSewa
- initiation:
form_post - return shape: encoded
data - verification: signature + status API
connectIPS
- initiation: likely
form_post - return shape: gateway-specific form/query fields
- verification: gateway-specific callback validation
Fonepay
- initiation:
redirect - return shape: query parameters or callback fields
- verification: gateway-specific checksum / status check
Khalti
- initiation: likely
sdk - return shape: token / pidx / lookup response
- verification: backend lookup API
As long as each gateway implementation can:
- initiate payment
- verify payment
- optionally parse its redirect payload
the browser flow stays the same.
QA Checklist
- Payment initiation returns
paymentId,redirectUrl, andgatewayPayload - Gateway success returns to
/api/payments/redirect/:paymentId/success - Gateway failure returns to
/api/payments/redirect/:paymentId/failure - Backend verifies before redirecting to the shared result page
- Frontend
/payments/resultworks for both subscription and order when implemented - Backend fallback
/api/payments/resultworks when no frontend result page exists nextis preserved correctly- Refreshing the callback URL is idempotent
- Refreshing the result page does not re-trigger verification
Current Backend Status
The subscription browser flow now uses backend-owned verification and no longer needs a public verify-payment endpoint. The shared result-page redirect is implemented in the API. If the frontend does not yet provide /payments/result, the backend fallback page at /api/payments/result handles the return flow temporarily.