daraja 0.2.0 copy "daraja: ^0.2.0" to clipboard
daraja: ^0.2.0 copied to clipboard

M-Pesa STK Push and B2C disbursements for Flutter — initiation, real-time callback delivery, timeout handling, and killed-app recovery. Backed by Appwrite.

daraja #

M-Pesa STK Push and B2C for Flutter, with the callback half built for you. Initiate a payment, get the result in the UI sub-second over a WebSocket, and survive app-kill and timeouts — without running a server.

final stream = await daraja.stkPush(
  phone: '0712345678',
  amount: 1000,
  reference: 'ORDER-001',   // max 12 chars
  description: 'Payment',    // max 13 chars
);

stream.listen((state) => switch (state) {
  PaymentSuccess(:final receiptNumber) => showReceipt(receiptNumber),
  PaymentFailed(:final message)        => showError(message),
  PaymentTimeout()                     => showNeutral(), // money may have moved
  _                                    => showProgress(),
});

Why #

Initiating an STK Push is one HTTP call. The hard part is everything after it: the result arrives asynchronously at a public URL you have to host, the customer backgrounds your app to enter their PIN, your tunnel dies mid-payment, and a polling loop adds 10–40s of lag to every payment.

daraja puts that URL in an Appwrite Function and streams the result back over Appwrite Realtime (sub-second). Polling is a fallback, not the path. App-kill recovery runs on next launch via restorePendingPayment(). You deploy one function, create one collection, and write about eight lines of app code.

Setup #

Deploy the function in function/ and create one collection — the runbook is function/README.md. It takes five minutes, but three things silently break every payment if you skip them:

  • Grant the function documents.write scope, then redeploy — scope changes don't take effect until you do.
  • Set the function's Execute access to Any. Safaricom sends no auth headers.
  • There is no APPWRITE_API_KEY env var on Appwrite 1.7+; the function reads the dynamic key from the x-appwrite-key header.

Then point daraja at your project:

final daraja = Daraja(
  config: DarajaConfig(
    consumerKey: '...', consumerSecret: '...', passkey: '...',
    shortcode: '174379', environment: DarajaEnvironment.sandbox,
    appwriteEndpoint: 'https://<region>.cloud.appwrite.io/v1',
    appwriteProjectId: '...', appwriteDatabaseId: 'payments',
    appwriteCollectionId: 'transactions',
    callbackDomain: 'https://<your-fn>.appwrite.run',
  ),
);

Pick a mode #

Appwrite enforces read permissions on Realtime and polling, so a document is only delivered to a client allowed to read it. That forces one choice:

  • PublicDaraja(config: ...), no userId. Documents are read("any"), keyed on the unguessable transaction id. Zero auth, fine for prototypes.
  • AuthenticatedDaraja(config: ..., appwriteClient: yourSignedInClient) plus a userId. Each user reads only their own payments. Requires an Appwrite session before you initiate.

Passing a userId without an authenticated client throws — that combination silently never resolves, so daraja rejects it up front.

Two things that aren't obvious #

PaymentTimeout is not PaymentFailed. Failed means Safaricom confirmed the money did not move. Timeout means 90s elapsed with no callback — the money may have moved, the receipt may exist. Show "status unknown" and a support path; never "payment failed," or customers pay twice.

Phone numbers are masked. Safaricom masks PhoneNumber in callbacks (0722000***), so daraja never reads it. Reconcile on receiptNumber (the M-Pesa receipt) and the userId you passed in — not the phone number.

PaymentFailed carries resultCode plus isInsufficientFunds / isWrongPin / isSubscriberLocked for the cases you'll actually branch on.

B2C disbursements #

Same lifecycle, stream, and timeout semantics via b2cPush(). Two differences: it needs a SecurityCredential (your initiator password RSA-encrypted with Safaricom's certificate, via SecurityCredential.generate) and a second collection. See ARCHITECTURE.md for the wiring and why B2C keys on Safaricom's OriginatorConversationID.

More #

  • ARCHITECTURE.md — why it's built this way: the trade-offs, the race window, the failure modes.
  • function/README.md — the deploy runbook.
  • flutter test runs the unit suite; test/integration/ holds the live tests (Realtime delivery, full lifecycle) gated behind --dart-define credentials.
0
likes
150
points
188
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

M-Pesa STK Push and B2C disbursements for Flutter — initiation, real-time callback delivery, timeout handling, and killed-app recovery. Backed by Appwrite.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

appwrite, clock, encrypt, flutter, flutter_riverpod, http, meta, pointycastle, shared_preferences

More

Packages that depend on daraja