daraja4j is a dependency-free Java SDK for the Safaricom Daraja M-Pesa API: OAuth access-token management, M-Pesa Express (STK Push), C2B, B2C, B2B, B2Pochi, transaction reversal, transaction status, account balance, the Pull API, and M-Pesa Ratiba standing orders — with a plain, framework-agnostic Java API that drops into Spring Boot, Jakarta EE, Quarkus, Micronaut, plain servlets, or a bare public static void main.
It is an independent, community-built SDK. It is not produced, endorsed, or supported by Safaricom or M-Pesa.
- Runs everywhere —
daraja4j-corecompiles and is tested against the Java 8 language level, so the same jar runs unmodified on Java 8, 11, 17, 21, and every release after it. (daraja4j-jakartaneeds Java 11+ anddaraja4j-spring-boot3-starterneeds Java 17+ — inherent to the ecosystems they target, Jakarta EE 9+ and Spring Boot 3 respectively.) - Zero runtime dependencies at the core —
daraja4j-coreuses onlyjavax.crypto,java.net,java.security, andjava.mathfrom the JDK itself. Nothing to conflict with your existing dependency tree. Framework modules add only what that framework already requires. - Framework-neutral by design — the core has no compile-time knowledge of Spring, servlets, or any web framework, so it works identically in a Spring Boot controller, a Quarkus resource, a Lambda handler, or a CLI tool.
- Correct by construction —
BigDecimalfor money (never a lossydouble), a builder API instead of stringly-typed maps, transparent OAuth token caching/refresh, and typed parsers for every callback shape Daraja sends. - Available for both Maven and Gradle — the source tree builds under both, so contributors aren't locked into one toolchain.
- Async-friendly — every operation has a
CompletableFuture-returning sibling, so it composes cleanly with reactive and async codebases without pulling in a reactive library. - A real JPMS module, not just a manifest hint —
daraja4j-coreships as a multi-release jar with a genuinemodule-info.javafor Java 9+, and swaps in ajava.net.http.HttpClient-based transport (HTTP/2, negotiated automatically) for Java 11+ — both transparent to callers. - Ships as a native binary too —
daraja4j-clibuilds with GraalVM native-image for a dependency-free, instant-startup executable, alongside the regular JVM fat jar.
| Supported | |
|---|---|
| Java | 8, 11, 17, 21, and every release in between (compiled at the --release 8 bytecode level) |
| Build tools (as a dependency) | Any Maven 3.x or Gradle version — it's a plain jar + standard POM once published |
| Build tools (building this repo) | Maven 3.6.3+, Gradle 8.0+ |
| Frameworks | Plain Java, Spring / Spring Boot (2.x and 3.x, dedicated starters), Jakarta EE & Java EE servlets (dedicated adapters), Quarkus, Micronaut, Vert.x, Android (API 26+, via desugaring) |
| Module system | Works on the classpath (Java 8+) and as a real named module on the module path (Java 9+) |
Live on Maven Central — 0.2.2 is published and resolvable with no extra repository configuration.
<dependency>
<groupId>io.github.josemodi97</groupId>
<artifactId>daraja4j-core</artifactId>
<version>0.2.2</version>
</dependency>implementation("io.github.josemodi97:daraja4j-core:0.2.2")implementation 'io.github.josemodi97:daraja4j-core:0.2.2'Javadoc: javadoc.io/doc/io.github.josemodi97/daraja4j-core
The daraja4j-gradle-plugin project-scaffolding plugin is uploaded to the Gradle Plugin Portal but pending its first-time manual ID review — see Project scaffolding below.
Building from source instead? mvn install / ./gradlew publishToMavenLocal from a clone of this repo installs to your local repository. See PLAN.md for the release process.
daraja4j-core is the only one you need for a plain Java app. Add one of these on top for framework glue:
| Artifact | For | Brings |
|---|---|---|
daraja4j-core |
Any Java 8+ app | Daraja4jClient, OAuth token caching, and every Daraja operation. No dependencies. |
daraja4j-servlet |
Tomcat 8/9, Spring Boot 2, plain Java EE servlets | javax.servlet callback handlers for STK, Result, and C2B notifications. |
daraja4j-jakarta |
Tomcat 10+, Spring Boot 3, Jakarta EE 9+ | The same handlers, for the jakarta.servlet namespace. |
daraja4j-spring-boot2-starter |
Spring Boot 2.x | Auto-configured Daraja4jClient bean from daraja4j.* properties, plus opt-in webhook endpoints + events. |
daraja4j-spring-boot3-starter |
Spring Boot 3.x | Same, for the Jakarta namespace (Java 17+ floor). |
daraja4j-cli |
Terminal / CI | stk-push, stk-query, status, b2c, reverse, balance, qr, parse-callback subcommands. |
Daraja4jConfig config = Daraja4jConfig.builder()
.consumerKey("YOUR_CONSUMER_KEY")
.consumerSecret("YOUR_CONSUMER_SECRET")
.environment(Daraja4jConfig.Environment.SANDBOX)
.defaultShortcode("174379")
.defaultPasskey("YOUR_PASSKEY")
.defaultCallbackUrl("https://yourapp.example.com/daraja4j/stk-callback")
.build();
Daraja4jClient client = new Daraja4jClient(config);Prefer environment variables (containers, CI, 12-factor apps)? Daraja4jConfig.fromEnvironment() reads DARAJA4J_CONSUMER_KEY, DARAJA4J_CONSUMER_SECRET, DARAJA4J_ENVIRONMENT, DARAJA4J_SHORTCODE, DARAJA4J_PASSKEY, DARAJA4J_CALLBACK_URL, and more — see Daraja4jConfig javadoc for the full list.
OAuth access tokens are fetched, cached, and refreshed automatically — you never touch a token directly.
StkPushResult ack = client.stkPush(StkPushRequest.builder()
.amount(500)
.partyA("0712345678")
.accountReference("INV-0001")
.transactionDesc("School fees")
.build());
System.out.println(ack.getCheckoutRequestId());ack is only Daraja's acknowledgement that the USSD prompt was sent — the actual outcome arrives later at your callbackUrl, or can be polled with client.stkPushQuery(...).
// e.g. inside a Spring @PostMapping, a servlet doPost, or any framework's
// raw-body-to-String binding:
StkCallbackResult result = StkCallbackResult.parse(rawJsonBody);
if (result.isSuccess()) {
markOrderPaid(result.getCheckoutRequestId(), result.getMpesaReceiptNumber());
} else {
log.warn("daraja4j: STK push failed - {}", result.getResultDesc());
}Read the Security section below before wiring this into production — Daraja sends no cryptographic signature on callbacks.
B2cResult ack = client.b2c(B2cRequest.builder()
.amount(1000)
.partyB("0712345678")
.remarks("Refund for order 4821")
.build());Requires initiatorName and securityCredential — see SecurityCredentialEncoder for encoding your Initiator password with Safaricom's public certificate.
Daraja4jStkCallbackServletHandler handler = new Daraja4jStkCallbackServletHandler()
.onSuccess((result, req, res) -> markOrderPaid(result.getCheckoutRequestId(), result.getMpesaReceiptNumber()))
.onFailure((result, req, res) -> log.warn(result.getResultDesc()));
// inside your doPost(HttpServletRequest req, HttpServletResponse res):
handler.handle(req, res);(daraja4j-jakarta is the identical API under jakarta.servlet imports instead of javax.servlet.) Daraja4jResultCallbackServletHandler covers B2C/B2B/B2Pochi/Reversal/Balance/StatusQuery results, and Daraja4jC2bServletHandler covers C2B confirmations.
# application.yml
daraja4j:
consumer-key: ${DARAJA4J_CONSUMER_KEY}
consumer-secret: ${DARAJA4J_CONSUMER_SECRET}
environment: SANDBOX
shortcode: "174379"
passkey: ${DARAJA4J_PASSKEY}
callback-url: https://yourapp.example.com/daraja4j/stk-callback
webhook:
stk:
enabled: trueThat's it — a Daraja4jClient bean is now available for injection, and POST /daraja4j/stk-callback is live. React to a payment without writing a controller:
@EventListener
void onPaid(Daraja4jStkPaymentVerifiedEvent event) {
orderService.markPaid(event.getResult().getCheckoutRequestId(), event.getResult().getMpesaReceiptNumber());
}The result and C2B endpoints are the same shape, gated by daraja4j.webhook.result.enabled and daraja4j.webhook.c2b.enabled respectively. Use daraja4j-spring-boot2-starter on Spring Boot 2.x, daraja4j-spring-boot3-starter on 3.x.
# trigger an STK push
java -jar daraja4j-cli.jar stk-push --consumer-key ... --consumer-secret ... --environment SANDBOX \
--shortcode 174379 --passkey ... --callback-url https://yourapp.example.com/daraja4j/stk-callback \
--amount 500 --phone 0712345678 --account-reference INV-0001
# query the outcome of an STK push
java -jar daraja4j-cli.jar stk-query --checkout-id ws_CO_191220191020363925
# generate a Dynamic QR code and save it as a PNG image
java -jar daraja4j-cli.jar qr --merchant-name "My Store" --ref-no INV-0001 --amount 500 --output qrcode.png
# check transaction status
java -jar daraja4j-cli.jar status --transaction-id NLJ7RT61SV
# parse a captured callback payload, piped straight from curl/a log
cat captured-callback.json | java -jar daraja4j-cli.jar parse-callback --type stkEvery flag falls back to a DARAJA4J_* environment variable, so CI pipelines can omit credentials from the command line entirely.
daraja4j-maven-plugin and daraja4j-gradle-plugin write a placeholder daraja4j.properties into your project (without overwriting one that already exists) — fill in your credentials and you're ready to build a Daraja4jClient.
# Maven
mvn io.github.josemodi97:daraja4j-maven-plugin:init
# Gradle (after adding: plugins { id("io.github.josemodi97.daraja4j") version "0.2.2" })
./gradlew daraja4jInitBoth auto-detect which framework you're using from your project's own dependencies — a jakarta.servlet-api/javax.servlet-api dependency, or a Spring Boot 2.x/3.x dependency, gets you a working, compile-verified STK-callback example dropped into src/main/java/. Override with -Ddaraja4j.framework=<value> (Maven) or framework.set("<value>") on the daraja4jInit task (Gradle). Supported values: auto (Maven default) / plain / servlet / jakarta / spring-boot2 / spring-boot3.
| Method | Description |
|---|---|
client.stkPush(StkPushRequest) |
Triggers an M-Pesa Express USSD PIN prompt |
client.stkPushQuery(StkPushQueryRequest) |
Polls the outcome of a previous STK push |
client.generateQrCode(QrCodeRequest) |
Generates an M-Pesa Dynamic QR code image |
client.registerC2bUrls(RegisterC2bUrlsRequest) |
Registers validation/confirmation URLs for a shortcode |
client.simulateC2b(C2bSimulateRequest) |
Simulates an inbound C2B payment — sandbox only |
client.b2c(B2cRequest) |
Sends money to a registered M-Pesa customer |
client.b2b(B2bRequest) |
Pays another organization's pay bill or till |
client.b2Pochi(B2PochiRequest) |
Pays a Pochi la Biashara-enabled personal till |
client.reverseTransaction(ReversalRequest) |
Reverses a completed C2B transaction |
client.queryTransactionStatus(TransactionStatusRequest) |
Checks the status of a prior transaction |
client.queryAccountBalance(AccountBalanceRequest) |
Queries the current account balance |
client.registerPullTransactions(PullTransactionsRegisterRequest) |
One-time Pull API registration |
client.queryPullTransactions(PullTransactionsQueryRequest) |
Retrieves C2B transactions for a period |
client.createStandingOrder(StandingOrderRequest) |
Creates an M-Pesa Ratiba standing order |
client.refreshToken() |
Forces a fresh OAuth token fetch |
PhoneNormalizer.normalize(String) |
Normalizes Kenyan MSISDNs (07..., 01..., +254... → 254...) |
StkCredentialsGenerator.password(...) |
Builds the STK Push Password/Timestamp pair |
SecurityCredentialEncoder.encode(...) |
RSA-encrypts an Initiator password for SecurityCredential |
Every operation above has a corresponding *Async CompletableFuture variant (stkPushAsync, b2cAsync, generateQrCodeAsync, etc.).
Full Javadoc: https://javadoc.io/doc/io.github.josemodi97/daraja4j-core (populates once published to Maven Central).
All 14 Daraja endpoints supported by daraja4j have been exercised and verified against the live Safaricom Daraja Sandbox environment:
| API Tool / Endpoint | HTTP / API Status | Outcome | Sandbox Response Details |
|---|---|---|---|
1. OAuth Token (/oauth/v1/generate) |
200 OK | PASSED | Valid access token generated (expires_in: 3599). |
2. Dynamic QR Code (/mpesa/qrcode/v1/generate) |
200 OK | PASSED | ResponseCode: "00", "The service request is processed successfully." |
3. STK Push (M-Pesa Express) (/mpesa/stkpush/v1/processrequest) |
200 OK | PASSED | ResponseCode: "0", CheckoutRequestID: ws_CO_05102026... |
4. STK Query (/mpesa/stkpushquery/v1/query) |
200 OK | PASSED | ResponseCode: "0", ResultCode: 4999 ("Transaction still under processing"). |
5. C2B Register URLs (v1 & v2) (/mpesa/c2b/v2/registerurl) |
200 OK | PASSED | ResponseCode: "00000000", ResponseDescription: "Success". |
6. C2B Simulate (/mpesa/c2b/v1/simulate) |
200 OK | PASSED | ResponseDescription: "Accept the service request successfully." |
7. B2C Payment (Disbursement) (/mpesa/b2c/v3/paymentrequest) |
200 OK | PASSED | ResponseCode: "0", ResponseDescription: "Accept the service request successfully." |
8. B2B Payment (/mpesa/b2b/v1/paymentrequest) |
200 OK | PASSED | ResponseCode: "0", ResponseDescription: "Accept the service request successfully." |
9. B2Pochi (/mpesa/b2c/v3/paymentrequest) |
200 OK | PASSED | ResponseCode: "0", ResponseDescription: "Accept the service request successfully." |
10. Account Balance (/mpesa/accountbalance/v1/query) |
200 OK | PASSED | ResponseCode: "0", ConversationID: AG_20261005_... |
11. Transaction Status (/mpesa/transactionstatus/v1/query) |
200 OK | PASSED | ResponseCode: "0", ConversationID: AG_20261005_... |
12. Transaction Reversal (/mpesa/reversal/v1/request) |
200 OK | PASSED | ResponseCode: "0", ConversationID: AG_20261005_... |
13. Pull Transactions Register (/pulltransactions/v1/register) |
400 Bad Request | ℹ️ Portal Req. | 400.001 - Invalid NominatedNumber (Requires a pre-nominated phone number registered under the shortcode on the Daraja portal). |
14. M-Pesa Ratiba (Standing Orders) (/standingorder/v1/createStandingOrderExternal) |
400 Bad Request | ℹ️ Portal Req. | responseCode: 400 - "Value in field 'BusinessShortCode' can only be what was issued for this application" (Requires the specific BusinessShortCode assigned when registering the Ratiba app). |
- Keep secrets out of source control. Load
consumerKey/consumerSecret/securityCredentialfrom environment variables, a secrets manager, orDaraja4jConfig.fromEnvironment()— never hardcode them. - Daraja sends no cryptographic signature on callbacks. Unlike some payment gateways, STK/Result/C2B callback bodies carry no HMAC or signature field to verify. The
.modelparsers (StkCallbackResult,Daraja4jResultCallback,C2bConfirmation,StandingOrderCallback) are structural parsers only — they validate JSON shape, not authenticity. Establish trust instead through:- Unguessable callback URLs — embed a long random path segment or per-merchant token in your
callbackUrl/resultUrl/confirmationUrl, and check it before parsing the body. - Reconciliation before crediting an account — treat a callback as advisory and confirm anything money-affecting with a follow-up
queryTransactionStatus/stkPushQuerycall before marking an order paid, rather than trusting the push alone. - Network-layer IP allow-listing as defense-in-depth only (Safaricom's outbound IP ranges can change — never rely on this as your sole control).
- HTTPS + POST-only + body-size-limited handlers.
- Unguessable callback URLs — embed a long random path segment or per-merchant token in your
SecurityCredentialrequires the right certificate for your environment.SecurityCredentialEncoder.encode(...)RSA-encrypts your Initiator password with Safaricom's public certificate — sandbox and production use different certificates. Using the wrong one for your target environment is the single most common Daraja integration failure, since the request otherwise looks well-formed and only fails opaquely at Safaricom's end.- STK Push timestamps must use Nairobi time.
StkCredentialsGenerator.timestamp()deliberately usesAfrica/Nairobirather than the JVM's default zone — a server hosted in UTC calling rawLocalDateTime.now()would silently fail Daraja's timestamp-freshness check. - Always normalize phone numbers with
PhoneNormalizer.normalize(...)before sending an STK push, B2C, or B2B request — Safaricom requires the2547XXXXXXXX/2541XXXXXXXXshape.
All modules listed above are implemented, tested, and published: 0.1.0 is live on Maven Central, with both a real Maven and a real Gradle build, daraja4j-core shipping as a genuine multi-release JPMS module, and GraalVM native-image wired for the CLI. What's left: daraja4j-gradle-plugin is uploaded to the Gradle Plugin Portal and awaiting its first-time manual ID review, and richer init scaffolding beyond a single STK-callback example. See PLAN.md for the full architecture and what's actually left.
Issues and pull requests are welcome — see PLAN.md for the module layout. Build everything with mvn test or ./gradlew test from the repo root; daraja4j-gradle-plugin is a standalone Gradle build (cd daraja4j-gradle-plugin && ./gradlew test) since a Gradle plugin project can't sanely nest inside the reactor it builds.
See CHANGELOG.md for release history.
MIT License. See LICENSE for details.