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 fileecho "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).
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):
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
{
"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"
}```- Implementation (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:
{
"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.
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):
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>
);
}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:
{
"sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a",
"paymentMethod": "telebirr",
"phone": "0916899465"
}```Implementation (Node.js):
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):
{
"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
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:
Option 1: Event-Based Status Check (Recommended)
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:
{
"sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a",
"paymentMethod": "telebirr",
"phone": "0916899465"
}```Expected Response (Based on Working Postman Example):
{
"sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a",
"status": "SUCCESS",
"message": "Process service request successfully."
}```Implementation (Node.js):
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}
- URL:
Expected Response:
{
"sessionId": "ea1f57ca-d34a-4b2f-b19c-5f84be20ec9a",
"status": "SUCCESS",
"message": "Process service request successfully."
}```Implementation (Node.js):
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:
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):
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):
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:
<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:
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:
{
"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