Skip to main content

Build Payment Verification and Order Controllers

This page focuses on concepts and patterns for building payment gateway controllers.

Controllers are server-side modules that handle HTTP requests, orchestrate business logic, and coordinate between different services (Platform APIs, payment gateways, databases). In extensions, controllers manage the critical flow of order creation, payment verification, and checkout completion.

Controllers handle three main responsibilities:

  1. Request Handling: Receive and validate HTTP requests
  2. Business Logic: Orchestrate complex workflows (order creation, payment verification)
  3. Service Coordination: Coordinate between Platform APIs, payment gateways, and databases

Controllers integrate with platform systems through:

  • FDK (Platform SDK): Access platform services (payment updates, coupon management)
  • Database: Store orders, transactions, and metadata

Controllers implement the backend logic for two main endpoints:

  1. Order Creation Controller: Handles the buy-now endpoint called by your injected script. This creates payment gateway orders and returns modal configuration.
  2. Payment Verification Controller: Handles the verify endpoint called by your payment handler. This verifies payments, completes checkout, and updates payment status.

The implementation details for these controllers are also referenced on the Script Injection and Verify Payment pages, which show how frontend code interacts with them.

The controller acts as the central orchestration layer that:

  • Receives requests from frontend/routes
  • Validates input data
  • Calls platform APIs (like Fynd GraphQL/FDK)
  • Integrates with payment gateway APIs
  • Manages database operations
  • Returns structured responses

How Controllers Work

The controller sits between routes and services, orchestrating the flow without directly handling HTTP concerns.

  1. Route receives request and passes to controller
  2. Controller validates input and extracts context
  3. Controller calls services (platform APIs, payment gateway, database)
  4. Controller processes results and handles errors
  5. Controller returns response to route/client
HTTP Request → Route → Controller → [Platform API | Payment Gateway | Database] → Response

Main Controller Functions

1. Order Creation Function

Creates payment gateway orders from platform cart data. Transform platform cart into payment gateway order format, create order with gateway, save to database. This function is called by your buy-now endpoint, which receives requests from the injected script when users click Buy Now. The detailed step-by-step implementation is covered in the Script Injection page.

For guest user, the request body includes is_anonymous_cart and cart_id. Read these from the request, fetch the cart using cart_id (and when is_anonymous_cart:true, use the platform's anonymous-cart header so the cart is resolved by ID). Persist cart_id, is_anonymous_cart, and user_id (if logged in) in the order meta so the verify step can run user resolution and platform checkout with the correct headers.

// Example: Order creation function structure
exports.createOrder = asyncHandler(async (req, res) => {
try {
// Step 1: Extract context (application/company IDs)
const { applicationId, companyId } = extractContext(req);
// For anonymous cart: read from request and pass to getPlatformCartData / order meta
const isAnonymousCart = Boolean(req.body?.is_anonymous_cart);
const userId = req.body?.user_id ?? null;

// Step 2: Fetch cart data from platform (use cart_id from body when provided; for anonymous, use anonymous-cart header)
const cartId = req.body?.cart_id ?? null;
const cart = await getPlatformCartData(req, cartId, isAnonymousCart);

// Step 3: Validate cart
if (!cart.is_valid || cart.items.length === 0) {
return res.status(400).json({
success: false,
message: "Cart is empty",
});
}

// Step 4: Get payment gateway credentials
const credentials = await getPaymentGatewayCredentials(applicationId);

// Step 5: Transform cart to payment gateway format
const lineItems = transformCartToLineItems(cart);
const orderAmount = calculateOrderAmount(cart);

// Step 6: Create order with payment gateway
const gatewayOrder = await createGatewayOrder(
{
amount: orderAmount,
currency: cart.currency,
line_items: lineItems,
notes: { cart_id: cart.id },
},
credentials,
);

// Step 7: Save order to database (include is_anonymous_cart and user_id for guest flow)
const order = await saveOrderToDatabase({
gateway_order_id: gatewayOrder.id,
amount: orderAmount,
cart_id: cart.id,
meta: {
cart,
gateway_order: gatewayOrder,
is_anonymous_cart: isAnonymousCart ?? false,
user_id: userId ?? null,
},
});

// Step 8: Return popup configuration
return res.json({
success: true,
popup_config: {
key: credentials.public_key,
amount: gatewayOrder.amount,
order_id: gatewayOrder.id,
},
order_id: order.id,
});
} catch (error) {
logger.error("Order creation failed:", error);
return res.status(500).json({
success: false,
message: error.message,
});
}
});

2. Payment Verification Function

Verifies payment signatures and completes platform checkout. Verify payment authenticity, extract addresses, complete platform checkout, update payment status.

This function is called by your verify endpoint, which receives requests from the payment handler after users complete payment. The complete verification flow and response structure are documented in the Verify Payment page.

For guest user's orders, the verification flow includes user resolution, checkout with the anonymous-cart header, and post-payment cart clearing. See Anonymous cart: user resolution and checkout and Anonymous cart: post-payment cart clearing below.

// Example: Payment verification function structure
exports.verifyPayment = asyncHandler(async (req, res) => {
try {
// Step 1: Extract payment data
const { payment_id, order_id, signature, internal_order_id } = req.body;

// Step 2: Find order in database
const order = await Order.findOne({ id: internal_order_id });
if (!order) {
return res.status(404).json({
success: false,
message: "Order not found",
});
}

// Step 3: Get credentials for signature verification
// Use app_id from order meta (as stored during order creation)
const applicationId = order.meta?.app_id || order.app_id;
if (!applicationId) {
throw new Error(
`Missing application_id in order for order ${internal_order_id}`,
);
}
const credentials = await getPaymentGatewayCredentials(applicationId);

// Step 4: Verify payment signature
// Signature format varies by payment gateway (e.g., "order_id|payment_id")
const isAuthentic = verifyPaymentSignature(
order_id,
payment_id,
signature,
credentials.secret_key,
);

if (!isAuthentic) {
return res.status(400).json({
success: false,
message: "Payment signature verification failed",
});
}

// Step 5: Update transaction status immediately after verification
await updateTransaction(internal_order_id, {
aggregator_payment_id: payment_id,
current_status: "complete",
});

// Step 6: Fetch order details from payment gateway (for addresses)
// This is needed to extract shipping/billing addresses
const gatewayOrderDetails = await fetchGatewayOrderDetails(
order_id,
credentials,
);

// Step 7: Extract addresses from payment gateway order details
// Address format varies by payment gateway
const addresses = extractAddresses(gatewayOrderDetails);

// Step 8: For anonymous cart, resolve user_id (search by phone then create if not found) before checkout
let userId = order.meta?.user_id ?? null;
if (order.meta?.is_anonymous_cart) {
userId = await resolveUserIdForAnonymousOrder(order, gatewayOrderDetails);
}
// Step 9: Complete platform checkout with addresses (and anonymous-cart header when applicable)
const buyNow = order.meta?.buyNow || false;
const checkoutResult = await completePlatformCheckout(
order,
addresses,
req,
buyNow,
userId,
);

// Step 10: Optionally clear anonymous cart after successful checkout (non-blocking). Call platform deleteCart with cart_id and x-anonymous-cart: true.
if (order.meta?.is_anonymous_cart && checkoutResult.success) {
await clearAnonymousCartIfNeeded(order);
}
// Step 11: Update payment status in platform via FDK
// This should be done AFTER successful platformCheckoutCartV2
// Payment update uses FDK's payment.updatePaymentSession() method
if (checkoutResult.success && checkoutResult.merchant_transaction_id) {
// Skip payment update for COD (handled by platform automatically)
const paymentMethod = await getPaymentMethodFromGateway(
payment_id,
credentials,
);
if (paymentMethod !== "cod") {
await updatePlatformPayment(
checkoutResult.merchant_transaction_id,
payment_id,
order_id,
credentials,
order,
);
}
}

// Step 12: Return comprehensive verification result
return res.json({
success: true,
message: "Payment verified successfully",
data: {
order_id: internal_order_id,
payment_id: payment_id,
status: "verified",
gateway_order_details: gatewayOrderDetails
? {
id: gatewayOrderDetails.id,
status: gatewayOrderDetails.status,
has_customer_details: !!gatewayOrderDetails.customer_details,
}
: null,
platform_checkout: {
order_id: checkoutResult.order_id,
merchant_transaction_id: checkoutResult.merchant_transaction_id,
success: checkoutResult.success,
message: checkoutResult.message,
},
payment_verification: {
success: true,
merchant_transaction_id: checkoutResult.merchant_transaction_id,
},
},
});
} catch (error) {
logger.error("Payment verification failed:", error);
return res.status(500).json({
success: false,
message: error.message,
});
}
});

3. Helper Functions

Controllers use helper functions for reusable operations. Provide fallbacks for different header formats.

  • Build GraphQL URL dynamically from request origin
  • Forward authorization headers
  • Handle GraphQL errors separately from data
  • Return cart data structure

Extract Context

Extract application and company identifiers from request headers. This context is needed for all subsequent operations in the controller, including credential retrieval and order creation. The context extraction pattern is used in both Order Creation and Payment Verification functions.

// Example: Extract application/company IDs
function extractContext(req) {
// Try headers first
const appDataHeader = req.headers["x-application-data"];
const userDataHeader = req.headers["x-user-data"];

if (appDataHeader) {
const appData = JSON.parse(appDataHeader);
return {
applicationId: appData.id,
companyId: appData.company_id,
};
}

// Fallback to user data header
if (userDataHeader) {
const userData = JSON.parse(userDataHeader);
return {
applicationId: userData.application_id,
companyId: userData.company_id,
};
}

throw new Error("Missing application context in headers");
}

Get Platform Cart Data

Fetch cart data from the platform GraphQL API. This helper function is used in the Order Creation Function to retrieve cart details needed for payment gateway order creation. The cart data includes items, prices, breakup values, and coupon information. When isAnonymousCart:true, pass the platform's anonymous-cart header (e.g. x-anonymous-cart: true) so the cart is resolved by cart_id only.

// Example: Fetch cart from platform GraphQL API (pass isAnonymousCart to send x-anonymous-cart header when true)
async function getPlatformCartData(
req,
cartId = null,
isAnonymousCart = false,
) {
// Build GraphQL URL from request origin
const baseUrl =
req.headers["origin"] || req.protocol + "://" + req.get("host");
const graphqlUrl = `${baseUrl}/api/service/application/graphql/`;

// Build GraphQL query
const query = `
query Cart($cartId: String) {
cart(id: $cartId) {
id
items {
quantity
product { name, uid }
price { base { effective } }
}
breakup_values {
raw {
total
subtotal
delivery_charge
}
}
}
}
`;

// Get authorization from request
const authorization =
req.headers["authorization"] || req.headers["Authorization"];

// For anonymous cart, add header so platform resolves cart by ID only
const headers = {
authorization: authorization,
"content-type": "application/json",
};
if (isAnonymousCart) {
headers["x-anonymous-cart"] = "true";
}
// Call platform API
const response = await axios.post(
graphqlUrl,
{
query,
variables: { cartId },
},
{ headers },
);

if (response.data.errors) {
throw new Error("GraphQL errors: " + JSON.stringify(response.data.errors));
}

return response.data.data.cart;
}

Get Payment Gateway Credentials

  • Store credentials encrypted in database
  • Decrypt using extension secret
  • Validate credential structure
  • Return in format needed by payment gateway SDK
  • Always retrieve from database
// Example: Fetch and decrypt credentials
async function getPaymentGatewayCredentials(appId) {
// Find encrypted credentials in database
const secret = await Secret.findOne({ app_id: appId });
if (!secret) {
throw new Error(`No credentials found for app_id: ${appId}`);
}

// Decrypt credentials using extension encryption secret
const credentials = EncryptHelper.decrypt(
config.extension.encrypt_secret,
secret.secrets,
);

// Validate structure (field names vary by payment gateway)
if (!credentials.key_id || !credentials.key_secret) {
throw new Error(
"Invalid credentials structure - missing key_id or key_secret",
);
}

// Return in format needed by payment gateway SDK
// Field names may vary: key_id/key_secret, api_key/api_secret, etc.
return {
key_id: credentials.key_id, // Public key (for frontend)
key_secret: credentials.key_secret, // Secret key (for backend only)
};
}

Verify Payment Signature

  • Signature format varies by payment gateway (e.g., "order_id|payment_id")
  • Use HMAC SHA256 for signature generation (most common)
  • Use constant-time comparison to prevent timing attacks
  • Never log full signatures (log only first few characters for debugging)
  • Always verify signature before processing payment
// Example: Verify payment signature
function verifyPaymentSignature(orderId, paymentId, signature, secretKey) {
// Build signature string (format depends on payment gateway)
// Common format: "order_id|payment_id" or "payment_id|order_id"
const signatureString = `${orderId}|${paymentId}`;

// Generate expected signature using HMAC SHA256
const expectedSignature = crypto
.createHmac("sha256", secretKey)
.update(signatureString)
.digest("hex");

// Compare signatures (constant-time comparison prevents timing attacks)
// Note: Some gateways may use different comparison methods
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature),
);
}

Order Meta for Anonymous Cart

When order creation receives is_anonymous_cart and cart_id from the frontend, persist them (and user_id when logged in) in the order record so the payment callback or webhook can run user resolution, call platform checkout with the anonymous-cart header, and clear the cart after success. Store at least: cart_id, is_anonymous_cart, user_id (null for guest), company_id, and application_id.

Complete Platform Checkout

This helper function is used in the Payment Verification Function after addresses are extracted from the payment gateway. It maps addresses to platform format, uses Platform API for checkout, includes payment method configuration, forwards authorization headers, and uses the cart ID from order meta (saved during order creation).

For guest user's orders, use the cart ID and send the platform's anonymous-cart header on the request. Returns platform order ID and transaction ID, which are used for payment update and redirection.

async function completePlatformCheckout(order, _addresses, req, buyNow = false, userId = null) {
if (order.meta?.is_anonymous_cart && !userId) {
throw new Error('user_id is required for anonymous checkout');
}

const url = `https://api.fynd.com/service/platform/cart/v2.0/company/{company_id}/application/{application_id}/checkout`;

const headers = {
authorization: req.headers['authorization'] || req.headers['Authorization'],
cookie: req.headers['cookie'] || '',
'content-type': 'application/json',
origin: req.headers['origin'],
...(order.meta?.is_anonymous_cart ? { 'x-anonymous-cart': 'true' } : {})
};

const address = {
name: "John Doe",
address: "Wework",
area: "Chakala",
phone: "1234567890",
pincode: "400093",
city: "Mumbai",
state: "Maharashtra",
country: "India",
country_iso_code: "IN",
country_phone_code: "+91",
email: "johndoe@example.com",
address_type: "home"
};

const payload = {
delivery_address: address,
billing_address: address,
payment_methods: { mode: 'payment_gateway', payment: 'required' },
...(userId ? { user_id: userId } : {})
};

const response = await axios.post(url, payload, { headers });
if (!response.data?.success) {
throw new Error('Checkout failed: ' + (response.data?.message || 'Unknown error'));
}

return {
success: response.data.success,
order_id: response.data.order_id,
merchant_transaction_id: response.data.data?.merchant_transaction_id,
message: response.data.message
};
}

Update Platform Payment via FDK

note

This step is not applicable for COD orders, as the platform handles payment session updates for COD automatically.

It updates the payment session using FDK's payment API, linking the payment gateway payment to the platform's payment session.

// Example: Update payment via FDK
async function updatePlatformPayment(
merchantTransactionId,
paymentId,
gatewayOrderId,
credentials,
order,
) {
// Get company_id and app_id from order
const companyId = order.company_id || order.meta?.company_id;
const applicationId = order.app_id || order.meta?.app_id;

if (!companyId || !applicationId) {
throw new Error("Missing company_id or app_id in order");
}

// Initialize FDK
const Fdkfactory = require("../fdk");
const fdkExtension = await Fdkfactory.fdkExtension();
const platformClient = await fdkExtension.getPlatformClient(companyId);

// Get checksum secret (from extension config or credentials)
const checksumSecret = config.extension.api_secret;

// Update payment session
// This links the payment gateway payment to the platform payment session
const paymentUpdateResponse = await platformClient
.application(applicationId)
.payment.updatePaymentSession({
payment_session_id: merchantTransactionId,
body: {
payment_id: paymentId,
aggregator_order_id: gatewayOrderId,
aggregator_payment_id: paymentId,
status: "success",
},
});

return {
success: true,
merchant_transaction_id: merchantTransactionId,
fynd_order_id: paymentUpdateResponse?.order_id,
};
}

Extract Addresses from Payment Gateway

It is used in the Payment Verification Function after fetching gateway order details. Payment gateways store user addresses in their order details. Extract these addresses after payment verification for use in platform checkout and address formats to the platform's expected format. The addresses are then mapped to platform format before calling platformCheckoutCartV2 FDK method.

Anonymous Cart: User Resolution and Checkout

User resolution runs only when the order is anonymous, platform context (e.g. company_id, application_id) is available, and you are about to call the platform checkout API.

Anonymous checkout needs a phone number to either search for an existing user (registered, not logged in) or create a new user (unregistered). Take phone number from payment gateway customer details. If phone is missing, fail with a clear error and do not call checkout.

Step 1: Search user: Call the platform searchUser API (e.g. by normalized phone). If a user is found, use that user_id.

Step 2: Create user: If search returns no users, create a new user with the platform createUser API.

// Example: build headers for platform cart/checkout calls
const headers = { "Content-Type": "application/json" /* , ... */ };
if (order.meta?.is_anonymous_cart) {
headers["x-anonymous-cart"] = "true";
}
// getCart, platformCheckoutCartV2, removeCoupon, deleteCart — all use same header when anonymous

Anonymous Cart: Post-payment Cart Clearing

After payment is confirmed and platform checkout has completed successfully, clear the anonymous cart so it is not reused. Call the platform’s delete-cart API with the same cart ID header.

FDK Method: deleteCart

Payment Gateway Integration Endpoints

Some payment gateways (like Razorpay Magic Checkout) require additional endpoints to support features like address validation, coupon management, and shipping information during the payment modal interaction. These endpoints are called by the payment gateway SDK, not by your frontend code. These endpoints are called by the payment gateway modal after it opens (using the modal configuration returned by your Buy Now Endpoint) but before the user completes payment. They enable the payment gateway to validate addresses, fetch coupons, and apply discounts during the modal interaction, providing a seamless checkout experience.

These endpoints are invoked by the payment gateway during the payment modal interaction, after the modal opens but before payment completion:

  1. checkCartServiceability: Triggered by the payment gateway when a user provides a shipping address in the modal. Checks if the cart is serviceable to the address and returns delivery charges, COD availability, delivery estimates, and overall serviceability status.
  2. getAppCoupons (Coupons Endpoint): Called when the payment gateway requests available coupons for the cart
  3. applyCoupon: Triggered when the payment gateway modal requests to apply a coupon code. It applies the coupon to the cart and responds with refreshed pricing details—including the updated cart, new delivery and COD charges, and the recalculated total—allowing the user to review accurate savings and payment amounts in real time.
  4. platformCheckoutCartV2: Triggered internally by your backend after verifying payment success. It creates the platform order using shipping and billing addresses from the payment gateway, and returns the platform order ID along with the merchant transaction ID.