Skip to Content
Integration GuidesDirect Checkout

Direct Checkout Flow

Direct Checkout Flow allows merchants to handle the payment process directly on their website or application using a KisPay payment form or modal, without redirecting customers to a KisPay-hosted checkout page. After the customer completes the payment, the transaction status is verified using the /api/payments/status/{sessionId} endpoint, and the system updates accordingly. Below are the detailed steps from beginning to end, using working samples as practical examples.

Step 1: Obtain and Secure Your API Key

  • Description: Begin by obtaining your API key from the KisPay Merchant Dashboard (e.g., KPG_PROD-xxxxx for production or a test key like KPG_TEST-xxxx for testing). Securely store this key to prevent exposure.

  • Action: Create an environment file to store the key and configure your server.

  • Example: Create a .env file in your project root:

    env file
    echo "KISPAY_API_KEY=kp_live_your_api_key_here" > .env echo "KISPAY_API_BASE_URL=https://api.kispay.et/api" >> .env echo ".env" >> .gitignore```
  • Ensure your server loads this environment variable (e.g., using a library like dotenv in Node.js).

Never hardcode the API key in your source code or expose it in client-side scripts.

Step 2: Set Up Your Server Environment

  • Description: Configure your backend server to handle API requests, payment processing, and status verification. Ensure it uses HTTPS with a valid SSL/TLS certificate, as required by KisPay.
  • Action: Set up a basic server with the necessary dependencies and endpoints.
  • Example: (Node.js with Express):
Node.Js with Express
const express = require('express'); const axios = require('axios'); require('dotenv').config(); const app = express(); app.use(express.json()); const API_KEY = process.env.KISPAY_API_KEY; const API_BASE_URL = process.env.KISPAY_API_BASE_URL; app.listen(3000, () => console.log('Server running on port 3000'));```

Step 3: Create a Checkout Session (Backend)

When a customer initiates a payment (e.g., clicks “Pay Now” on your site), your backend creates a checkout session by calling the KisPay API. This session stores payment details and prepares the payment form data for direct processing.

  • Action: Implement an endpoint to create the session and return the session ID for direct payment processing.
  • Endpoint: POST /checkout/create_session
  • Required Fields: amount, orderNo, description, phone, email, fullName, successUrl, cancelUrl, errorUrl, redirectUrl
Sample Request Body
{ "amount": 44000, "orderNo": "0045", "description": "samsung", "phone": "0941420279", "email": "merchant@gmail.com", "fullName": "Test", "redirectUrl": "http://www.google.com?e=1231/redirect", "errorUrl": "http://www.google.com?e=1231/error", "cancelUrl": "http://www.google.com?e=1231/cancel", "successUrl": "http://www.google.com?e=1231/success" }```
redirectUrl, errorUrl, cancelUrl, and successUrl should be HTTPS for production
  • Implementation (Node.js):
Sample Implementation With Node.Js
async function createSession(data) { const response = await axios.post(`${API_BASE_URL}/checkout/create_session`, data, { headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, }); return response.data.body; } app.post("/api/create-session", async (req, res) => { const sessionData = req.body; try { const session = await createSession(sessionData); res.json(session); } catch (error) { res.status(500).json({ error: "Failed to create session" }); } });
  • Response:
Sample Response
{ "statusCode": 201, "message": "Checkout session created successfully", "body": { "id": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "merchantId": "68c1a71c-ace8-419a-b548-1ccaba414219", "merchantBusinessName": "Googlead", "merchantBusinessTelephone": "0936707070", "merchantBusinessAddressCity": "Addis Ababa", "MerchantBusinessEmail": "abe@gmail.com", "amount": "44,000.00", "currency": "ETB", "orderNo": "0045", "description": "samsung", "fullName": "Test", "phone": "0916899465", "email": "merchant@gmail.com", "successUrl": "http://www.google.com?e=1231/success", "cancelUrl": "http://www.google.com?e=1231/cancel", "errorUrl": "http://www.google.com?e=1231/error", "redirectUrl": "http://www.google.com?e=1231/redirect", "status": "PENDING", "createdAt": "2025-10-22T18:31:13.286+00:00" }, "timestamp": "2025-10-22T18:31:13.292+00:00" }```
  • Key Fields in Response:
    • id: Unique session ID (e.g., ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a).
    • status: Initial state is PENDING
    • Note: Unlike redirect checkout, there is no paymentUrl since payment is handled directly on your site.
Store the id (session ID) in your database for tracking.

Step 4: Display Payment Form or Modal (Frontend)

Display a payment form or modal on your page to collect payment details (e.g., phone number and payment method) and initiate the payment process using the session ID. This keeps the customer on your site throughout the payment process.

  • Action: Implement a frontend component to handle the payment form and submit payment data.
  • Example: (React with Material-UI, inspired by modal patterns):
PaymentModal Component
import { Alert, Box, Button, Modal, Snackbar, TextField, MenuItem } from "@mui/material"; import { useState } from "react"; const style = { position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", width: "90%", maxWidth: "600px", maxHeight: "90vh", overflowY: "auto", bgcolor: "background.paper", borderRadius: 2, boxShadow: 24, p: { xs: 2, sm: 3, md: 4 }, }; type AlertSeverity = "error" | "info" | "success" | "warning"; interface Snack { open: boolean; message: string; severity: AlertSeverity; } export default function PaymentModal({ sessionId, amount, description, onClose }) { const [snack, setSnack] = useState<Snack>({ open: false, message: "", severity: "info", }); const [phone, setPhone] = useState(""); const [paymentMethod, setPaymentMethod] = useState("telebirr"); const [loading, setLoading] = useState(false); const handleSnackOpen = (message, severity) => { setSnack({ open: true, message, severity }); }; const handleSnackClose = (event, reason) => { if (reason !== "clickaway") setSnack((prev) => ({ ...prev, open: false })); }; const handlePayment = async () => { if (!phone || phone.length < 10) { handleSnackOpen("Please enter a valid phone number", "error"); return; } setLoading(true); try { const response = await fetch(`/api/process-payment`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, paymentMethod, phone, }), }); const data = await response.json(); if (response.ok && data.body?.status === "pending") { handleSnackOpen("Payment request sent. Please check your phone to complete payment.", "success"); // Use event-based status check (recommended - see Step 6) checkPaymentStatusEvent(sessionId); } else { handleSnackOpen(data.message || "Payment initiation failed", "error"); setLoading(false); } } catch (error) { handleSnackOpen("Payment processing failed", "error"); setLoading(false); } }; const checkPaymentStatusEvent = async (sessionId) => { // Event-based approach (recommended) - waits for status from KisPay try { const response = await fetch(`/api/check-status-event`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, paymentMethod, phone, }), }); const data = await response.json(); if (data.status === "SUCCESS") { handleSnackOpen("Payment completed successfully!", "success"); setTimeout(() => { window.location.href = data.successUrl || "/success"; }, 2000); } else if (data.status === "FAILED") { handleSnackOpen("Payment failed. Please try again.", "error"); setLoading(false); } } catch (error) { handleSnackOpen("Status verification failed", "error"); setLoading(false); } }; return ( <Box> <Snackbar anchorOrigin={{ vertical: "top", horizontal: "center" }} open={snack.open} autoHideDuration={4000} onClose={handleSnackClose}> <Alert onClose={handleSnackClose} severity={snack.severity} variant="filled"> {snack.message} </Alert> </Snackbar> <Modal open={true} onClose={onClose}> <Box sx={style}> <h1 className="text-3xl text-neutral-600 font-extrabold my-2">Pay Now</h1> <p className="text-gray-600 mb-4"> Amount: ETB {amount} <br /> Description: {description} </p> <TextField select label="Payment Method" variant="outlined" fullWidth margin="normal" value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}> <MenuItem value="telebirr">TeleBirr</MenuItem> <MenuItem value="cbebirr">CBE Birr</MenuItem> <MenuItem value="awashbirr">Awash Birr</MenuItem> </TextField> <TextField label="Phone Number" type="tel" variant="outlined" fullWidth margin="normal" placeholder="e.g., 0916899465" value={phone} onChange={(e) => setPhone(e.target.value)} /> <div className="w-full flex justify-end items-center gap-5 my-3"> <Button onClick={onClose} variant="outlined" color="secondary" disabled={loading}> Cancel </Button> <Button variant="contained" color="primary" onClick={handlePayment} disabled={loading}> {loading ? "Processing..." : "Pay Now"} </Button> </div> </Box> </Modal> </Box> ); }
The modal stays on your site and handles payment without redirecting to KisPay’s checkout page. The example above uses the event-based status check (recommended), which is explained in detail in Step 6.

Step 5: Process Direct Payment (Backend)

Handle the payment request from the frontend, validate the input (sessionId, paymentMethod, and phone), and initiate the direct payment with KisPay.

  • Action: Implement an endpoint to process the direct payment and return the result.
  • Endpoint: POST /api/checkout/direct_payment
  • Required Fields: sessionId, paymentMethod, phone

Sample Request Body:

Sample Request Body
{ "sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "paymentMethod": "telebirr", "phone": "0916899465" }```

Implementation (Node.js):

Direct Payment Processing
async function processDirectPayment(sessionId, paymentMethod, phone) { const url = `${API_BASE_URL}/checkout/direct_payment`; const response = await axios.post( url, { sessionId, paymentMethod, phone }, { headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, } ); return response.data; } app.post("/api/process-payment", async (req, res) => { const { sessionId, paymentMethod, phone } = req.body; if (!sessionId || !paymentMethod || !phone) { return res.status(400).json({ error: "Session ID, payment method, and phone are required", }); } try { const response = await processDirectPayment(sessionId, paymentMethod, phone); res.json(response); } catch (error) { res.status(500).json({ error: "Payment processing failed", message: error.response?.data?.message || error.message, }); } });

Response (Based on Working Postman Example):

Sample Response
{ "statusCode": 200, "message": "Direct payment initiated", "body": { "status": "pending", "requires_redirect": false, "message": "Accept the service request successfully." }, "timestamp": "2025-10-23T07:32:01.640+00:00" }```
  • Key Response Fields:
    • status: “pending” indicates the payment request has been sent to the customer’s phone
    • requires_redirect: false confirms no redirect is needed (direct checkout)
    • message: Confirmation that the service request was accepted
The customer will receive a push notification on their phone to approve the payment.

Step 6: Verify Transaction Status

After the payment is initiated, verify the transaction status using one of two methods. The customer will complete the payment on their phone (e.g., by entering a PIN in the TeleBirr app), and your system checks the status until it changes to SUCCESS or FAILED.

KisPay provides two approaches for checking payment status:

This method uses Server-Sent Events (SSE) or long-polling to receive real-time status updates from KisPay, which is more efficient than manual polling.

  • Endpoint: POST /api/payments/status
  • Required Fields: sessionId, paymentMethod, phone

Sample Request Body:

Event-Based Request Body
{ "sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "paymentMethod": "telebirr", "phone": "0916899465" }```

Expected Response (Based on Working Postman Example):

Sample Response - Success
{ "sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "status": "SUCCESS", "message": "Process service request successfully." }```

Implementation (Node.js):

Event-Based Status Verification
async function checkPaymentStatusEvent(sessionId, paymentMethod, phone) { const url = `${API_BASE_URL}/payments/status`; const response = await axios.post( url, { sessionId, paymentMethod, phone }, { headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, } ); return response.data; } app.post("/api/check-status-event", async (req, res) => { const { sessionId, paymentMethod, phone } = req.body; if (!sessionId || !paymentMethod || !phone) { return res.status(400).json({ error: "Session ID, payment method, and phone are required" }); } try { const statusData = await checkPaymentStatusEvent(sessionId, paymentMethod, phone); // Update your database based on status if (statusData.status === "SUCCESS") { console.log(`Order ${sessionId} completed successfully`); // Update order status in database // Send confirmation email // Fulfill order } else if (statusData.status === "FAILED") { console.log(`Order ${sessionId} failed`); // Log failure, notify customer } res.json(statusData); } catch (error) { res.status(500).json({ error: "Failed to verify status", message: error.response?.data?.message || error.message, }); } });

The event-based approach automatically waits for status updates from KisPay’s payment gateway. Once the customer completes or cancels the payment, the status is immediately returned without the need for continuous polling.

Option 2: Direct Status Query (Alternative)

This method queries the payment status directly using a GET request. Suitable for scenarios where you need to check status independently or periodically.

  • Endpoint: GET /api/payments/status/{sessionId}
  • Sample Request:
    • URL: https://api.kispay.et/api/payments/status/ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a
    • Headers: Authorization: Bearer ${API_KEY}

Expected Response:

Sample Response - Success
{ "sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "status": "SUCCESS", "message": "Process service request successfully." }```

Implementation (Node.js):

Direct Status Query Implementation
async function checkPaymentStatusDirect(sessionId) { const url = `${API_BASE_URL}/payments/status/${sessionId}`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${API_KEY}`, }, }); return response.data; } app.get("/api/check-status", async (req, res) => { const { sessionId } = req.query; if (!sessionId) { return res.status(400).json({ error: "Session ID required" }); } try { const statusData = await checkPaymentStatusDirect(sessionId); // Update your database based on status if (statusData.status === "SUCCESS") { console.log(`Order ${sessionId} completed successfully`); // Update order status in database // Send confirmation email // Fulfill order } else if (statusData.status === "FAILED") { console.log(`Order ${sessionId} failed`); // Log failure, notify customer } res.json(statusData); } catch (error) { res.status(500).json({ error: "Failed to verify status", message: error.response?.data?.message || error.message, }); } });

For Option 2 (Direct Query): Manual polling is not the recommended approach as it increases server load and may cause delays in status updates. If you must use this method, implement polling on the frontend to check payment status every 5-10 seconds until a final status (SUCCESS or FAILED) is received, with a maximum timeout of 5 minutes. We strongly recommend using Option 1 (Event-Based) for real-time status updates.

Frontend Implementation Example (Using Event-Based Approach)

Update the PaymentModal component from Step 4 to use the event-based status check:

Updated pollPaymentStatus function
const pollPaymentStatus = async (sessionId) => { // Use event-based approach (recommended) try { const response = await fetch(`/api/check-status-event`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, paymentMethod, phone, }), }); const data = await response.json(); if (data.status === "SUCCESS") { handleSnackOpen("Payment completed successfully!", "success"); setTimeout(() => { window.location.href = data.successUrl || "/success"; }, 2000); } else if (data.status === "FAILED") { handleSnackOpen("Payment failed. Please try again.", "error"); setLoading(false); } } catch (error) { handleSnackOpen("Status verification failed", "error"); setLoading(false); } };

Alternatively, for the direct query approach (with polling):

Polling with Direct Query
const pollPaymentStatusDirect = async (sessionId) => { const maxAttempts = 60; // Poll for 5 minutes (60 * 5 seconds) let attempts = 0; const intervalId = setInterval(async () => { attempts++; try { const response = await fetch(`/api/check-status?sessionId=${sessionId}`); const data = await response.json(); if (data.status === "SUCCESS") { clearInterval(intervalId); handleSnackOpen("Payment completed successfully!", "success"); setTimeout(() => { window.location.href = data.successUrl || "/success"; }, 2000); } else if (data.status === "FAILED") { clearInterval(intervalId); handleSnackOpen("Payment failed. Please try again.", "error"); setLoading(false); } if (attempts >= maxAttempts) { clearInterval(intervalId); handleSnackOpen("Payment verification timeout. Please check your order status.", "warning"); setLoading(false); } } catch (error) { console.error("Status check error:", error); } }, 5000); // Check every 5 seconds };

Step 7: Handle Payment Outcome

Based on the verified transaction status, update your system and redirect or notify the customer accordingly.

  • Action: Implement logic to handle the outcome and redirect to appropriate URLs.

Example (Node.js):

Payment Outcome Handler
app.get("/api/handle-outcome", async (req, res) => { const { sessionId } = req.query; if (!sessionId) { return res.status(400).send("Session ID required"); } try { const statusData = await checkPaymentStatus(sessionId); // Retrieve session data from your database to get redirect URLs const session = await getSessionFromDatabase(sessionId); let outcomeUrl; switch (statusData.status) { case "SUCCESS": console.log(`Order ${sessionId} completed successfully`); outcomeUrl = session.successUrl || "http://www.google.com?e=1231/success"; break; case "FAILED": outcomeUrl = session.errorUrl || "http://www.google.com?e=1231/error"; break; case "CANCELLED": case "EXPIRED": outcomeUrl = session.cancelUrl || "http://www.google.com?e=1231/cancel"; break; default: outcomeUrl = session.errorUrl || "http://www.google.com?e=1231/error"; } res.redirect(outcomeUrl); } catch (error) { res.redirect("http://www.google.com?e=1231/error"); } });

Sample Success Page:

sample success page
<h1>Payment Successful!</h1> <p>Order #0045 for Samsung (44,000 ETB) is confirmed.</p> <p>Thank you for your payment!</p>

Step 8: Complete Frontend Integration Example

Here’s a complete example of how to integrate direct checkout in your page:

Complete Integration Example
import { useState } from "react"; import PaymentModal from "./components/PaymentModal"; export default function CheckoutPage() { const [showModal, setShowModal] = useState(false); const [sessionData, setSessionData] = useState(null); const initiateCheckout = async () => { try { const response = await fetch("/api/create-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: 44000, orderNo: "0045", description: "samsung", phone: "0916899465", email: "merchant@gmail.com", fullName: "Test Customer", redirectUrl: "https://yoursite.com/redirect", errorUrl: "https://yoursite.com/error", cancelUrl: "https://yoursite.com/cancel", successUrl: "https://yoursite.com/success", }), }); const data = await response.json(); setSessionData(data); setShowModal(true); // Show payment modal instead of redirecting } catch (error) { alert("Failed to create checkout session"); } }; return ( <div> <h1>Product: Samsung Phone</h1> <p>Price: 44,000 ETB</p> <button onClick={initiateCheckout}>Pay Now with Direct Checkout</button> {showModal && sessionData && <PaymentModal sessionId={sessionData.id} amount={sessionData.amount} description={sessionData.description} onClose={() => setShowModal(false)} />} </div> ); }

Step 9: Test the Entire Flow

Use the following test data to verify your integration:

Complete Test Scenario
{ "step1": { "endpoint": "https://api.kispay.et/api/checkout/create_session", "method": "POST", "headers": { "Authorization": "Bearer YOUR_TEST_API_KEY", "Content-Type": "application/json" }, "body": { "amount": 44000, "orderNo": "0045", "description": "samsung", "phone": "0916899465", "email": "merchant@gmail.com", "fullName": "Test", "redirectUrl": "http://www.google.com?e=1231/redirect", "errorUrl": "http://www.google.com?e=1231/error", "cancelUrl": "http://www.google.com?e=1231/cancel", "successUrl": "http://www.google.com?e=1231/success" } }, "step2": { "endpoint": "https://api.kispay.et/api/checkout/direct_payment", "method": "POST", "headers": { "Authorization": "Bearer YOUR_TEST_API_KEY", "Content-Type": "application/json" }, "body": { "sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "paymentMethod": "telebirr", "phone": "0916899465" } }, "step3_option1_event_based": { "description": "Event-based status check (Recommended)", "endpoint": "https://api.kispay.et/api/payments/status", "method": "POST", "headers": { "Authorization": "Bearer YOUR_TEST_API_KEY", "Content-Type": "application/json" }, "body": { "sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "paymentMethod": "telebirr", "phone": "0916899465" } }, "step3_option2_direct_query": { "description": "Direct status query (Alternative)", "endpoint": "https://api.kispay.et/api/payments/status/ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a", "method": "GET", "headers": { "Authorization": "Bearer YOUR_TEST_API_KEY" } } }

Testing Checklist:

  • Create session successfully and store session ID
  • Display payment modal with payment method selection
  • Submit direct payment request with valid phone number
  • Receive “pending” status indicating payment request sent
  • Customer receives push notification on their phone
  • Use event-based status check (Option 1) or poll status endpoint (Option 2) until status changes to SUCCESS
  • Redirect to success URL upon completion
  • Test failure scenarios (invalid phone, cancelled payment)
  • Test timeout scenarios to ensure proper error handling

Step 10: Go Live and Monitor Your Transactions

  • Replace the test API key with your Production API key (KPG_PROD-xxxxx)
  • Ensure all redirect URLs use HTTPS
  • Monitor transactions at https://merchant.kispay.et 
  • Set up proper error handling and logging for production
Direct Checkout provides a seamless payment experience by keeping customers on your site throughout the entire payment process, improving conversion rates and maintaining brand consistency.
Last updated on