CLONESAHIBINDEN
FRONTEND GUIDE FOR AI CODING AGENTS - PART 11 - Listing Service Listing Payment Flow
This document is a part of a REST API guide for the clonesahibinden project. It is designed for AI agents that will generate frontend code to consume the project’s backend.
Stripe Payment Flow For Listing
Listing is a data object that stores order information
used for Stripe payments. The payment flow can only start after an
instance of this data object is created in the database.
The ID of this data object—referenced as listingId in the
general business logic—will be used as the orderId in the
payment flow.
Accessing the service API for the payment flow API
The Clonesahibinden application doesn’t have a separate payment
service; the payment flow is handled within the same service that
manages orders. To access the related APIs, use the base URL of the
listing service. Note that the application may be
deployed to Preview, Staging, or Production. As with all API access,
you should call the API using the base URL for the selected
deployment.
For the listing service, the base URLs are:
-
Preview:
https://clonesahibinden.prw.mindbricks.com/listing-api -
Staging:
https://clonesahibinden-stage.mindbricks.co/listing-api -
Production:
https://clonesahibinden.mindbricks.co/listing-api
Creating the Listing
While creating the listing instance is part of the
business logic and can be implemented according to your architecture,
this instance acts as the central hub for the payment flow and its
related data objects. The order object is typically created via its
own API (see the Business API for the create route of
listing). The payment flow begins
after the object is created.
Because of the data object’s Stripe order settings, the payment flow is aware of the following fields, references, and their purposes:
-
id(used asorderIdor${dataObject.objectName}Id): The unique identifier of the data object instance at the center of the payment flow. -
orderId: The order identifier is resolved fromthis.listing.id. -
amount: The payment amount is resolved fromthis.listing.price. -
currency: The payment currency is resolved fromthis.listing.currency. -
description: The payment description is resolved fromPremium upgrade for listing ${this.listing.title}. -
orderStatusProperty:statusis updated automatically by the payment flow using a mapped status value. -
orderStatusUpdateDateProperty:updatedAtstores the timestamp of the latest payment status update. -
orderOwnerIdProperty:userIdis used by the payment flow to verify the order owner and match it with the current user’s ID. -
mapPaymentResultToOrderStatus: The order status is written to the data object instance using the following mapping.
paymentResultStarted:"pending_review"
paymentResultCanceled:"pending_review"
paymentResultFailed:"denied"
paymentResultSuccess:this.listing.status === "pending_review" ? "active" : this.listing.status
Before Payment Flow Starts
It is assumed that the frontend provides a “Pay” or
“Checkout” button that initiates the payment flow.
The following steps occur after the user clicks this button.
Note that an listing instance must already exist to
represent the order being paid, with its initial status set.
A Stripe payment flow can be implemented in several ways, but the best
practice is to use a PaymentIntent and manage it
jointly from the backend and frontend.
A PaymentIntent represents the intent to collect payment for
a given order (or any payable entity).
In the Clonesahibinden application, the
PaymentIntent is created in the backend, while the
PaymentMethod (the user’s stored card information) is
created in the frontend.
Only the PaymentMethod ID and minimal metadata are stored in the
backend for later reference.
The frontend first requests the current user’s saved payment methods
from the backend, displays them in a list, and provides UI options to
add or remove payment methods.
The user must select a Payment Method before starting the payment
flow.
Listing the Payment Methods for the User
To list the payment methods of the currently logged-in user, call the following system API (unversioned):
GET /payment-methods/list
This endpoint requires no parameters and returns an array of payment methods belonging to the user — without any envelope.
const response = await fetch("$serviceUrl/payment-methods/list", {
method: "GET",
headers: { "Content-Type": "application/json" },
});
Example response:
[
{
"id": "19a5fbfd-3c25-405b-a7f7-06f023f2ca01",
"paymentMethodId": "pm_1SQv9CP5uUv56Cse5BQ3nGW8",
"userId": "f7103b85-fcda-4dec-92c6-c336f71fd3a2",
"customerId": "cus_TNgWUw5QkmUPLa",
"cardHolderName": "John Doe",
"cardHolderZip": "34662",
"platform": "stripe",
"cardInfo": {
"brand": "visa",
"last4": "4242",
"checks": {
"cvc_check": "pass",
"address_postal_code_check": "pass"
},
"funding": "credit",
"exp_month": 11,
"exp_year": 2033
},
"isActive": true,
"createdAt": "2025-11-07T19:16:38.469Z",
"updatedAt": "2025-11-07T19:16:38.469Z",
"_owner": "f7103b85-fcda-4dec-92c6-c336f71fd3a2"
}
]
In each payment method object, the following fields are useful for displaying to the user:
for (const method of paymentMethods) {
const brand = method.cardInfo.brand; // use brand for displaying VISA/MASTERCARD icons
const paymentMethodId = method.paymentMethodId; // send this when initiating the payment flow
const cardHolderName = method.cardHolderName; // show in list
const number = `**** **** **** ${method.cardInfo.last4}`; // masked card number
const expDate = `${method.cardInfo.exp_month}/${method.cardInfo.exp_year}`; // expiry date
const id = method.id; // internal DB record ID, used for deletion
const customerId = method.customerId; // Stripe customer reference
}
If the list is empty, prompt the user to add a new payment method.
Creating a Payment Method
The payment page (or user profile page) should allow users to add a new payment method (credit card). Creating a Payment Method is a secure operation handled entirely through Stripe.js on the frontend — the backend never handles sensitive card data. After a card is successfully created, the backend only stores its reference (PaymentMethod ID) for reuse.
Stripe provides multiple ways to collect card information, all through secure UI elements. Below is an example setup — refer to the latest Stripe documentation for alternative patterns.
To initialize Stripe on the frontend, include your public key:
<script src="https://js.stripe.com/v3/?advancedFraudSignals=false"></script>
const stripe = Stripe("pk_test_51POkqt4..................");
const elements = stripe.elements();
const cardNumberElement = elements.create("cardNumber", {
style: { base: { color: "#545454", fontSize: "16px" } },
});
cardNumberElement.mount("#card-number-element");
const cardExpiryElement = elements.create("cardExpiry", {
style: { base: { color: "#545454", fontSize: "16px" } },
});
cardExpiryElement.mount("#card-expiry-element");
const cardCvcElement = elements.create("cardCvc", {
style: { base: { color: "#545454", fontSize: "16px" } },
});
cardCvcElement.mount("#card-cvc-element");
// Note: cardholder name and ZIP code are collected via non-Stripe inputs (not secure).
You can dynamically show the card brand while typing:
cardNumberElement.on("change", (event) => {
const cardBrand = event.brand;
const cardNumberDiv = document.getElementById("card-number-element");
cardNumberDiv.style.backgroundImage = getBrandImageUrl(cardBrand);
});
Once the user completes the card form, create the Payment Method on Stripe. Note that the expiry and CVC fields are securely handled by Stripe.js and are never readable from your code.
const { paymentMethod, error } = await stripe.createPaymentMethod({
type: "card",
card: cardNumberElement,
billing_details: {
name: cardholderName.value,
address: { postal_code: cardholderZip.value },
},
});
When a paymentMethod is successfully created, send its ID
to your backend to attach it to the logged-in user’s account.
Use the system API (unversioned):
POST /payment-methods/add
Example:
const response = await fetch("$serviceUrl/payment-methods/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
});
When addPaymentMethod is called, the backend retrieves or
creates the user’s Stripe Customer ID, attaches the Payment Method to
that customer, and stores the reference in the local database for
future use.
Example response:
{
"isActive": true,
"cardHolderName": "John Doe",
"userId": "f7103b85-fcda-4dec-92c6-c336f71fd3a2",
"customerId": "cus_TNgWUw5QkmUPLa",
"paymentMethodId": "pm_1SQw5aP5uUv56CseDGzT1dzP",
"platform": "stripe",
"cardHolderZip": "34662",
"cardInfo": {
"brand": "visa",
"last4": "4242",
"funding": "credit",
"exp_month": 11,
"exp_year": 2033
},
"id": "19a5ff70-4986-4760-8fc4-6b591bd6bbbf",
"createdAt": "2025-11-07T20:16:55.451Z",
"updatedAt": "2025-11-07T20:16:55.451Z"
}
You can append this new entry directly to the UI list or refresh the
list using the listPaymentMethods API.
Deleting a Payment Method
To remove a saved payment method from the current user’s account, call the system API (unversioned):
DELETE /payment-methods/delete/:paymentMethodId
Example:
await fetch(
`$serviceUrl/payment-methods/delete/${paymentMethodId}`,
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
}
);
Starting the Payment Flow in Backend — Creation and Confirmation of the PaymentIntent Object
The payment flow is initiated in the backend through the
startListingPayment API.
This API must be called with one of the user’s existing payment
methods. Therefore, ensure that the frontend
forces the user to select a payment method before
initiating the payment.
The startListingPayment API is a versioned
Business Logic API and follows the same structure as
other business APIs.
In the Clonesahibinden application, the payment flow starts by
creating a Stripe PaymentIntent and confirming it in
a single step within the backend.
In a typical (“happy”) path, when the
startListingPayment API is called, the response will
include a successful or failed PaymentIntent result inside the
paymentResult object, along with the
listing object.
However, in certain edge cases—such as when 3D Secure (3DS) or other
bank-level authentication is required—the confirmation step cannot
complete immediately.
In such cases, control should return to a frontend page to allow the
user to finish the process.
To enable this, a return_url must be
provided during the PaymentIntent creation step.
Although technically optional, it is
strongly recommended to include a
return_url.
This ensures that the frontend payment result page can display both
successful and failed payments and complete flows that require user
interaction.
The return_url must be a frontend URL.
The paymentUserParams parameter of the
startListingPayment API contains the data necessary to
create the Stripe PaymentIntent.
Call the API as follows:
const response = await fetch(
`$serviceUrl/v1/startlistingpayment/${orderId}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
paymentUserParams: {
paymentMethodId,
return_url: `${yourFrontendReturnUrl}`,
},
}),
}
);
The API response will contain a paymentResult object. If
an error occurs, it will begin with
{ "result": "ERR" }. Otherwise, it
will include the PaymentIntent information:
{
"paymentResult": {
"success": true,
"paymentTicketId": "19a60f8f-eeff-43a2-9954-58b18839e1da",
"orderId": "19a60f84-56ee-40c4-b9c1-392f83877838",
"paymentId": "pi_3SR0UHP5uUv56Cse1kwQWCK8",
"paymentStatus": "succeeded",
"paymentIntentInfo": {
"paymentIntentId": "pi_3SR0UHP5uUv56Cse1kwQWCK8",
"clientSecret": "pi_3SR0UHP5uUv56Cse1kwQWCK8_secret_PTc3DriD0YU5Th4isBepvDWdg",
"publicKey": "pk_test_51POkqWP5uU",
"status": "succeeded"
},
"statusLiteral": "success",
"amount": 10,
"currency": "USD",
"description": "Your credit card is charged for babilOrder for 10",
"metadata": {
"order": "Purchase-Purchase-order",
"orderId": "19a60f84-56ee-40c4-b9c1-392f83877838",
"checkoutName": "babilOrder"
},
"paymentUserParams": {
"paymentMethodId": "pm_1SQw5aP5uUv56CseDGzT1dzP",
"return_url": "${yourFrontendReturnUrl}"
}
}
}
Start Listingpayment API
Start payment for listing
Rest Route
The startListingPayment API REST controller can be
triggered via the following route:
/v1/startlistingpayment/:listingId
Rest Request Parameters
The startListingPayment api has got 2 regular request
parameters
| Parameter | Type | Required | Population |
|---|---|---|---|
| listingId | ID | true | request.params?.[“listingId”] |
| paymentUserParams | Object | false | request.body?.[“paymentUserParams”] |
| listingId : This id paremeter is used to select the required data object that will be updated | |||
| paymentUserParams : The user parameters that should be defined to start a stripe payment process |
REST Request To access the api you can use the REST controller with the path PATCH /v1/startlistingpayment/:listingId
axios({
method: 'PATCH',
url: `/v1/startlistingpayment/${listingId}`,
data: {
paymentUserParams:"Object",
},
params: {
}
});
REST Response
{
"status": "OK",
"statusCode": "200",
"elapsedMs": 126,
"ssoTime": 120,
"source": "db",
"cacheKey": "hexCode",
"userId": "ID",
"sessionId": "ID",
"requestId": "ID",
"dataName": "listing",
"method": "PATCH",
"action": "update",
"appVersion": "Version",
"rowCount": 1,
"listing": {
"id": "ID",
"attributes": "Object",
"categoryId": "ID",
"condition": "Enum",
"condition_idx": "Integer",
"contactEmail": "String",
"contactPhone": "String",
"currency": "String",
"description": "Text",
"expiresAt": "Date",
"favoriteCount": "Integer",
"isPremium": "Boolean",
"listingType": "Enum",
"listingType_idx": "Integer",
"locationId": "ID",
"_paymentConfirmation": "String",
"premiumExpiry": "Date",
"premiumType": "Enum",
"premiumType_idx": "Integer",
"price": "Double",
"status": "Enum",
"status_idx": "Integer",
"subcategoryId": "ID",
"title": "String",
"userId": "ID",
"viewsCount": "Integer",
"paymentConfirmation": "Enum",
"paymentConfirmation_idx": "Integer",
"isActive": true,
"recordVersion": "Integer",
"createdAt": "Date",
"updatedAt": "Date",
"_owner": "ID"
},
"paymentResult": {
"paymentTicketId": "ID",
"orderId": "ID",
"paymentId": "String",
"paymentStatus": "Enum",
"paymentIntentInfo": "Object",
"statusLiteral": "String",
"amount": "Double",
"currency": "String",
"success": true,
"description": "String",
"metadata": "Object",
"paymentUserParams": "Object"
}
}
Analyzing the API Response
After calling the startListingPayment API, the most
common expected outcome is a confirmed and completed payment. However,
several alternate cases should be handled on the frontend.
System Error Case
The API may return a classic service-level error (unrelated to
payment). Check the HTTP status code of the response. It should be
200 or 201. Any 400,
401, 403, or 404 indicates a
system error.
{
"result": "ERR",
"status": 404,
"message": "Record not found",
"date": "2025-11-08T00:57:54.820Z"
}
Handle system errors on the payment page (show a retry option). Do not navigate to the result page.
Payment Error Case
The API performs both database operations and the Stripe payment
operation. If the payment fails but the service logic succeeds, the
API may still return a 200 OK status, with the failure
recorded in the paymentResult.
In this case, show an error message and allow the user to retry.
{
"status": "OK",
"statusCode": "200",
"listing": {
"id": "19a60f8f-eeff-43a2-9954-58b18839e1da",
"status": "failed"
},
"paymentResult": {
"result": "ERR",
"status": 500,
"message": "Stripe error message: Your card number is incorrect.",
"errCode": "invalid_number",
"date": "2025-11-08T00:57:54.820Z"
}
}
Payment errors should be handled on the payment page (retry option). Do not go to the result page.
Happy Case
When both the service and payment result succeed, this is considered
the happy path. In this case, use the
listing and paymentResult objects in the
response to display a success message to the user.
amount and description values are included
to help you show payment details on the result page.
{
"status": "OK",
"statusCode": "200",
"order": {
"id": "19a60f8f-eeff-43a2-9954-58b18839e1da",
"status": "paid"
},
"paymentResult": {
"success": true,
"paymentStatus": "succeeded",
"paymentIntentInfo": {
"status": "succeeded"
},
"amount": 10,
"currency": "USD",
"description": "Your credit card is charged for babilOrder for 10"
}
}
To verify success:
if (paymentResult.paymentIntentInfo.status === "succeeded") {
// Redirect to result page
}
Note: A successful result does not trigger fulfillment immediately. Fulfillment begins only after the Stripe webhook updates the database. It’s recommended to show a short “success” toast, wait a few milliseconds, and then navigate to the result page.
Handle the happy case in the result page by sending
the listingId and the payment intent secret.
const orderId = new URLSearchParams(window.location.search).get("orderId");
const url = new URL(`$yourResultPageUrl`, location.origin);
url.searchParams.set("orderId", orderId);
url.searchParams.set("payment_intent_client_secret", currentPaymentIntent.clientSecret);
setTimeout(() => { window.location.href = url.toString(); }, 600);
Edge Cases
Although startListingPayment is designed to handle both
creation and confirmation in one step, Stripe may return an incomplete
result if third-party authentication or redirect steps are required.
You must handle these cases in both the payment page and the result page, because some next actions are available immediately, while others occur only after a redirect.
If the paymentIntentInfo.status equals
"requires_action", handle it using Stripe.js as
shown below:
if (paymentResult.paymentIntentInfo.status === "requires_action") {
await runNextAction(
paymentResult.paymentIntentInfo.clientSecret,
paymentResult.paymentIntentInfo.publicKey
);
}
Helper function:
async function runNextAction(clientSecret, publicKey) {
const stripe = Stripe(publicKey);
const { error } = await stripe.handleNextAction({ clientSecret });
if (error) {
console.log("next_action error:", error);
showToast(error.code + ": " + error.message, "fa-circle-xmark text-red-500");
throw new Error(error.message);
}
}
After handling the next action, re-fetch the PaymentIntent from Stripe, evaluate its status, show appropriate feedback, and navigate to the result page.
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
if (paymentIntent.status === "succeeded") {
showToast("Payment successful!", "fa-circle-check text-green-500");
} else if (paymentIntent.status === "processing") {
showToast("Payment is processing…", "fa-circle-info text-blue-500");
} else if (paymentIntent.status === "requires_payment_method") {
showToast("Payment failed. Try another card.", "fa-circle-xmark text-red-500");
}
const orderId = new URLSearchParams(window.location.search).get("orderId");
const url = new URL(`$yourResultPageUrl`, location.origin);
url.searchParams.set("orderId", orderId);
url.searchParams.set("payment_intent_client_secret", currentPaymentIntent.clientSecret);
setTimeout(() => { window.location.href = url.toString(); }, 600);
The Result Page
The payment result page should handle the following steps:
-
Read
orderIdandpayment_intent_client_secretfrom the query parameters. - Retrieve the PaymentIntent from Stripe and check its status.
-
If required, handle any
next_actionand re-fetch the PaymentIntent. -
If the status is
"succeeded", display a clear visual confirmation. -
Fetch the
listinginstance from the backend to display any additional order or fulfillment details.
Note that paymentIntent status only gives information about the Stripe
side. The listing instance in the service should also ve
updated to start the fulfillment. In most cases, the
startlistingPayment api updates the status of the order
using the response of the paymentIntent confirmation, but as stated
above in some cases this update can be done only when the webhook
executes. So in teh result page always get the final payment status in
the `listing.
To ensure that service i To fetch the listing instance,
you van use the related api which is given before, and to ensure that
the service is updated with the latest status read the
paymentConfirmation field of the listing instance.
if (listing.paymentConfirmation == "canceled") {
// the payment is canceled, user can be informed that they should try again
} if (listing.paymentConfirmation == "paid") {
// service knows that payment is done, user can be informed that fullfillment started
} else {
// it may be pending, processing
// Fetch the object again until a canceled or paid status
}
Payment Flow via MCP (AI Chat Integration)
The payment flow is also accessible through the MCP (Model Context
Protocol) AI chat interface. The listing service exposes
an initiatePayment MCP tool that the AI can call when the
user wants to pay for an order.
How initiatePayment Works in MCP
- User asks to pay — e.g., “I want to pay for my order”
-
AI calls
initiatePaymentMCP tool withorderId(andorderTypeif multiple order types exist) - Tool validates the order exists, is payable, and the user is authorized
-
Tool returns
__frontendActionwithtype: "payment"— this is NOT a direct payment execution -
Frontend chat UI renders a
PaymentActionCardwith a “Pay Now” button -
User clicks “Pay Now” — the frontend opens a
payment modal with
CheckoutForm - Standard Stripe flow proceeds (payment method selection, 3DS handling, etc.)
Frontend Action Response Format
The initiatePayment MCP tool returns:
{
"__frontendAction": {
"type": "payment",
"orderId": "uuid",
"orderType": "listing",
"serviceName": "listing",
"amount": 99.99,
"currency": "USD",
"description": "Order description"
},
"message": "Payment is ready. Click the button below to proceed."
}
MCP Client Architecture
The frontend communicates with MCP tools through the MCP BFF (Backend-for-Frontend) service. The MCP BFF aggregates tool calls across all backend services and provides:
-
SSE Streaming: Chat messages stream via
/api/chat/streamwith event types:start,text,tool_start,tool_executing,tool_result,error,done -
Tool Result Extraction: The frontend’s
MessageBubblecomponent inspects tool results for__frontendActionfields -
Action Dispatch: The
ActionCardcomponent dispatches to type-specific cards (e.g.,PaymentActionCardfortype: "payment")
The PaymentActionCard component handles the rest:
fetching order details, rendering the payment UI, and completing the
Stripe checkout flow — all within the chat interface.