M-Pesa STK Push Integration: Complete Daraja API Guide

Table of contents
M-Pesa moves billions daily, yet integrating it feels like decoding a puzzle. Here's the reality: it's four concepts—access tokens, STK Push, callbacks, and queries. Understand these fundamentals and you'll integrate payments with confidence.

Introduction

If you're building any kind of digital product in Kenya—whether it's an e-commerce platform, a booking system, or a subscription service—you'll eventually need to integrate M-Pesa payments. It's not optional anymore; it's expected. M-Pesa has become so ubiquitous that users simply assume your application will accept it.

But here's the thing: M-Pesa integration through Safaricom's Daraja API can feel intimidating at first. There are credentials to manage, endpoints to understand, and a specific flow you need to follow. Miss one step, and nothing works. Get it right, and you've unlocked seamless payment processing for millions of potential customers.

This guide walks you through the entire M-Pesa integration process, from creating your first Daraja account to handling payment callbacks. We'll use a practical example—a movie ticket booking system—to make everything concrete. By the end, you'll understand not just how to integrate M-Pesa, but why each step matters and how all the pieces fit together.

Let's dive in.

Getting Started with Daraja

Creating Your Daraja Account

The first step in working with M-Pesa integration is creating a Daraja API account. Head over to daraja.safaricom.co.ke and create an account. If you already have one, you can simply log in.

The one-time password (OTP) verification screen during Daraja login. Safaricom sends a verification code to the registered email address, adding a security layer before granting access to API credentials and the developer dashboard.
Daraja OTP Verification Screen

The login process is straightforward but secure. When you attempt to log in, Daraja sends a one-time password (OTP) to your registered email address. Enter this OTP, and you'll gain access to your dashboard. This two-factor authentication ensures that only you can access your developer credentials.

Understanding What a Daraja "App" Really Means

Once you're logged into your dashboard, you'll see options to create "apps." Now, this is where many developers get confused, so let's clear it up right away.

When Daraja talks about creating an "app," it doesn't mean what you might traditionally think of when you hear that word. It's not like Facebook, Twitter, or any social media platform. You're not building a standalone application that lives on Safaricom's servers.

Instead, a Daraja "app" is simply a set of credentials that allows your actual application—whether it's a website, mobile app, or backend service—to communicate with Safaricom's systems. Think of it as getting an access card to a building. The card isn't the building itself; it's just what lets you in.

The main Daraja developer dashboard displaying both production and sandbox applications.
Daraja Dashboard with Production and Sandbox Apps

Creating an app in Daraja is how you obtain the authentication credentials you'll need to make API requests. That's it. The naming is a bit misleading, but once you understand this distinction, everything else makes more sense.

Sandbox vs Production: Two Separate Worlds

Daraja provides two distinct environments, and understanding the difference between them is crucial.

Sandbox apps are for development and testing. This is where you experiment, break things, and figure out how everything works. When you send money through a sandbox transaction, Safaricom states that it will be returned to you. Now, in practice, this doesn't always happen consistently—sometimes the money comes back, sometimes it doesn't. But the key point is that sandbox is your playground. You're not dealing with real production transactions, so you can test freely without financial risk.

Production apps are for real, live transactions. When you create a production app and integrate it into your system, actual money flows through it. Payments go directly to the Till number or Paybill number associated with that app. This is real business, real money, and real responsibility.

For everything we're doing in this guide, you'll be working primarily with sandbox apps. Only when you're completely confident that everything works should you even think about moving to production.

Here's a practical tip: you can use the same sandbox app for multiple projects during development. Since it's just for testing and demonstrating that your integration works, there's no need to create a new sandbox app for every single project. However, for production, each business or project should have its own dedicated app tied to its specific Till or Paybill number.

Creating Your First Daraja App

Let's walk through creating an app. From your Daraja dashboard, click on "Create App." You'll see a form asking for some information.

The Daraja app creation interface where developers select products to enable (Lipa na M-Pesa, M-Pesa Ratiba, B2C, etc.) and configure app settings. This step generates the Consumer Key and Consumer Secret credentials needed for API authentication.
Creating a Sandbox App on Daraja Portal

Application Name: This is just a label for your own reference. Daraja has specific rules about what characters you can use—only letters, numbers, spaces, and underscores are allowed. You might call it "Movie Booking System Sandbox" or "E-commerce Test App" or whatever makes sense to you. The name doesn't affect functionality; it's purely organizational.

Products to Map: This is where things get interesting. Daraja exposes numerous APIs that correspond to different Safaricom products. You'll see options like:

  • Lipa na M-Pesa (also called M-Pesa Express or STK Push) - This is the main one you'll be using
  • M-Pesa Ratiba - For recurring payments, like subscription services
  • Business to Business (B2B) - For transactions between businesses
  • Business to Customer (B2C) - For disbursements, like sending money to users
  • Customer to Business (C2B) - For collecting payments
  • Various other specialized APIs

Each product represents a different capability. For a sandbox app, here's the recommendation: select all of them. Why? Because during development, you might realize you need a feature that wasn't part of your initial plan. If you've already selected all products when creating your sandbox app, you can simply use that functionality without having to create another app.

For production apps, however, you should only select the specific products you actually need. This is a security best practice. By limiting which APIs can be used with your production credentials, you prevent anyone from using APIs that could be catastrophic to your business if misused.

Once you've selected your products, click "Create App." Your app will be created and appear in your dashboard.

Understanding Your Daraja Credentials

When you look at your newly created app in the dashboard, you'll see several important pieces of information. Let's break down each one and understand what it's for.

The Daraja developer portal showing a sandbox app's credentials panel. Displays the Consumer Key, Consumer Secret, and Passkey required for API authentication.
Daraja Sandbox App Credentials Panel

Consumer Key and Consumer Secret

These two credentials work together like a username and password system.

The Consumer Key is your identifier. It tells Safaricom which application is making a request. Think of it like a username—it identifies you but doesn't prove you have permission.

The Consumer Secret is your authentication credential. It's like a password—it proves you're authorized to use this app. Just as you keep passwords secret, you should treat your Consumer Secret the same way. Only share it with developers who are working on the project, or with clients who need to provide it to their development team.

These credentials are what you'll use to tell Safaricom who you are so they can give you an access token. The Consumer Key and Consumer Secret are specifically for obtaining the access token, while the access token itself is what you use for actual payment operations.

Passkey

The Passkey is a cryptographic string that you'll use when generating passwords for STK Push requests. For production apps, Safaricom sends the Passkey to your registered email after you create the app. You'll receive an email with a subject line like "STK Push Credentials" containing this long, complex string.

For sandbox development, you can use a shared sandbox Passkey since you're just testing. We'll cover exactly how the Passkey is used when we get to the STK Push section.

Shortcode

The business Shortcode is a critical piece of information. In production, you'll see it clearly displayed in your app details. This Shortcode identifies the M-Pesa account that will receive payments.

However—and this is important—for sandbox apps, the Shortcode field sometimes shows "Not Available" instead of providing an actual number. This is a limitation of Safaricom's sandbox environment that hasn't been fully addressed. It can be frustrating, but there's a practical workaround.

Since sandbox is only for testing anyway, and all sandbox transactions work the same way regardless of whose credentials you use, you can simply use a known working sandbox Shortcode. The commonly used sandbox Shortcode is 174379. You just need credentials that demonstrate your application works correctly. You can use the same sandbox credentials across multiple projects during development—there's no security risk because it's all test data anyway.

For production, this isn't an issue. Your production Shortcode will be clearly displayed and will be tied to your actual Till or Paybill number.

Security Considerations

For sandbox apps, the security requirements are somewhat relaxed because you're just testing with fake transactions. However, for production credentials, security is paramount. Your production Consumer Key, Consumer Secret, and Passkey should be:

  • Never committed to version control systems like Git
  • Stored as environment variables or in secure configuration management systems
  • Only shared with trusted team members who absolutely need them
  • Rotated periodically if there's any suspicion of compromise

Think of your production credentials like the keys to your bank vault. Treat them accordingly.

The Four Pillars of M-Pesa Integration

Before we dive into the technical details, you need to understand that M-Pesa integration revolves around four fundamental concepts. Everything you do with Daraja fits into one of these four categories:

  1. Getting an Access Token - How you authenticate with Safaricom's systems
  2. STK Push - How you trigger the payment prompt on a user's phone
  3. STK Query - How you check the status of a transaction (optional)
  4. Callback - How you receive the final results from Safaricom

These aren't just random steps; they form the complete picture of how M-Pesa integration works. Let's understand each one in detail.

What is STK Push?

Before we go further, let's make sure we're all on the same page about what STK Push actually is. STK stands for SIM Toolkit. An STK Push is when you trigger a payment prompt that appears directly on a user's phone, asking them to enter their M-Pesa PIN to authorize a payment.

If you've ever made a payment online and suddenly your phone buzzed with a pop-up asking for your M-Pesa PIN, that was an STK Push. The user doesn't have to manually go to the M-Pesa menu, select "Lipa na M-Pesa," enter your business number, enter the amount, and so on. All that complexity is handled automatically. They just see a prompt asking: "Do you want to pay X shillings to Business Y? Enter your PIN."

This is the fundamental flow you'll be implementing regardless of what kind of application you're building. Whether it's a hotel booking system, a restaurant ordering platform, an SGR ticket purchase system, or an e-commerce checkout, the pattern is the same: the user initiates a payment, receives a prompt on their phone, and either completes or cancels the transaction.

Getting an Access Token: Your First Step

Every interaction with Daraja starts with getting an access token. Let's understand what this is and why it matters.

What is an Access Token?

An access token is a temporary credential that allows you to make requests to Safaricom's API. Instead of sending your Consumer Key and Consumer Secret with every single request (which would be a security risk), you exchange them once for an access token, then use that token for subsequent requests.

The access token is valid for exactly one hour—that's 3,600 seconds. After one hour, the token expires and can no longer be used. This time limitation enhances security by minimizing the exposure of your main credentials.

The Philosophy: Fresh Tokens Every Time

Now, you might think: "If the token lasts for one hour, I should store it and keep track of when it expires, right? That way I don't have to keep requesting new ones."

Technically, yes, you could do that. But here's the recommended approach that makes your life much simpler: request a fresh access token every time you need to make an API call.

Think about it like this. Imagine there's a building with a reception desk. The receptionist gives you a key that's valid for one hour. You could take that key, go do your work, come back later, check if the hour has passed, and if it has, get a new key. But that requires you to constantly check whether your current key has expired.

Or, you could simply stop by the reception desk every time you need to enter and pick up a fresh key. You never have to worry about whether your current key has expired because you're always getting a new one.

This second approach is what you should do with access tokens. Don't track expiration. Don't try to reuse tokens. Just get a new one each time. It's simpler, more reliable, and the performance overhead is negligible.

How to Get an Access Token

Getting an access token is an HTTP request to a specific endpoint. Let me show you exactly how this works using a tool called Postman, which makes testing API endpoints much easier.

The Endpoint:

For sandbox:

markdown
https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials

For production:

markdown
https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials

Notice the difference: sandbox URLs use sandbox.safaricom.co.ke while production URLs use api.safaricom.co.ke. This pattern holds true for all Daraja endpoints.

The HTTP Method: GET

This is important. For getting an access token, you use a GET request. Other endpoints will use POST, but this one specifically requires GET.

Authentication: Basic Auth

You need to provide authentication credentials with this request. Specifically, you use what's called Basic Authentication, where:

  • Username: Your Consumer Key
  • Password: Your Consumer Secret

Making the Request in Postman

Postman interface demonstrating the access token retrieval process using Basic Authentication with Consumer Key and Consumer Secret. The GET request to Safaricom's OAuth endpoint returns an access token valid for one hour, required by API calls.
Access Token Request in Postman

Let me walk you through this step by step as if you're sitting next to me:

  1. Open Postman and create a new collection. Call it something like "M-Pesa Integration" or "Daraja API Testing"
  2. Inside that collection, create a new request. Name it "Get Access Token"
  3. Set the request method to GET (use the dropdown menu)
  4. Paste the endpoint URL into the URL field
  5. Go to the "Authorization" tab
  6. From the "Type" dropdown, select "Basic Auth"
  7. In the "Username" field, paste your Consumer Key (copy it from your Daraja app dashboard)
  8. In the "Password" field, paste your Consumer Secret (copy it from your Daraja app dashboard)
  9. Click "Send"

The Response

When the request is successful, you'll receive a JSON response that looks like this:

json
{
  "access_token": "3TRSAg7mIKHQN...",
  "expires_in": "3599"
}

The access_token is a long string of characters. This is what you'll use for all subsequent API calls. The expires_in tells you how many seconds until this token becomes invalid—in this case, 3,599 seconds, which is essentially one hour.

Every time you click "Send" in Postman with the same credentials, you'll get a different access token. Notice how the token string changes each time. You might see one starting with "3TRSA..." then the next one starts with "UiECH..." and so on. Each one is unique and each one is valid for one hour from the time it was issued.

This is why you don't need to worry about tracking expiration. Just request a new token whenever you need to make an API call.

Understanding the STK Push Process with a Real Example

Now that you know how to get an access token, let's move to the core of M-Pesa integration: the STK Push. To make this concrete and practical, we'll work through a complete example using a movie ticket booking system.

Why a Movie Booking System?

This example helps you see how everything fits together in a real application. Users can browse available movies, select one they want to watch, provide their details, and make a payment. It's complex enough to demonstrate all the key concepts but simple enough to understand clearly.

The patterns you learn here apply to any payment scenario. Whether you're building an e-commerce checkout, a hotel booking system, a subscription platform, or a bill payment system, the fundamental flow remains the same.

The user-facing movie booking system showing ticket selection for multiple films. Users enter their details and quantity, triggering the backend STK Push flow that creates the ticket record and initiates M-Pesa payment.
Movie Ticket Booking Interface

The Database Design: Foundation First

Before we touch any payment code, we need to understand how to structure our data. Database design comes first because it determines how you store and retrieve all the information needed for the payment process.

Entity-Relationship Diagram showing the three core tables: Movie (storing film details and pricing), Ticket (tracking customer purchases and payment status), and PushRequest (linking CheckoutRequestIDs to tickets for callback processing).
Movie Ticket System Database ERD

Our system requires three main tables: movie, ticket, and pushrequest. Let's understand each one and why it exists.

The Movie Table

sql
CREATE TABLE IF NOT EXISTS movie (
    movieId INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    showTime DATETIME NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    maximumTickets INT NOT NULL DEFAULT 100,
    imageUrl VARCHAR(255),
    dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    lastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

You cannot have ticket sales without movies to sell tickets for, so this table stores all the movie information. Each movie has:

  • A unique movieId (auto-incremented by the database)
  • A title like "Top Gun: Maverick" or "Oppenheimer"
  • A description containing a brief synopsis
  • A showTime indicating when the movie will be screened
  • A price showing how much one ticket costs
  • A maximumTickets limit to prevent overselling
  • An imageUrl for displaying movie posters
  • Timestamp fields for tracking when records were created and updated

Think about any movie booking website you've used, or even Netflix. You see the movie name, a description of what it's about, information about when it's available, and pricing details. All of that information needs to be stored somewhere—that's what this table does.

The Ticket Table

sql
CREATE TABLE IF NOT EXISTS ticket (
    ticketId INT AUTO_INCREMENT PRIMARY KEY,
    movieId INT NOT NULL,
    customerName VARCHAR(255) NOT NULL,
    phoneNumber VARCHAR(20) NOT NULL,
    quantity INT NOT NULL,
    totalAmount DECIMAL(10, 2) NOT NULL,
    paymentStatus ENUM('Pending', 'Paid', 'Failed') NOT NULL DEFAULT 'Pending',
    mpesaReceiptNumber VARCHAR(100),
    transactionDate DATETIME,
    dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    lastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT fk_Ticket_Movie FOREIGN KEY (movieId) 
        REFERENCES movie(movieId) ON DELETE CASCADE
);

A movie can have multiple tickets—thirty different people might buy tickets for the same movie. The relationship between movies and tickets is one-to-many. One movie, many tickets.

Let's walk through each field in the ticket table:

ticketId: An auto-incrementing primary key. Understanding auto-increment is important. Think about admission numbers in a school system. One student gets admission number 1, the next gets 2, then 3, then 4, and so on. The database handles this automatically so there are no conflicts or duplicates. Same principle here—ticket IDs are assigned automatically.

movieId: Which movie is this ticket for? If a user buys a ticket for "Oppenheimer" and that movie has a movieId of 4 in the database, this field stores 4. It's a foreign key linking to the movie table.

customerName: The name of the person buying the ticket, as entered by them in the booking form.

phoneNumber: The customer's phone number. This is crucial because it's where the STK Push prompt will be sent.

quantity: How many tickets are they purchasing? Someone might buy 3 tickets for their family.

totalAmount: The calculated total cost. If a movie ticket costs 15 shillings and they want 3 tickets, the total is 45 shillings. This is calculated as quantity × price.

paymentStatus: This deserves special attention because it's central to how the system works. Let's talk about it in detail.

Understanding Payment Status: The Three States

When a ticket record is first created, before any payment has been made, you don't know whether the transaction will succeed or fail. The transaction hasn't happened yet. The user hasn't entered their PIN. They haven't confirmed anything. At this point, the outcome is completely unknown.

A transaction can be successful, or it can fail. And there are many reasons why a transaction might fail:

  • The user might not have enough money in their M-Pesa account. If someone wants to pay 39 shillings but only has 30 shillings available, the transaction will fail.
  • The user's phone might be turned off. If the phone is off, Safaricom cannot deliver the payment prompt, so the transaction fails.
  • There might be network issues preventing the transaction from completing.
  • There might be system issues at Safaricom.
  • The user might enter the wrong PIN. After several wrong attempts, the transaction fails.
  • The user might simply cancel the transaction when they see the prompt.

Given all these possible outcomes, you need a way to track the status of each ticket. The paymentStatus field can have three possible values:

  1. Pending - The payment hasn't been completed yet and we're waiting to see what happens
  2. Paid (or "Successful") - The payment went through completely
  3. Failed - Something went wrong and the payment didn't complete

When you first create a ticket record, before initiating any payment, you set the paymentStatus to 'Pending'. You're not sure what will happen, so you mark it as pending. Later, based on the results you receive from Safaricom via the callback, you'll update this status to either 'Paid' or 'Failed'.

mpesaReceiptNumber: This field stores the confirmation code that Safaricom sends when a payment is successful. If you look at your M-Pesa messages, you'll see codes like "UCH2G9LNVQ" or similar combinations of letters and numbers. These are receipt numbers, and they're important because they serve as proof that a payment was made.

But here's the key insight: when you create a ticket record initially, before any payment has happened, there is no receipt number yet. You can't have a receipt for a payment that hasn't occurred. Therefore, you set this field to NULL initially.

transactionDate: This stores when the payment was actually completed. If you look at your M-Pesa messages, you'll see something like "UCH2G9LNVQ confirmed. You have sent 20 shillings to Business Name on 17th of March 2026 at 12:23 PM." The date and time shown there is the transaction date.

Again, when you're first creating the ticket record, the transaction hasn't happened yet, so there is no transaction date. You set this field to NULL initially as well.

The timestamp fields dateCreated and lastUpdated are handled automatically by the database and track when the record was created and last modified.

The PushRequest Table

sql
CREATE TABLE IF NOT EXISTS pushrequest (
    pushRequestId INT AUTO_INCREMENT PRIMARY KEY,
    ticketId INT NOT NULL,
    checkoutRequestId VARCHAR(255) NOT NULL,
    dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    lastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT fk_PushRequest_Ticket FOREIGN KEY (ticketId) 
        REFERENCES ticket(ticketId) ON DELETE CASCADE
);

This table might seem mysterious at first. Why do we need it? The answer becomes clear when we understand the CheckoutRequestID concept, which we'll cover in detail shortly. For now, just know that this table exists to link Safaricom's transaction identifiers back to our ticket records.

The User Flow: From Browse to Payment

Let's walk through the complete user experience to understand how all the pieces fit together.

Detailed flowchart mapping the STK Push process from payment initiation through callback handling. Demonstrates decision points for success/failure paths, database updates at each stage, and the critical waiting period between push initiation and results.
STK Push Workflow Flowchart

Step 1: Display Available Movies

The system queries the movie table and shows all available movies to the user. Each movie displays its title, description, show time, and price. The user can browse through these options.

Step 2: User Selects a Movie

The user sees a movie they want to watch—let's say "Oppenheimer"—and clicks a "Book Now" button. This selection captures the movieId which we'll need for the next steps.

Step 3: Collect Booking Details

The system presents a booking form asking for:

  • Customer name
  • Phone number
  • How many tickets they want to purchase

Let's say the user enters:

  • Name: "Barasa Michael"
  • Phone: "0114742348"
  • Quantity: 3 tickets

The show time is displayed (July 21st, 2023 at 8 PM), and the price shows 15 shillings per ticket. When the user specifies 3 tickets, the system automatically calculates that the total amount is 45 shillings (3 × 15).

Step 4: User Submits the Booking

The user clicks a "Book Ticket" or "Proceed to Payment" button. This sends all the booking information to your backend system.

This flow is consistent across different types of systems. Whether you're building a hotel booking system, a restaurant ordering platform, SGR ticket booking, or anything else, the pattern is similar: display options, user makes a selection, collect their details, they submit, and then you process the payment.

Creating the Ticket Record: A Critical Step

Here's something that trips up many developers: you must create the ticket record in your database BEFORE you initiate the payment. This is not optional. This is mandatory.

Let me be very clear about the sequence:

  1. User submits their booking information
  2. You create the ticket record in the database first
  3. Then you initiate the STK Push

Not the other way around. The database record comes first.

Why This Order Matters

Think about it from a practical perspective. When Safaricom sends you the payment results later (through the callback), you need somewhere to store that information. You need a ticket record to update. If you tried to initiate the payment first and create the record later, you'd have payment results with nowhere to put them.

Additionally, you need the ticket ID to link the payment request to a specific booking. This becomes crucial when handling concurrent transactions, as we'll see shortly.

The Insert Statement

When the user submits their booking information, your backend receives:

  • The selected movieId
  • The customerName
  • The phoneNumber
  • The quantity

You use this information to create a new ticket record:

sql
INSERT INTO ticket (
    movieId, 
    customerName, 
    phoneNumber, 
    quantity, 
    totalAmount, 
    paymentStatus, 
    mpesaReceiptNumber, 
    transactionDate
) VALUES (
    4,                    -- movieId from user selection
    'Barasa Michael',     -- customerName from user input
    '0114742348',         -- phoneNumber from user input
    3,                    -- quantity from user input
    45.00,                -- calculated: 3 × 15
    'Pending',            -- initial status
    NULL,                 -- no receipt yet
    NULL                  -- no transaction date yet
);

Let's walk through where each value comes from:

movieId: From the user's selection. If they selected Oppenheimer and that movie has a movieId of 4, you use 4.

customerName, phoneNumber, quantity: Directly from what the user entered in the form.

totalAmount: You calculate this by querying the movie table to get the price, then multiplying by the quantity. If the price is 15 and quantity is 3, the total is 45.

paymentStatus: Set to 'Pending' because we don't know yet whether the payment will succeed or fail.

mpesaReceiptNumber: Set to NULL because the transaction hasn't happened yet, so there's no receipt.

transactionDate: Set to NULL for the same reason—no transaction yet means no transaction date.

After this INSERT statement executes, you get back a ticketId. Let's say it returns 67. This ticket ID is important—you'll need it for the next steps.

Phone Number Formatting: A Critical Requirement

Before you can initiate the STK Push, you need to ensure the phone number is in the correct format. Safaricom has specific requirements, and if you don't format the number correctly, your API request will fail.

The Formatting Rules

Users can enter their phone numbers in various ways:

  • +254114742348 (with country code and plus sign)
  • 0114742348 (starting with zero)
  • +254725820898 (another number with plus)
  • 0725820898 (another number with zero)

All of these are valid ways that users might input their phone numbers, but Safaricom requires a specific format: 254XXXXXXXXX (no plus sign, no leading zero, just 254 followed by the digits).

The rules are straightforward:

  1. If there's a plus sign (+) at the beginning, remove it
  2. If the number starts with a zero (0), replace that zero with 254

Let's see the transformations:

  • +254114742348 becomes 254114742348 (remove the plus)
  • 0114742348 becomes 254114742348 (replace 0 with 254)
  • +254725820898 becomes 254725820898 (remove the plus)
  • 0725820898 becomes 254725820898 (replace 0 with 254)

Every phone number must be converted to start with 254 without a plus sign. This formatting is a requirement from Safaricom. If you don't format correctly, your STK Push request will be rejected.

You must implement this formatting logic in your code before making the STK Push request. The formatted number is what you'll use for all M-Pesa API calls.

Preparing for the STK Push

At this point, you've completed several important steps:

✓ Displayed movies to the user
✓ User selected a movie
✓ Collected their booking details (name, phone, quantity)
✓ Created a ticket record in the database with status "Pending"
✓ Formatted the phone number correctly

Now you're ready to perform the actual STK Push—to send a request to Safaricom asking them to prompt the user's phone with a payment request.

But before you can do that, you need to understand all the parameters that an STK Push request requires. There are quite a few of them, and each one serves a specific purpose.

Getting a Fresh Access Token

Remember what we discussed earlier about access tokens? Before making the STK Push request, you need to get a fresh access token. Don't try to reuse an old one. Just make a quick GET request to the access token endpoint with your Consumer Key and Consumer Secret, get the new token, and you're ready to proceed.

This is the pattern for every STK Push:

  1. Get access token
  2. Use that token to make STK Push request

Simple and reliable.

The STK Push Endpoint

For sandbox

markdown
https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest

For production

markdown
https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest

HTTP Method: POST

Notice this is different from getting an access token, which used GET. For STK Push, you use POST.

Authorization: Bearer Token

This is also different from the access token request. For STK Push, you don't use Basic Auth. Instead, you use what's called a Bearer Token. You go to the Authorization tab in Postman, select "Bearer Token," and paste in the access token you just received from the previous request.

Headers:

You need to set a header indicating that you're sending JSON data:

Content-Type: application/json

Body:

The request body contains all the parameters Safaricom needs to process the payment. This is where it gets detailed, so let's go through each parameter carefully.

Postman configured for an STK Push request showing the complete payload: BusinessShortCode, Base64-encoded Password, Timestamp, TransactionType, Amount, PartyA/PartyB phone numbers, CallbackURL, and transaction reference fields.
STK Push API Request in Postman

Understanding STK Push Parameters

The STK Push request requires eleven parameters. Understanding each one and where its value comes from is essential for successful integration.

1. BusinessShortCode

This is your business shortcode—the M-Pesa number that will receive the payment.

For production: You'll find this in your Daraja app dashboard. It's clearly displayed in your production app details.

For sandbox: As we discussed earlier, the sandbox dashboard sometimes shows "Not Available" instead of providing a shortcode. The practical solution is to use the standard sandbox shortcode: 174379

Since sandbox is only for testing and all sandbox transactions work the same way, using a shared sandbox shortcode is perfectly fine. You just need to demonstrate that your application works correctly.

2. Password

This is one of the more complex parameters because it's not something you simply look up—it's something you generate.

The password is created by concatenating three pieces of information and then encoding the result using Base64 encoding:

Password = Base64Encode(BusinessShortCode + Passkey + Timestamp)

Let's break this down step by step.

BusinessShortCode: We already know this—it's 174379 for sandbox.

Passkey: This is the cryptographic string we discussed earlier. For production, it's sent to your email when you create the app. For sandbox, you can use the standard sandbox passkey:

bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919

Timestamp: This is the current date and time in the format YYYYMMDDHHmmss. For example, if the current time is March 17th, 2026 at 19:28:48, the timestamp is 20260317192848.

Concatenation: You join these three pieces together with no spaces or separators:

174379bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c91920260317192848

Base64 Encoding: You then take this long concatenated string and encode it using Base64 encoding. Base64 is a standard encoding method that converts data into a specific format. You don't need to understand the mathematical details of how it works—you just need to know that your programming language has built-in functions to perform Base64 encoding.

For example, in Python there's a base64 library. In JavaScript there are Base64 encoding functions. You take your concatenated string, pass it to the encoding function, and it gives you back the encoded result.

This encoded result is what you use as the Password parameter.

Here's a simple Python script that demonstrates the password generation process:

python
import base64
from datetime import datetime

BUSINESS_SHORT_CODE = "174379"
PASSKEY = "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919"
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")

# Concatenate the three pieces
concatenated = BUSINESS_SHORT_CODE + PASSKEY + timestamp

# Encode to Base64
password = base64.b64encode(concatenated.encode()).decode()

print(f"Generated Password: {password}")

When you run this, it outputs the Base64-encoded password that you'll use in your STK Push request.

3. Timestamp

This is simply the current date and time in the format YYYYMMDDHHmmss.

Important: You must use the current time when making the request. Don't use a hardcoded or old timestamp.

The timestamp serves an important purpose: it makes each request unique. No two moments in time are exactly the same. The second that just passed will never occur again. By using the current timestamp, you ensure that each request is unique, which helps prevent duplicate transactions and provides a clear audit trail.

Note that you use the same timestamp for both generating the password and as the Timestamp parameter. Don't generate the password with one timestamp and then use a different timestamp for this parameter—they must match.

4. TransactionType

This parameter can have one of two values, depending on whether you're using a Paybill or a Till number:

"CustomerPayBillOnline" - Use this if you're using a Paybill number
"CustomerBuyGoodsOnline" - Use this if you're using a Till number

The choice depends on what type of M-Pesa number your business is using. Paybill numbers are typically used for businesses that receive payments for services or bills. Till numbers are typically used for businesses selling goods.

For our sandbox testing with the standard sandbox shortcode, we use "CustomerPayBillOnline".

5. Amount

This is the payment amount in whole numbers only. Do not use decimal numbers. The amount must be an integer.

If the total for three tickets is 45 shillings, you put 45.
If someone is buying goods worth 30,000 shillings and you give them a 1,000 shilling discount (making the final amount 29,000), you put 29000.

For testing, use 1 to represent one shilling. There's no reason to test with large amounts and risk losing money. One shilling is sufficient to verify that everything works correctly.

6. PartyA

This is the phone number of the customer making the payment. You use the formatted phone number that you prepared earlier—the one starting with 254.

If the user's phone number is 254114742348, that's what you put here.

7. PartyB

This represents the organization receiving the money.

For CustomerPayBillOnline (Paybill): PartyB is typically the same as the BusinessShortCode. They're the same value.

For CustomerBuyGoodsOnline (Till): PartyB is the Till number, which is different from the shortcode. For Till numbers, Safaricom provides the PartyB value separately when they set up your production account.

For our sandbox testing with CustomerPayBillOnline, PartyB is 174379 (same as the BusinessShortCode).

8. PhoneNumber

This is the phone number that will receive the STK Push prompt. It's the same value as PartyA.

You might wonder why this parameter exists if it's the same as PartyA. That's simply how the API is designed. You provide the same phone number in both places.

9. CallBackURL

This is critically important, and we'll discuss it in much greater detail in a later section.

For now, understand that the CallBackURL is an endpoint on your server where Safaricom will send the results of the transaction. When the user completes or cancels the payment, Safaricom sends the result to this URL.

Critical requirement: The URL must be a complete HTTPS URL. It must use HTTPS, not HTTP. Safaricom requires this for security.

Example: https://yourdomain.com/api/mpesa/callback

10. AccountReference

This is a reference for the transaction, with a maximum of 12 characters.

You might be tempted to make this dynamic—perhaps using it to store an order number or invoice number. However, the 12-character limit makes this problematic. If you use something like "Invoice" plus a number, and that number grows large enough, you'll exceed 12 characters.

The recommendation is to keep this simple and static. Use something like "Invoice" or "Payment" or "Ticket" or another short, fixed value. Don't try to make it dynamic because the character limit will cause problems.

11. TransactionDesc

This is a description of the transaction, with a maximum of 13 characters.

The same limitations apply here as with AccountReference. The 13-character limit makes it difficult to use dynamic values. Keep it simple with something like "Ticket" or "Payment" or another short description.

Making the STK Push Request

Now that you understand all the parameters, let's see what the complete request looks like.

In Postman, you create a new POST request. You set the endpoint URL, add the Bearer Token authorization with your access token, set the Content-Type header to application/json, and create the request body:

json
{
  "BusinessShortCode": "174379",
  "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjYwMzE3MTkyODQ4",
  "Timestamp": "20260317192848",
  "TransactionType": "CustomerPayBillOnline",
  "Amount": 1,
  "PartyA": "254114742348",
  "PartyB": "174379",
  "PhoneNumber": "254114742348",
  "CallBackURL": "https://yourdomain.com/api/mpesa/callback",
  "AccountReference": "Ticket",
  "TransactionDesc": "Payment"
}

When you send this request and everything is correct, you'll receive a response from Safaricom.

Understanding the STK Push Response

The response from Safaricom contains several fields. Let's look at what a successful response looks like:

json
{
  "MerchantRequestID": "29115-34620561-1",
  "CheckoutRequestID": "ws_CO_20052025195358733114742348",
  "ResponseCode": "0",
  "ResponseDescription": "Success. Request accepted for processing",
  "CustomerMessage": "Success. Request accepted for processing"
}

ResponseCode and ResponseDescription tell you whether Safaricom accepted your request. A ResponseCode of "0" with a ResponseDescription of "Success. Request accepted for processing" means Safaricom has successfully sent the payment prompt to the user's phone.

This is crucial to understand: This response does NOT mean the payment is complete. It only means the prompt was successfully delivered to the user's phone.

At this point, the user sees a prompt on their phone showing:

  • The amount they're being asked to pay
  • Your business name
  • A request to enter their M-Pesa PIN

The user can now either:

  • Enter their PIN and complete the payment
  • Cancel the transaction
  • Ignore it and let it time out

The CheckoutRequestID: Your Transaction Identifier

Among all the fields in the STK Push response, the CheckoutRequestID is the most critical. You must understand why this field is so important and what you need to do with it.

The Concurrent Transaction Problem

Imagine you're building a successful movie booking platform. At any given moment during peak hours, you might have one hundred people all trying to buy tickets simultaneously. Your system sends one hundred STK Push requests to Safaricom, one for each customer. Safaricom processes all these requests and will eventually send one hundred responses back to your system through the callback.

Now here's the problem: When those callback responses arrive, how do you know which response belongs to which ticket?

Let's make this concrete. Say two different users both happen to buy three tickets to different movies, and both orders total 45 shillings. When a callback arrives saying "45 shillings was paid," which ticket was it for? The person who bought three Oppenheimer tickets? Or the person who bought three Barbie tickets?

You need a way to uniquely identify each transaction. This is exactly what the CheckoutRequestID provides.

How CheckoutRequestID Works

Every STK Push request generates a unique CheckoutRequestID. This ID appears in two places:

  1. In the initial response when you make the STK Push request
  2. Later in the callback when Safaricom sends you the final transaction results

By matching the CheckoutRequestID from the callback to the CheckoutRequestID you stored earlier, you can determine exactly which ticket a particular payment result belongs to.

The format looks like this: ws_CO_20052025195358733114742348

It's a unique string that identifies this specific payment request.

Storing the CheckoutRequestID

Remember that pushrequest table we created earlier? This is where it comes into play.

After you receive the STK Push response, you immediately create a record linking the CheckoutRequestID to your ticketId:

sql
INSERT INTO pushrequest (ticketId, checkoutRequestId) 
VALUES (67, 'ws_CO_20052025195358733114742348');

This creates the connection you'll need later. You're saying: "The CheckoutRequestID ws_CO_20052025195358733114742348 is associated with ticket number 67."

When the callback arrives later, it will include this same CheckoutRequestID. You'll query the pushrequest table:

sql
SELECT ticketId 
FROM pushrequest 
WHERE checkoutRequestId = 'ws_CO_20052025195358733114742348';

This query returns 67, and now you know: "This callback is about ticket 67." You can then update the correct ticket record with the payment results.

This is the essential link that makes everything work in a high-traffic, concurrent environment.

The Waiting Period: Understanding Async Behavior

After you've initiated the STK Push and stored the CheckoutRequestID, your system enters a waiting state. This is an important concept to grasp.

You are no longer in control of the process. The user has the payment prompt on their phone. They might:

  • Complete it immediately
  • Wait a few minutes before deciding
  • Cancel it
  • Simply ignore it
  • Experience network issues
  • Have their phone turned off

Any number of things might happen, and you cannot predict or control them.

Your system must wait for Safaricom to send you the results. You are not actively checking or polling for results. You are passively waiting for Safaricom to contact you.

This is what the callback is for. Safaricom will make an HTTP request to your CallBackURL when the transaction reaches a final state—whether that state is success or failure.

During this waiting period, your user interface might show the user a message like:

  • "Transaction processing. Please check your phone."
  • "Waiting for payment confirmation..."
  • "Please enter your PIN on your phone to complete the payment."

This keeps the user informed that something is happening and they shouldn't navigate away or refresh the page.

The waiting period is entirely normal. It's how the asynchronous nature of M-Pesa integration works. Your application initiates the request, then waits for the callback.

Understanding the Callback: Where Results Are Delivered

The callback is perhaps the most important part of the entire M-Pesa integration, and it's also where many developers encounter confusion. Let's make sure you understand exactly what it is and how it works.

What is a Callback?

A callback is an endpoint that you create on your server specifically to receive results from Safaricom.

Think about the endpoints in any application you've built. If you've created a system with user registration, you have an endpoint for handling registration. If you have login functionality, you have an endpoint for handling login. If you have profile updates, you have an endpoint for that.

An endpoint is simply a URL on your server that handles specific functionality. For example:

Each endpoint receives certain data, processes it, and returns a response.

The callback URL is exactly the same concept. It's an endpoint you create specifically for receiving M-Pesa transaction results. When Safaricom finishes processing a transaction—whether it succeeds or fails—Safaricom makes an HTTP POST request to your callback URL, sending the transaction details in the request body.

The HTTPS Requirement

There's one critical requirement for the callback URL: it must use HTTPS, not HTTP.

HTTP is unencrypted, meaning data travels in plain text. HTTPS is encrypted, protecting the transaction data in transit. Safaricom requires this for security—they won't send sensitive payment information over an unencrypted connection.

Your callback URL must be:

  • On a server with a valid SSL certificate
  • Publicly accessible so Safaricom's servers can reach it
  • Able to handle POST requests

Example: https://yourdomain.com/api/mpesa/callback

The Callback Response Structure

When Safaricom sends transaction results to your callback URL, the structure of the data depends on whether the transaction was successful or failed.

Successful Transaction

json
{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "38c2-4f3b-9aa9-1a2ea6ec7e4d1070625",
      "CheckoutRequestID": "ws_CO_20052025195358733114742348",
      "ResultCode": 0,
      "ResultDesc": "The service request is processed successfully.",
      "CallbackMetadata": {
        "Item": [
          {
            "Name": "Amount",
            "Value": 50
          },
          {
            "Name": "MpesaReceiptNumber",
            "Value": "TEE2QR5JMI"
          },
          {
            "Name": "Balance"
          },
          {
            "Name": "TransactionDate",
            "Value": 20250514201201
          },
          {
            "Name": "PhoneNumber",
            "Value": 254114742348
          }
        ]
      }
    }
  }
}

Let's break down the important fields:

ResultCode: A value of 0 indicates success. Any non-zero value indicates failure.

ResultDesc: A text description of the result. For success, it says something like "The service request is processed successfully."

CallbackMetadata: This section only appears for successful transactions. It contains an array of items with payment details:

  • Amount: How much was paid (50 in this example)
  • MpesaReceiptNumber: The confirmation code (TEE2QR5JMI in this example)
  • TransactionDate: When payment was completed, in format YYYYMMDDHHmmss (20250514201201 means May 14, 2025 at 20:12:01)
  • PhoneNumber: Which number made the payment (254114742348)

Failed Transaction

json
{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "38c2-4f3b-9aa9-1a2ea6ec7e4d1070625",
      "CheckoutRequestID": "ws_CO_14052025201149444114742348",
      "ResultCode": 1032,
      "ResultDesc": "Request cancelled by user"
    }
  }
}

For failed transactions, the structure is simpler:

ResultCode: A non-zero value (1032 in this example) indicates failure ResultDesc: Explains what went wrong ("Request cancelled by user") No CallbackMetadata: Since the payment didn't complete, there's no receipt number, no transaction date, and no amount actually paid

Different result codes indicate different failure reasons:

  • User cancelled the transaction
  • Insufficient funds
  • Wrong PIN entered
  • Phone unreachable
  • Timeout
  • System error

Processing the Callback: Success Case

When your callback endpoint receives data from Safaricom for a successful transaction, here's exactly what you need to do:

Step 1: Extract the CheckoutRequestID

From the callback payload, extract the CheckoutRequestID. In our example, it's ws_CO_20052025195358733114742348.

Step 2: Find the Associated Ticket

Query your pushrequest table to find which ticket this CheckoutRequestID is associated with:

sql
SELECT ticketId 
FROM pushrequest 
WHERE checkoutRequestId = 'ws_CO_20052025195358733114742348';

This returns the ticket ID—let's say it's 67.

Step 3: Extract Payment Details

From the CallbackMetadata section of the payload, extract:

  • The MpesaReceiptNumber (e.g., "TEE2QR5JMI")
  • The TransactionDate (e.g., 20250514201201)

Step 4: Update the Ticket Record

Update the ticket record with the payment details:

sql
UPDATE ticket 
SET 
    paymentStatus = 'Paid',
    mpesaReceiptNumber = 'TEE2QR5JMI',
    transactionDate = '2025-05-14 20:12:01'
WHERE ticketId = 67;

Notice:

  • paymentStatus changes from 'Pending' to 'Paid'
  • mpesaReceiptNumber gets the actual receipt number
  • transactionDate gets the actual date and time (you may need to parse the timestamp format from YYYYMMDDHHmmss to a proper DATETIME)

Step 5: Send User Notifications (Optional but Recommended)

This is the appropriate time to send any user notifications. If you want to send a confirmation email telling the customer their payment was successful and here are their tickets, you send that email now. If you want to send an SMS with their ticket details, you send it now.

All communications about successful payment happen in the callback handler after you've updated the database.

Step 6: Respond to Safaricom

After processing the successful payment, you must send a response back to Safaricom. You send an HTTP 200 status code with a simple acknowledgment:

sql
{
  "ResultCode": 0,
  "ResultDesc": "Success"
}

This response does not indicate whether the payment was successful—Safaricom already knows that. It simply acknowledges that you received their callback. You're telling them: "I got your message, thank you."

Processing the Callback: Failure Case

For failed transactions, the process is similar but simpler:

Step 1: Extract the CheckoutRequestID

Same as before—extract it from the callback payload.

Step 2: Find the Associated Ticket

Query the pushrequest table:

sql
SELECT ticketId 
FROM pushrequest 
WHERE checkoutRequestId = 'ws_CO_14052025201149444114742348';

This returns the ticket ID—let's say it's 67.

Step 3: Update the Ticket Record

For failures, you only update the payment status:

sql
UPDATE ticket 
SET paymentStatus = 'Failed'
WHERE ticketId = 67;

Notice:

  • paymentStatus changes from 'Pending' to 'Failed'
  • mpesaReceiptNumber remains NULL (there's no receipt because payment didn't complete)
  • transactionDate remains NULL (no successful transaction means no transaction date)

You might optionally store the ResultDesc somewhere for debugging or customer support purposes, but it's not required.

Step 4: Send User Notifications (Optional)

You might want to notify the user that payment failed. You could send an email or SMS saying something like "Your payment was not completed. Please try again." This helps the user understand what happened and gives them an opportunity to retry.

Step 5: Respond to Safaricom

Critically important: You still send an HTTP 200 response to Safaricom, even for failed transactions.

json
{
  "ResultCode": 0,
  "ResultDesc": "Success"
}

This confuses some developers. Why are we sending "Success" when the payment failed?

Because the 200 response isn't about whether the payment succeeded. It's about whether you successfully received the callback. You're telling Safaricom: "I got your message about the failed payment. Thank you for informing me."

Safaricom already knows the payment failed—they're the ones telling you about it. What they need to know is whether you received that information. That's what the 200 response confirms.

If you don't send a 200 response, Safaricom might retry sending the callback, thinking you didn't receive it. Always acknowledge receipt, regardless of the payment outcome.

STK Query: Checking Transaction Status (Optional)

After covering access tokens, STK Push, and callbacks, there's one more piece to discuss: STK Query. But first, let me be very clear about something.

STK Query is optional. You can build a fully functional M-Pesa integration without ever using STK Query. The callback is what handles the actual processing of payment results.

So why does STK Query exist, and when might you want to use it?

The User Experience Gap

End-to-end sequence diagram illustrating the complete M-Pesa payment flow: user selection, backend ticket creation, STK Push initiation, user phone prompt, payment completion, and callback processing.
Complete M-Pesa Integration Flow Sequence Diagram

Remember that after you initiate an STK Push, there's a waiting period. The user needs to check their phone, enter their PIN, and confirm the payment. During this time, your application is waiting for the callback from Safaricom.

But from the user's perspective on your website or app, it might look like nothing is happening. They might wonder:

  • Is the system working?
  • Did the payment go through?
  • Should I try again?
  • Did something go wrong?

STK Query allows you to proactively check the status of a transaction and provide feedback to the user. You could poll Safaricom every few seconds, asking for the status of the transaction, and update your user interface to show:

  • "Waiting for payment..."
  • "Payment successful"
  • "Payment failed"

This makes the user experience smoother because they get immediate feedback rather than just seeing a blank waiting screen.

Important Limitations

However, there are important limitations you must understand about STK Query

It's Not Reliable for Database Updates

The callback is the authoritative source of transaction results. STK Query can fail due to:

  • Rate limiting (Safaricom limits how many queries you can make)
  • Network issues
  • Temporary system problems

If you relied on STK Query to update your database and the query failed, you'd have incorrect data. That's why you must always use the callback for database updates, never STK Query.

Rate Limiting

Safaricom implements rate limiting on STK Query requests. If you send too many queries in a short period, Safaricom will start rejecting your requests with error responses. This makes sense from their perspective—they don't want their systems overwhelmed by constant polling.

But it means you can't rely on STK Query being available whenever you want.

When to Use STK Query

Given these limitations, STK Query is best used sparingly and only for enhancing the user interface.

You might query once or twice during the waiting period to show the user that their transaction is being processed. But you never use it as the primary mechanism for determining transaction outcomes. That's what the callback is for.

Think of it this way:

  • Callback: The authoritative, reliable source of transaction results. Always process here.
  • STK Query: An optional UI enhancement to improve user experience. Never rely on it for critical functionality.

Making an STK Query Request

If you decide to use STK Query, the process is similar to other API calls.

Endpoint

For sandbox:

markdown
https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query

For production:

markdown
https://api.safaricom.co.ke/mpesa/stkpushquery/v1/query

HTTP Method: POST

Authorization: Bearer Token (using a fresh access token)

Required Parameters:

  1. BusinessShortCode - Same shortcode you used for the STK Push
  2. Password - Generated the same way as for STK Push (Base64 encoding of Shortcode + Passkey + Timestamp)
  3. Timestamp - Current time in YYYYMMDDHHmmss format
  4. CheckoutRequestID - The ID from the original STK Push request

A Postman interface showing an STK Query API request to check transaction status. Notice the POST method, Bearer token authorization, and the JSON payload containing BusinessShortCode, Password, Timestamp, and CheckoutRequestID parameters.
STK Query Request in Postman

When you send this query, Safaricom responds with the current status of the transaction. The response structure is similar to what you would receive in a callback.

You can use this information to update your user interface, but remember: never use it to update your database. The callback is the only reliable source for that.

The Complete Integration Flow: Putting It All Together

Let's review the complete flow from start to finish so you can see how all these pieces fit together in a real application.

Display Available Movies

Your application displays available movies by querying the movie table. Each movie shows its title, description, show time, and price.

User Selects a Movie

The user sees "Oppenheimer" and clicks "Book Now." This captures the movieId.

Collect Booking Details

The system presents a form:

  • Customer name: "Baraza Michael"
  • Phone number: "0114742348"
  • Quantity: 3 tickets

The system shows the total: 3 tickets × 15 shillings = 45 shillings.

User Submits

The user clicks "Book Ticket." The frontend sends all this data to your backend.

Create Ticket Record

Your backend creates a ticket record in the database:

sql
INSERT INTO ticket (movieId, customerName, phoneNumber, quantity, totalAmount, paymentStatus)
VALUES (4, 'Barasa Michael', '0114742348', 3, 45.00, 'Pending');
This returns ticketId = 67.

Format Phone Number

Convert 0114742348 to 254114742348.

Get Access Token

Make a GET request to the access token endpoint with your Consumer Key and Consumer Secret. Receive the access token.

Generate Password

Concatenate: 174379 + passkey + timestamp
Base64 encode the result.

Initiate STK Push

Make a POST request to the STK Push endpoint with all required parameters. Safaricom responds with:

json
{
  "CheckoutRequestID": "ws_CO_20052025195358733114742348",
  "ResponseCode": "0",
  "ResponseDescription": "Success. Request accepted for processing"
}

Store Push Request

Create a record linking the CheckoutRequestID to the ticket:

sql
INSERT INTO pushrequest (ticketId, checkoutRequestId)
VALUES (67, 'ws_CO_20052025195358733114742348');

Wait

Your system waits. The user sees the prompt on their phone. They enter their PIN or cancel or ignore it.

Callback Arrives

Minutes later (or seconds, depending on the user), Safaricom sends an HTTP POST to your callback URL with the results.

A. If Successful

Extract the CheckoutRequestID from the callback. Query pushrequest table to find ticketId = 67. Extract receipt number and transaction date. Update the ticket:

sql
UPDATE ticket 
SET paymentStatus = 'Paid',
    mpesaReceiptNumber = 'TEE2QR5JMI',
    transactionDate = '2025-05-14 20:12:01'
WHERE ticketId = 67;

Send confirmation email/SMS to user. Respond to Safaricom with HTTP 200.

B. If Failed

Extract the CheckoutRequestID. Find the ticket. Update it:

sql
UPDATE ticket 
SET paymentStatus = 'Failed'
WHERE ticketId = 67;

Optionally notify user. Respond to Safaricom with HTTP 200.

User Interface Updates

Your frontend (through polling, WebSockets, or page refresh) detects that the ticket status changed. For success, it shows a confirmation message and ticket details. For failure, it shows an error and offers to retry.

That's the complete flow. Every payment integration you build follows this same pattern.

Key Principles to Remember

As you implement M-Pesa integration, keep these critical points in mind:

Access Tokens

  • Request a fresh token for each API operation
  • Don't try to manage expiration
  • It's simpler and more reliable to just get a new token every time

Phone Number Formatting

  • Remove the plus sign if present
  • Replace leading zero with 254
  • Always validate the format before making API calls
  • This is a common source of errors, so pay close attention

Database Records First

  • Always create your database record before initiating the STK Push
  • Never do it the other way around
  • You need that record to exist so you have something to update when the callback arrives

CheckoutRequestID is Critical

  • Always store it and link it to your transaction record
  • This is how you match callback results to the correct transaction
  • Without this, you can't handle concurrent transactions properly

STK Push Response vs Payment Completion

  • The STK Push response only tells you the prompt was delivered
  • It does NOT mean payment is complete
  • Never update your database based on the STK Push response alone

Callbacks Handle Everything

  • Always process both success and failure cases in your callback
  • Both require proper database updates
  • Always respond to Safaricom with HTTP 200 after receiving a callback, regardless of payment outcome

Never Use STK Query for Database Updates

  • Use it only for user interface enhancements if you use it at all
  • The callback is the authoritative source of transaction results
  • Rate limiting makes STK Query unreliable for critical operations

HTTPS is Mandatory

  • Your callback URL must use HTTPS
  • HTTP will not work
  • Get a valid SSL certificate for your domain

Testing Thoroughly

  • Test extensively in sandbox before moving to production
  • Test every scenario you can think of: success, failure, cancellation, timeout
  • Use 1 shilling for all sandbox tests to minimize costs

Production Credentials are Sacred

  • Never commit them to version control
  • Store them as environment variables or in secure configuration systems
  • Only share with trusted team members who absolutely need them

Common Integration Patterns

The movie ticket booking example demonstrates the fundamental pattern, but M-Pesa integration applies to countless other scenarios. Understanding how to adapt this pattern to different use cases is valuable.

E-Commerce Checkout

The pattern is nearly identical to the movie example. Instead of a movie table, you have a product table. Instead of a ticket table, you have an order table and perhaps an order_items table for storing multiple products in a single order.

The user adds products to their cart, proceeds to checkout, and the system calculates the total. You create the order record, initiate the STK Push, and handle the callback to update the order status.

Booking Systems (Hotels, Flights, Trains)

You might have additional complexity around inventory management. When a user initiates a booking, you might temporarily reserve that seat or room while waiting for payment. If the payment succeeds, you confirm the reservation. If it fails or times out, you release the reservation back to available inventory.

The core payment flow remains the same—create the booking record, initiate STK Push, wait for callback, update based on results.

Subscription Services

You create a subscription record when the user signs up. The first payment works exactly like the examples shown. For recurring payments, you might use M-Pesa Ratiba, which is Safaricom's API for automatic recurring charges. The initial integration pattern remains the same, but you have additional functionality for managing renewals.

Bill Payment Systems

For utilities, service fees, or any kind of bill payment, the pattern is the same. You create a bill record or payment record, initiate the STK Push for the bill amount, and update the record based on the callback results.

In all these cases, the core flow never changes:

  1. Create a record before payment
  2. Initiate STK Push with the appropriate amount
  3. Wait for the callback
  4. Update your database based on the results

Development Environment Considerations

One challenge you'll face during development is the HTTPS requirement for callbacks. Safaricom requires that your callback URL use HTTPS, but local development environments typically don't have SSL certificates.

The Local Development Problem

When you're developing on your local machine (localhost), you're usually running on HTTP, not HTTPS. But Safaricom won't send callbacks to HTTP URLs. Additionally, your localhost isn't publicly accessible—Safaricom's servers can't reach it even if they wanted to.

This creates a chicken-and-egg problem: you need to test callbacks to make sure your code works, but you can't receive callbacks during local development.

The Solution: Ngrok

The practical solution is to use a tool called Ngrok. Ngrok creates a secure tunnel from a public HTTPS URL to your localhost, solving both problems at once:

  1. It provides a public URL that Safaricom's servers can reach
  2. That URL uses HTTPS, meeting Safaricom's security requirement
  3. It tunnels requests to your local development server

The Ngrok dashboard displaying authentication token configuration. This secure tunneling service enables local development servers to receive HTTPS callbacks from Safaricom during M-Pesa integration testing, solving the localhost accessibility challenge.
Ngrok Authentication Dashboard

How Ngrok Works

You run Ngrok on your computer. It connects to Ngrok's cloud service and opens a tunnel. Ngrok gives you a public HTTPS URL like https://abcd1234.ngrok.io. When Safaricom sends a callback to that URL, Ngrok receives it and forwards it to your localhost (for example, localhost:5000).

Installing Ngrok

On Windows:

  1. Download from https://ngrok.com/download
  2. Unzip the file
  3. Optionally move ngrok.exe to a directory in your PATH

On Linux:

bash
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
unzip ngrok-stable-linux-amd64.zip
sudo mv ngrok /usr/local/bin

Configuring Ngrok

  1. Create a free account at https://dashboard.ngrok.com
  2. Get your authentication token from the dashboard
  3. Configure ngrok with your token:
bash
ngrok config add-authtoken YOUR_AUTHTOKEN

Running Ngrok

If your local server is running on port 5000:

bash
ngrok http 5000

Ngrok will display output showing your public URLs. You'll see something like:

An active Ngrok tunnel forwarding HTTPS requests to localhost:5000. The terminal displays the public HTTPS URL that Safaricom can reach during local development, enabling real-time callback testing without deploying to a production server.
Ngrok Terminal Session in Action

Use that HTTPS URL (https://5eaf-31-14-252-3.ngrok-free.app/api/mpesa/callback) as your callback URL in your M-Pesa API requests.

Important Notes

  • Your local server must be running and handling POST requests at the specified endpoint
  • The ngrok URL changes each time you restart ngrok (unless you pay for a static subdomain)
  • Some secure networks (like university networks) may block ngrok, requiring a VPN or personal network
  • Never use ngrok URLs in production—only for local development and testing

This setup allows you to develop and test locally while still being able to receive callbacks from Safaricom.

Conclusion: You're Ready to Integrate

You've now learned the complete M-Pesa integration process from start to finish. Let's recap the journey:

You understand what a Daraja "app" really means—not a standalone application, but a set of credentials for your actual application to communicate with Safaricom's systems.

You know the difference between sandbox and production environments, and why you should do all your development and testing in sandbox before moving to production.

You understand the four fundamental concepts that make up M-Pesa integration:

  1. Access Tokens - How you authenticate with Safaricom
  2. STK Push - How you trigger payment prompts
  3. STK Query - How you optionally check status for UX
  4. Callback - How you receive definitive results

You've learned the critical importance of database design, specifically:

  • Creating the ticket record before initiating payment
  • Understanding the three payment states: Pending, Paid, Failed
  • Why receipt numbers and transaction dates start as NULL
  • How the pushrequest table links CheckoutRequestIDs to tickets

You understand phone number formatting requirements and why they matter.

You know every parameter required for an STK Push request and where each value comes from.

You grasp why the CheckoutRequestID is critical for handling concurrent transactions in production systems.

You understand the asynchronous nature of the process—how you initiate the push, then wait for Safaricom to send results via callback.

You know how to process both successful and failed callbacks, what to update in your database, and why you always send HTTP 200 back to Safaricom regardless of payment outcome.

You understand that STK Query is optional and only for user experience enhancement, never for database updates.

Most importantly, you understand the complete end-to-end flow and how all these pieces fit together in a real application.

The patterns you've learned here apply to any payment scenario. Whether you're building e-commerce, bookings, subscriptions, or bill payments, the fundamental flow remains the same. You've gained transferable knowledge that you can apply across countless projects.

M-Pesa integration might have seemed intimidating at first, but now you understand that it's really just a series of logical steps. Each step has a clear purpose. Each piece of data has a specific role. When you understand why things work the way they do, implementation becomes straightforward.

Start with sandbox. Test thoroughly. Build confidence in your understanding. And when you're ready, move to production and start processing real payments for real customers.

You now have the knowledge. The rest is just implementation.

Next Steps

In a follow-up article, we'll dive deep into the code implementation, showing you exactly how to build this integration in Python, PHP, and JavaScript. But for now, you have the conceptual foundation you need to understand M-Pesa integration completely. The implementation details will make much more sense because you understand the why behind every step.

Discussion (0)

Be the first to comment

Join the discussion

Share your thoughts and engage with the community

Your comment will appear immediately

No comments yet

Be the first to share your thoughts on this article!