mpesa

package
v1.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: AGPL-3.0 Imports: 25 Imported by: 0

Documentation

Overview

Package mpesa is a client for Safaricom's Daraja API gateway.

The package is a client library and nothing more. It holds no database, no queue and no notification path, so several of the safety rules the platform depends on cannot be enforced here. They are the caller's, and they are listed below because getting them wrong moves real money.

Callbacks are not evidence

Daraja signs nothing. A callback carries no HMAC, no shared secret and no mutual TLS, so anyone who learns a callback URL can post a well-formed payment notification to it. Never credit anything on the strength of a callback alone: confirm it independently with ExpressQuery, PullTransactions or TransactionStatus first.

A queue timeout is not a failure

Every Initiator call takes both a ResultURL and a QueueTimeOutURL. A delivery to the timeout URL means Daraja did not finish processing in time. It does not mean the transaction failed, and it says nothing about whether the transaction later completed. Treat it as unknown and resolve it with TransactionStatus. Retrying on a timeout is how money moves twice.

The two deliveries cannot be told apart by their payload; a timeout body names Safaricom's own internal listener, not ours — so ParseResult takes the kind as an argument. Route each URL to a distinct handler and pass the kind that URL was registered as.

Reversal has preconditions this package cannot check

Reverse moves money out and is irreversible. Before calling it the caller must establish that the transaction was observed from Safaricom rather than asserted by a callback, that it credited no loan, that no reversal is already in flight for it, and that the amount matches exactly. None of that is visible from here.

Verification fails closed

ValidateMobileNumber reports success only on the documented success code. Any unrecognised response is not-verified. A verifier that answers "verified" to a code it does not understand is worse than no verifier.

Certificates

The embedded Safaricom certificates are expired — sandbox since 2016, production since 2018 — and that is expected. Daraja publishes them as a carrier for an RSA public key, not as a trust anchor, and Safaricom has never rotated them. Nothing here validates a chain or an expiry date.

The wire shapes and result-code tables were read from Safaricom's published documentation. The design owes a debt to github.com/jwambugu/mpesa-golang-sdk, which is a useful reference implementation and not a dependency; the places where this package deliberately departs from it are recorded in the vault.

Index

Constants

View Source
const (
	HakikishaFound    = "0"
	HakikishaNotFound = "1"
)

Hakikisha response codes.

Variables

This section is empty.

Functions

func AssertCallbackURL

func AssertCallbackURL(value string) error

AssertCallbackURL rejects a URL Daraja will not accept, at the point it can still be changed rather than at a one-time registration call.

func ExpressRejection

func ExpressRejection(resp *ExpressResponse) error

ExpressRejection classifies an in-band express decline (HTTP 200, ResponseCode non-zero) that never reaches the transport classifier.

func NormalizeMSISDN

func NormalizeMSISDN(value string) (string, error)

NormalizeMSISDN renders a Kenyan number in the 2547XXXXXXXX form Daraja requires: twelve digits, no plus, no separators.

A national-format number cannot be resolved without country context, so pkg/phone yields nothing for it and this treats it as Kenyan, which is the only country this rail serves.

func ParseTimestamp

func ParseTimestamp(value string) (time.Time, bool)

ParseTimestamp decodes any of the timestamp renderings Daraja uses.

Types

type APIError

type APIError struct {
	RequestID    string `json:"requestId"`
	ErrorCode    string `json:"errorCode"`
	ErrorMessage string `json:"errorMessage"`
}

APIError is Daraja's synchronous error body.

type AccountBalance

type AccountBalance struct {
	Name      string
	Currency  string
	Available int64
	Uncleared int64
	Reserved  int64
}

AccountBalance is one account out of a Daraja balance string.

func ParseBalances

func ParseBalances(value string) []AccountBalance

ParseBalances decodes Daraja's balance encoding, which is not JSON:

Working Account|KES|346568.83|6186.83|340382.00|0.00
Working Account|KES|700000.00|0.00|0.00|0.00&Utility Account|KES|228037.00|...

Accounts are separated by &, fields by |. Parsing is deliberately lenient: this arrives after the money has already moved, so a field Safaricom changes must not be able to fail a transaction.

type AccountBalanceRequest

type AccountBalanceRequest struct {
	// PartyA defaults to the configured collection shortcode.
	PartyA         uint
	IdentifierType PartyIdentifierType
	Remarks        string
	URLs           AsyncURLs
}

AccountBalanceRequest asks for a shortcode's account balances.

It is also the cheapest end-to-end check of the Initiator and SecurityCredential path: if this works, the credentials Reversal and Transaction Status depend on are correct. Running it before trusting a reversal is worth the call.

type AccountResolver

type AccountResolver interface {
	ResolveAccount(accountNumber string) (accountName string, found bool, err error)
}

AccountResolver answers Safaricom's question.

Implementations must be fast: this sits in front of a customer holding a handset, and it must not consult anything slower than one indexed lookup. They must also never place a person's name in AccountName; see HakikishaResponse.

type AsyncAck

type AsyncAck struct {
	OriginatorConversationID string `json:"OriginatorConversationID"`
	ConversationID           string `json:"ConversationID"`
	ResponseCode             string `json:"ResponseCode"`
	ResponseDescription      string `json:"ResponseDescription"`
}

AsyncAck acknowledges that Daraja accepted an asynchronous request. It says nothing about the outcome, which arrives at the ResultURL.

func (AsyncAck) Accepted

func (a AsyncAck) Accepted() bool

Accepted reports whether Daraja took the request.

type AsyncOutcome

type AsyncOutcome struct {
	Kind OutcomeKind

	// Retryable reports whether the identical request may succeed if resent.
	// For a request with a client-supplied idempotency key this means reusing
	// the same key, never minting a fresh one.
	Retryable bool

	// Message is a short operator-facing summary.
	Message string
}

AsyncOutcome is what an asynchronous result code means to an operator. It is the operator-side analogue of ExpressOutcome: enough context to act, without reading Safaricom's prose.

func AsyncOutcomeFor

func AsyncOutcomeFor(family ResultFamily, code string) AsyncOutcome

AsyncOutcomeFor classifies an asynchronous result code for a result family.

The family-specific map is checked first, then the shared initiator set. An undocumented code falls back to operational and non-retryable, so an unknown failure is never retried blindly.

type AsyncURLs

type AsyncURLs struct {
	ResultURL       string
	QueueTimeOutURL string
}

AsyncURLs are the two callbacks every Initiator-bearing endpoint requires.

They must be distinct routes. A queue timeout and a result carry the same envelope and cannot be told apart by their body, so the only thing that distinguishes them is which URL received the post.

type B2BRequest

type B2BRequest struct {
	// PartyA defaults to the configured collection shortcode — the float this
	// package's collections land in is the one every envelope-B command here
	// is expected to move money out of.
	PartyA uint

	// PartyB is the receiving paybill or till. Ignored by PayTaxToKRA (always
	// 572572) and by BusinessPayToBulk when zero (defaults to the configured
	// disbursement shortcode).
	PartyB uint

	AmountKES int64

	// AccountReference is at most 13 characters — one more than STK's 12, per
	// Daraja's own example (ACC#03929/4yu).
	AccountReference string

	// Requester is the consumer's MSISDN this payment is made on behalf of.
	// Optional; populate it whenever the B2B payment exists because of a
	// specific borrower, e.g. paying a supplier directly for purpose-bound
	// lending.
	Requester string

	Remarks string

	URLs AsyncURLs
}

B2BRequest pays a shortcode from another shortcode's MMF/Working account.

type B2CRequest

type B2CRequest struct {
	// Command defaults to CommandBusinessPayment.
	Command PayoutCommand

	// OriginatorConversationID is caller-supplied and is B2C's native
	// idempotency key — Daraja rejects reuse with 500.002.1001, which means
	// "this request already reached us", never a reason to retry with a
	// fresh ID. See §7 of the plan.
	OriginatorConversationID string

	// PartyA defaults to the configured disbursement shortcode.
	PartyA uint

	// PartyB is the recipient MSISDN: 12 digits, no '+'.
	PartyB string

	AmountKES int64

	// Remarks is 2-100 characters.
	Remarks string

	// Occasion is 1-100 characters when set; optional.
	Occasion string

	URLs AsyncURLs
}

B2CRequest pays an MSISDN.

type C2BNotification

type C2BNotification struct {
	TransactionType   string
	TransID           string
	TransTime         string
	TransAmountMinor  int64
	BusinessShortCode string

	// BillRefNumber is the account number the payer typed, and on a shared
	// paybill it is the only thing binding a payment to a loan. It is null for
	// till payments.
	BillRefNumber string

	InvoiceNumber string

	// OrgAccountBalanceMinor is blank on validation and the post-payment
	// balance on confirmation.
	OrgAccountBalanceMinor int64

	// ThirdPartyTransID is ours to set: a validation response may return one,
	// and Daraja echoes it on the matching confirmation.
	ThirdPartyTransID string

	// MSISDN is masked on C2B v2. It cannot be used as a phone number; the
	// unmasked value comes from Pull Transaction.
	MSISDN MaskedMSISDN

	FirstName  string
	MiddleName string
	LastName   string
}

C2BNotification is a validation or confirmation payload.

It is not evidence. Daraja signs nothing, so a well-formed notification proves only that something posted to a URL. Confirm independently before it moves anything.

func ParseC2BNotification

func ParseC2BNotification(raw []byte) (*C2BNotification, error)

ParseC2BNotification decodes a validation or confirmation payload.

func (C2BNotification) PaidAt

func (n C2BNotification) PaidAt() (parsed string, ok bool)

PaidAt decodes TransTime.

type C2BNotificationWire

type C2BNotificationWire struct {
	// TransactionType is Safaricom's transaction type, e.g. "Pay Bill" or "Buy Goods".
	TransactionType string `json:"TransactionType" example:"Pay Bill"`
	// TransID is Safaricom's unique receipt number for this payment.
	TransID string `json:"TransID" example:"OEI2AK4Q16"`
	// TransTime is the payment timestamp as YYYYMMDDHHmmss.
	TransTime string `json:"TransTime" example:"20260911120000"`
	// TransAmount is the payment amount in KES, as a decimal string.
	TransAmount string `json:"TransAmount" example:"500.00"`
	// BusinessShortCode is the paybill/till the payment was made to.
	BusinessShortCode string `json:"BusinessShortCode" example:"174379"`
	// BillRefNumber is the account reference the payer typed, exactly as entered.
	BillRefNumber string `json:"BillRefNumber" example:"MV7K3QA9"`
	// InvoiceNumber is an optional invoice reference; usually empty for C2B.
	InvoiceNumber string `json:"InvoiceNumber"`
	// OrgAccountBalance is the shortcode's balance after this transaction, as a decimal string.
	OrgAccountBalance string `json:"OrgAccountBalance" example:"150000.00"`
	// ThirdPartyTransID is echoed back from our validation response, present only on the confirmation callback.
	ThirdPartyTransID string `json:"ThirdPartyTransID"`
	// MSISDN is the payer's phone number.
	MSISDN string `json:"MSISDN" example:"254712345678"`
	// FirstName is the payer's first name as held by Safaricom.
	FirstName string `json:"FirstName"`
	// MiddleName is the payer's middle name as held by Safaricom.
	MiddleName string `json:"MiddleName"`
	// LastName is the payer's last name as held by Safaricom.
	LastName string `json:"LastName"`
}

C2BNotificationWire is the wire shape of a C2B validation or confirmation payload. Exported so the receiving routes can name it in their OpenAPI definitions.

type C2BPayload

type C2BPayload struct {
	// TransID is the M-Pesa receipt and the natural idempotency key: a unique
	// index on it makes a duplicate confirmation a no-op rather than a second
	// credit.
	TransID string

	// BillRefNumber is the reference the payer typed. On a shared paybill it
	// is the only thing binding the payment to a loan.
	BillRefNumber string

	// MaskedMSISDN is all a confirmation discloses. The unmasked number comes
	// from Pull Transaction.
	MaskedMSISDN MaskedMSISDN

	ThirdPartyTransID string
	AmountMinor       int64
	PaidAt            time.Time
}

C2BPayload is what a paybill collection produces.

func (C2BPayload) ProviderID

func (C2BPayload) ProviderID() cashin.ProviderID

type Callback

type Callback struct {
	Kind    CallbackKind
	Outcome Outcome
	Result  *Result
}

Callback is a decoded asynchronous delivery.

func ParseCallback

func ParseCallback(kind CallbackKind, raw []byte) (*Callback, error)

ParseCallback decodes a delivery and classifies it.

A delivery to the queue-timeout URL is always OutcomeUnknown, whatever its body says. Daraja's wording — the request "timed out before processing" — invites reading it as a failure, but it establishes only that Daraja did not finish in time; the transaction may well have completed afterwards. Resolve an unknown with TransactionStatus. Never retry from one, and in particular never retry with a fresh OriginatorConversationID, which is what defeats the only server-side duplicate protection Daraja offers.

type CallbackKind

type CallbackKind string

CallbackKind identifies which of the two URLs Daraja was given received a delivery.

It is a parameter rather than something inferred from the payload because the payload cannot tell you: a queue-timeout body carries a ReferenceItem naming Safaricom's own internal listener, not the URL it was posted to. Register the result and timeout URLs as distinct routes and pass the kind that route was registered as.

const (
	CallbackResult  CallbackKind = "result"
	CallbackTimeout CallbackKind = "timeout"
)

The two deliveries.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a Daraja API client.

func New

func New(cfg Config) (*Client, error)

New builds a Client. It validates configuration eagerly so a misconfiguration is a boot failure rather than a failed payment.

func (*Client) AccessToken

func (c *Client) AccessToken(ctx context.Context) (string, error)

AccessToken returns a cached token, minting one if none is live.

func (*Client) AccountBalance

func (c *Client) AccountBalance(ctx context.Context, req AccountBalanceRequest) (*AsyncAck, error)

AccountBalance asks Daraja for the shortcode balances. The figures arrive at the result URL as a pipe-delimited string; see ParseBalances.

func (*Client) B2C

func (c *Client) B2C(ctx context.Context, req B2CRequest) (*AsyncAck, error)

B2C pays a registered M-PESA customer from the disbursement shortcode.

func (*Client) B2Pochi

func (c *Client) B2Pochi(ctx context.Context, req B2CRequest) (*AsyncAck, error)

B2Pochi pays a business's Pochi la Biashara wallet. Envelope A despite living under Daraja's B2B navigation — see §3 of the plan — because it pays an MSISDN, not a shortcode.

func (*Client) BusinessBuyGoods

func (c *Client) BusinessBuyGoods(ctx context.Context, req B2BRequest) (*AsyncAck, error)

BusinessBuyGoods pays a till.

func (*Client) BusinessPayBill

func (c *Client) BusinessPayBill(ctx context.Context, req B2BRequest) (*AsyncAck, error)

BusinessPayBill pays a paybill.

func (*Client) BusinessPayToBulk

func (c *Client) BusinessPayToBulk(ctx context.Context, req B2BRequest) (*AsyncAck, error)

BusinessPayToBulk moves float from the collection shortcode to the disbursement shortcode — the bridge that makes the closed loop of §9 possible (collections fund disbursements without a manual sweep). This is the one envelope-B command where both parties are ours, which makes it the safest to exercise first: a double-send is recoverable because the money never leaves our own accounts.

func (*Client) CollectionShortcode

func (c *Client) CollectionShortcode() uint

CollectionShortcode reports the configured collection shortcode.

func (*Client) DisbursementShortcode

func (c *Client) DisbursementShortcode() uint

DisbursementShortcode reports the configured disbursement shortcode.

func (*Client) DynamicQR

func (c *Client) DynamicQR(ctx context.Context, req QRRequest) (*QRResponse, error)

DynamicQR generates a scannable payment code.

func (*Client) Environment

func (c *Client) Environment() Environment

Environment reports which deployment the client talks to.

func (*Client) Express

func (c *Client) Express(ctx context.Context, req ExpressRequest) (*ExpressResponse, error)

Express pushes a payment prompt to the payer's handset.

func (*Client) ExpressQuery

func (c *Client) ExpressQuery(ctx context.Context, checkoutRequestID string, shortcode uint) (*ExpressQueryResponse, error)

ExpressQuery asks Daraja for the state of a checkout.

Daraja is widely reported to error rather than report "pending" while a prompt is still on the handset, and documents neither behaviour. Callers must treat an error as "not yet resolved" and retry, not as a terminal failure.

func (*Client) PayTaxToKRA

func (c *Client) PayTaxToKRA(ctx context.Context, req B2BRequest) (*AsyncAck, error)

PayTaxToKRA remits to KRA. PartyB is fixed at 572572 and is not a parameter — a caller cannot express a different destination. AccountReference should be the KRA-issued Payment Registration Number, not a loan reference.

func (*Client) PullAll

func (c *Client) PullAll(ctx context.Context, from, to time.Time, shortcode uint) ([]PulledTransaction, error)

PullAll walks every page in a window.

The offset must advance or the walk would loop forever on a Daraja that ignores it, so a non-advancing page fails loudly rather than hanging.

func (*Client) PullRegister

func (c *Client) PullRegister(ctx context.Context, req PullRegisterRequest) (*PullRegisterResponse, error)

PullRegister binds a shortcode to the Pull API.

func (*Client) PullTransactions

func (c *Client) PullTransactions(ctx context.Context, from, to time.Time, offset int, shortcode uint) ([]PulledTransaction, error)

PullTransactions fetches settled payments in a window.

An empty window is reported as response code 1001 with a Response body of the string "[[]]" rather than an empty array. That is a success — no payments happened — and treating it as an error makes every quiet hour look like an outage.

func (*Client) QueryOrgInfo

func (c *Client) QueryOrgInfo(ctx context.Context, req OrgInfoRequest) (*OrgInfoResponse, error)

QueryOrgInfo resolves a shortcode to its trading name — the guard the plan recommends in front of every B2B disbursement, since paying the wrong paybill is not reversible the way an over-collection is. Synchronous, bearer-only: no Initiator, no SecurityCredential, no callback, and (unlike every other Initiator-bearing call in this package) nothing here can lock the API operator's password.

func (*Client) RegisterURL

func (c *Client) RegisterURL(ctx context.Context, req RegisterURLRequest) (*RegisterURLResponse, error)

RegisterURL binds callback URLs to a shortcode.

In production this is effectively one-time: re-registering requires Safaricom to delete the existing URLs first. Treat it as a deliberate operator action, not something a service does at boot.

func (*Client) Reverse

func (c *Client) Reverse(ctx context.Context, req ReversalRequest) (*AsyncAck, error)

Reverse returns a payment to its payer.

CommandID, RecieverIdentifierType and SecurityCredential are set here rather than taken from the caller, because each has exactly one correct value and none of them is guessable. Note Safaricom's misspelling of "Receiver" in the wire field, which must be reproduced exactly.

func (*Client) SecurityCredential

func (c *Client) SecurityCredential() (string, error)

SecurityCredential encrypts the initiator password with Safaricom's public key, as every Initiator-bearing endpoint requires.

The padding is PKCS #1 v1.5, which the standard library deprecates as dangerous. It is not a choice: Safaricom defines the SecurityCredential as a v1.5 ciphertext, and a credential produced with OAEP is one Daraja cannot decrypt — it comes back as result code 2001, indistinguishable from a wrong password. Nothing changes here until Safaricom changes the protocol.

The exposure is narrower than the deprecation implies. The attack it warns about is a Bleichenbacher padding oracle, which is a risk to the party performing the decryption; we only ever encrypt, with a public key, and the plaintext is a credential the holder already knows.

The certificate is a carrier for a public key and not a trust anchor: Safaricom's published certificates expired years ago and have never been rotated, so nothing here checks a chain or an expiry date.

func (*Client) Simulate

func (c *Client) Simulate(ctx context.Context, req SimulateRequest) (*SimulateResponse, error)

Simulate triggers a C2B payment in sandbox.

func (*Client) TransactionStatus

func (c *Client) TransactionStatus(ctx context.Context, req TransactionStatusRequest) (*AsyncAck, error)

TransactionStatus asks Daraja to resolve a transaction. The answer arrives at the result URL, not here.

func (*Client) ValidateMobileNumber

func (c *Client) ValidateMobileNumber(ctx context.Context, msisdn string, idType IDType, idNumber string, shortcode uint) (*NumberValidation, error)

ValidateMobileNumber checks that an MSISDN is registered under an ID number.

It fails closed. Only the documented match code counts as a match; every other response, documented or not, is a non-match. A verifier that answers "verified" to a code it does not understand is worse than no verifier, because it is trusted.

Call this out of band. It is a paid synchronous round trip to a third party and the C2B validation window is eight seconds, so blocking a payment on it means one slow response stalls every concurrent payment.

type Config

type Config struct {
	Environment Environment

	ConsumerKey    string
	ConsumerSecret string

	// CollectionShortcode receives C2B and M-Pesa Express payments.
	CollectionShortcode uint

	// DisbursementShortcode funds payouts and is the PartyA of balance and
	// reversal queries against the payout side.
	DisbursementShortcode uint

	// Passkey signs the M-Pesa Express password. Issued per shortcode.
	Passkey string

	// InitiatorName is the M-PESA API operator username. InitiatorPassword is
	// its plaintext password, encrypted per call into a SecurityCredential.
	InitiatorName     string
	InitiatorPassword string

	// BaseURL overrides Environment.BaseURL. Tests point it at a stub.
	BaseURL string

	// Certificate overrides the embedded Safaricom certificate. Tests supply
	// one whose private half they hold so a SecurityCredential can be decrypted
	// and checked rather than merely inspected.
	Certificate []byte

	// HttpClient defaults to a client with a 30 second timeout.
	HttpClient HttpClient

	// TokenStore defaults to an in-process store. Deployments with more than
	// one replica should supply a shared one: Daraja invalidates the previous
	// access token on every mint, so per-process caches evict each other.
	TokenStore TokenStore

	// Clock defaults to time.Now. Overridable so timestamp derivation and
	// token expiry are testable.
	Clock func() time.Time
}

Config carries everything a Client needs.

Shortcodes are split because collections and disbursement sit on separate M-Pesa shortcodes in the general case. Setting both to the same value is valid when one shortcode carries both product sets.

type DarajaError

type DarajaError struct {
	StatusCode int
	RequestID  string
	Code       string
	Message    string
}

DarajaError is a synchronous rejection. It is wrapped by the oops error the package returns, so callers can reach it with errors.As to inspect the exact Daraja code rather than the coarse pkgErrors code.

func (*DarajaError) Error

func (e *DarajaError) Error() string

type Environment

type Environment string

Environment selects the Daraja deployment a Client talks to.

const (
	EnvironmentSandbox    Environment = "sandbox"
	EnvironmentProduction Environment = "production"
)

The two Daraja deployments.

func (Environment) BaseURL

func (e Environment) BaseURL() string

BaseURL is the Daraja host for the environment.

func (Environment) IsProduction

func (e Environment) IsProduction() bool

IsProduction reports whether e is the live environment.

func (Environment) Valid

func (e Environment) Valid() bool

Valid reports whether e is a known environment.

type ExpressCallback

type ExpressCallback struct {
	MerchantRequestID string
	CheckoutRequestID string
	ResultCode        int64
	ResultDesc        string

	// The following are present only on success; CallbackMetadata is absent
	// entirely when the customer did not pay.
	AmountKES     int64
	AmountMinor   int64
	ReceiptNumber string
	Payer         string
	CompletedAt   time.Time
}

ExpressCallback is the decoded stkCallback body.

Note the items use Name rather than the Key used by every Result envelope, so this does not share a decoder with resultparams.go.

func ParseExpressCallback

func ParseExpressCallback(raw []byte) (*ExpressCallback, error)

ParseExpressCallback decodes an M-Pesa Express callback.

func (ExpressCallback) Succeeded

func (e ExpressCallback) Succeeded() bool

Succeeded reports whether the customer paid.

type ExpressCallbackBody

type ExpressCallbackBody struct {
	// STKCallback is the actual result payload for one STK push.
	STKCallback ExpressCallbackResult `json:"stkCallback"`
}

ExpressCallbackBody wraps the stkCallback object.

type ExpressCallbackEnvelope

type ExpressCallbackEnvelope struct {
	// Body wraps the stkCallback object; Safaricom always nests it one level deep.
	Body ExpressCallbackBody `json:"Body"`
}

ExpressCallbackEnvelope is the wire shape of an M-Pesa Express result delivery. Exported so the receiving route can name it in its OpenAPI definition.

type ExpressCallbackMetadataHolder

type ExpressCallbackMetadataHolder struct {
	// Item is the list of name/value receipt fields (Amount, MpesaReceiptNumber, TransactionDate, PhoneNumber, ...).
	Item []ExpressCallbackMetadataItem `json:"Item"`
}

ExpressCallbackMetadataHolder carries the receipt entries on success.

type ExpressCallbackMetadataItem

type ExpressCallbackMetadataItem struct {
	// Name identifies which receipt field this is, e.g. "Amount", "MpesaReceiptNumber".
	Name string `json:"Name" example:"MpesaReceiptNumber"`
	// Value is the field's raw JSON value; its type (string vs number) depends on Name.
	Value json.RawMessage `json:"Value"`
}

ExpressCallbackMetadataItem is one CallbackMetadata entry. Value is held raw because its JSON type depends on Name.

type ExpressCallbackResult

type ExpressCallbackResult struct {
	// MerchantRequestID is the ID Safaricom assigned when the STK push was initiated.
	MerchantRequestID string `json:"MerchantRequestID" example:"29115-34620561-1"`
	// CheckoutRequestID is the ID this codebase used to originate the push and correlate the result.
	CheckoutRequestID string `json:"CheckoutRequestID" example:"ws_CO_191220191020363925"`
	// ResultCode is 0 on success; any other value maps through ExpressOutcomeFor to decide retryability.
	ResultCode FlexibleInt64 `json:"ResultCode" example:"0"`
	// ResultDesc is Safaricom's human-readable outcome description.
	ResultDesc string `json:"ResultDesc" example:"The service request is processed successfully."`
	// CallbackMetadata carries the payment receipt details; nil when the customer did not pay.
	CallbackMetadata *ExpressCallbackMetadataHolder `json:"CallbackMetadata"`
}

ExpressCallbackResult is the outcome of one STK push as it arrives on the wire. CallbackMetadata is absent unless the customer paid.

type ExpressOutcome

type ExpressOutcome struct {
	// Retryable reports whether trying again could succeed without anything
	// else changing.
	Retryable bool

	// Operational reports whether this is our problem rather than the payer's.
	// An operational failure must never be surfaced to a borrower as if they
	// did something wrong.
	Operational bool

	// Message is safe to show a borrower. GSM 03.38 characters only, because
	// it may be rendered on a feature phone over USSD or SMS.
	Message string
}

ExpressOutcome is what a result code means to the borrower.

func ExpressOutcomeFor

func ExpressOutcomeFor(resultCode int64) ExpressOutcome

ExpressOutcomeFor reports what a result code means. An undocumented code — including the sentinel -1 used for an unparseable or unrecognised one — is treated as operational, so an unknown failure is never blamed on the borrower's PIN or balance.

It is also given the same bounded retry budget as a documented transient code (Retryable: true — see MpesaSTKLoanDriver.retryOrExpire's STKMaxAttempts, not unlimited retries here), rather than the zero chances an unconditional false would give it. "Undocumented" is not evidence of "permanent": Daraja's own documented codes list is known incomplete (the community scrape backing this table doesn't cover the login-gated pages — see the daraja-docs-mirror vault doc), and a code observed against sandbox (e.g. 4999, see yellowcard-offramp-webhook-race-2026-09-10.md §3) may simply be one Safaricom hasn't published here yet, not a hard rejection. The cost of retrying a genuinely permanent unknown code a few extra times is small and bounded; the cost of giving a transient one zero chances is a borrower whose STK repayment expires for no real reason.

type ExpressPayload

type ExpressPayload struct {
	MerchantRequestID string
	CheckoutRequestID string
	CustomerMessage   string
	PromptedAmountKES int64
}

ExpressPayload is what an M-Pesa Express collection returns.

A prompt that was accepted is not a payment. CheckoutRequestID identifies something that may still be cancelled, time out, or be declined for a wrong PIN, and it is the handle for resolving which.

func (ExpressPayload) ProviderID

func (ExpressPayload) ProviderID() cashin.ProviderID

type ExpressQueryResponse

type ExpressQueryResponse struct {
	ResponseCode        string `json:"ResponseCode"`
	ResponseDescription string `json:"ResponseDescription"`
	MerchantRequestID   string `json:"MerchantRequestID"`
	CheckoutRequestID   string `json:"CheckoutRequestID"`

	// ResultCode is held raw because Daraja sends it as a number, a quoted
	// number, or an alphanumeric code. SFC_IC0003 ("the operator does not
	// exist") is the documented case, and collapsing it into an integer would
	// make an unmappable failure read as success 0.
	ResultCode json.RawMessage `json:"ResultCode"`

	ResultDesc string `json:"ResultDesc"`
}

ExpressQueryResponse reports the state of a checkout.

func (ExpressQueryResponse) Outcome

func (q ExpressQueryResponse) Outcome() (int64, ExpressOutcome)

Outcome resolves the raw result code to a code and a borrower-facing outcome. Numeric codes come from the table; SFC_IC0003 is folded onto 2028, which Safaricom pairs it with. An unrecognised value yields the operational, non-retryable fallback rather than a mistaken success.

type ExpressRequest

type ExpressRequest struct {
	// Shortcode defaults to the configured collection shortcode.
	Shortcode uint

	TransactionType TransactionType

	// AmountKES is whole shillings. M-Pesa Express does not accept cents.
	AmountKES int64

	// Payer receives the prompt and is debited. Accepted in any Kenyan format
	// and normalised to 2547XXXXXXXX.
	Payer string

	// CallbackURL receives the result. It must not contain a word Daraja
	// blocks; see AssertCallbackURL.
	CallbackURL string

	// AccountReference is shown to the customer in the prompt. Twelve
	// characters at most.
	AccountReference string

	// TransactionDesc is optional. Thirteen characters at most.
	TransactionDesc string
}

ExpressRequest asks Daraja to push a payment prompt to a handset.

type ExpressResponse

type ExpressResponse struct {
	MerchantRequestID   string `json:"MerchantRequestID"`
	CheckoutRequestID   string `json:"CheckoutRequestID"`
	ResponseCode        string `json:"ResponseCode"`
	ResponseDescription string `json:"ResponseDescription"`
	CustomerMessage     string `json:"CustomerMessage"`

	// Set on some in-band business failures that bypass the HTTP classifier.
	ErrorCode    string `json:"errorCode,omitempty"`
	ErrorMessage string `json:"errorMessage,omitempty"`
}

ExpressResponse acknowledges that the prompt was accepted for processing. It does not mean the customer paid — that arrives on the callback.

func (ExpressResponse) Accepted

func (r ExpressResponse) Accepted() bool

Accepted reports whether Daraja took the prompt for delivery. An accepted prompt is not a payment — only the callback or a query can say which.

type FlexibleInt64

type FlexibleInt64 int64

FlexibleInt64 decodes a JSON number or a quoted number.

Daraja is inconsistent about which it sends, and the inconsistency follows success and failure rather than the endpoint: ResultCode arrives as a number on a successful reversal and as a string on a failed one. A plain int64 field therefore fails to unmarshal exactly when something has already gone wrong.

A value that is neither a number nor a quoted number is an error, not a zero. Some Daraja result codes are strings — SFC_IC0003 is "the operator does not exist" — and defaulting an unparseable value to 0 would read a failure as a success wherever ResultCode is compared to zero. Returning the error makes a string-coded result fail loudly at parse instead.

func (FlexibleInt64) Int64

func (f FlexibleInt64) Int64() int64

Int64 is the decoded value.

func (*FlexibleInt64) UnmarshalJSON

func (f *FlexibleInt64) UnmarshalJSON(raw []byte) error

UnmarshalJSON accepts 0, "0", "" and null.

type HakikishaRequest

type HakikishaRequest struct {
	// AccountNumber is the reference the payer typed on their handset, so it
	// arrives exactly as they typed it.
	AccountNumber string `json:"accountNumber" example:"MV7K3QA9"`

	// ShortCode is the M-Pesa paybill/till the payer is sending to.
	ShortCode string `json:"shortCode" example:"174379"`

	// Timestamp arrives as either a string or a number.
	Timestamp FlexibleInt64 `json:"timestamp" example:"20260911120000"`

	// TransactionID is Safaricom's identifier for this validation request.
	TransactionID string `json:"transactionId" example:"OEI2AK4Q16"`
}

HakikishaRequest is what Safaricom asks us.

func ParseHakikishaRequest

func ParseHakikishaRequest(raw []byte) (*HakikishaRequest, error)

ParseHakikishaRequest decodes an inbound request.

type HakikishaResponse

type HakikishaResponse struct {
	// AccountName is shown to the payer on their handset; must identify the obligation, never the borrower. Empty when not found.
	AccountName string `json:"accountName" example:"Microvault Loan MV7K3QA9"`
	// AccountNumber echoes back the reference that was resolved.
	AccountNumber string `json:"accountNumber" example:"MV7K3QA9"`
	// ResponseCode is HakikishaFound ("0") or HakikishaNotFound ("1").
	ResponseCode string `json:"responseCode" example:"0"`
	// ResponseDesc is a short human-readable status, e.g. "Success" or "Account not found".
	ResponseDesc string `json:"responseDesc" example:"Success"`
}

HakikishaResponse is what we answer.

AccountName is shown to whichever M-Pesa customer supplied the account number, which makes this endpoint a name-disclosure oracle for anyone who can guess a reference. It must not carry a borrower's name. Something that identifies the obligation without identifying the person — "Microvault Loan MV7K3QA9" — tells the payer what they need and nobody else anything.

func AccountFound

func AccountFound(accountNumber, accountName string) HakikishaResponse

AccountFound builds an affirmative answer.

func AccountNotFound

func AccountNotFound(accountNumber string) HakikishaResponse

AccountNotFound builds a negative answer. It carries no account name, so an unknown reference discloses nothing at all.

type HttpClient

type HttpClient interface {
	Do(req *http.Request) (*http.Response, error)
}

HttpClient is the outbound seam. *http.Client satisfies it.

type IDType

type IDType string

IDType selects the identity document a number is checked against.

const (
	IDTypeNational IDType = "01"
	IDTypeMilitary IDType = "02"
	IDTypePassport IDType = "05"
)

The document types Safaricom accepts.

type MaskedMSISDN

type MaskedMSISDN string

MaskedMSISDN is a phone number as Daraja discloses it on a C2B confirmation: "2547 ***** 126". It is a distinct type so it can never be passed where a real number is expected — the masking is not reversible, and a masked value silently used as an MSISDN would attribute payments to a number that does not exist.

func (MaskedMSISDN) Masked

func (m MaskedMSISDN) Masked() bool

Masked reports whether the value actually carries mask characters. A C2B confirmation is documented as masked, so a value without them is worth noticing rather than trusting.

func (MaskedMSISDN) String

func (m MaskedMSISDN) String() string

String renders the masked value.

type MemoryTokenStore

type MemoryTokenStore struct {
	// contains filtered or unexported fields
}

MemoryTokenStore is the default in-process TokenStore. Correct for a single replica and for tests; see TokenStore for why a deployment with more than one replica should supply a shared implementation.

func NewMemoryTokenStore

func NewMemoryTokenStore() *MemoryTokenStore

NewMemoryTokenStore builds an empty in-process store.

func (*MemoryTokenStore) Delete

func (m *MemoryTokenStore) Delete(_ context.Context, key string) error

Delete evicts key.

func (*MemoryTokenStore) Get

Get returns the cached token for key.

func (*MemoryTokenStore) Set

func (m *MemoryTokenStore) Set(_ context.Context, key, token string, expiresAt time.Time) error

Set caches token under key.

type NumberValidation

type NumberValidation struct {
	ResponseRefID string
	ResponseCode  string
	Message       string

	// Matched is true only on the documented success code. Anything
	// unrecognised is false.
	Matched bool
}

NumberValidation is the verdict.

type Options

type Options struct {
	TransactionType TransactionType
	Shortcode       uint

	// TransactionDesc is shown in the prompt. Thirteen characters at most.
	TransactionDesc string
}

Options carries M-Pesa-specific extras for a collection request.

func (Options) ProviderID

func (Options) ProviderID() cashin.ProviderID

type OrgIdentifierType

type OrgIdentifierType string

OrgIdentifierType distinguishes a paybill from a till on this endpoint specifically. It is its own type rather than PartyIdentifierType because the two disagree on what "2" means: here it is a till, where IdentifierTillOwner ("2") means something else entirely on Transaction Status and Account Balance. Passing one where the other belongs would be a call that succeeds against the wrong semantics.

const (
	OrgIdentifierTill    OrgIdentifierType = "2"
	OrgIdentifierPaybill OrgIdentifierType = "4"
)

The two identifier types this endpoint accepts.

type OrgInfoRequest

type OrgInfoRequest struct {
	IdentifierType OrgIdentifierType
	// Identifier is the till or paybill number, as a string on the wire.
	Identifier string
}

OrgInfoRequest names the shortcode to resolve.

type OrgInfoResponse

type OrgInfoResponse struct {
	ConversationID        string `json:"ConversationID"`
	ResponseCode          string `json:"ResponseCode"`
	ResponseMessage       string `json:"ResponseMessage"`
	DetailedMessage       string `json:"DetailedMessage"`
	OrganizationShortCode string `json:"OrganizationShortCode"`
	OrganizationName      string `json:"OrganizationName"`
	// ChargeProfileID determines who bears the B2B transaction's cost. The
	// published profile-ID mapping is partial and the documentation's own
	// sample returns an ID absent from it — record it verbatim and map it
	// when known; never fail on an unrecognised value.
	ChargeProfileID string `json:"ChargeProfileID"`
}

OrgInfoResponse answers with the organisation's trading name and tariff.

func (OrgInfoResponse) Success

func (r OrgInfoResponse) Success() bool

Success reports whether Safaricom resolved the identifier. See orgInfoSuccess for why this checks "4000" and not "0".

type Outcome

type Outcome string

Outcome is what a delivery establishes about a transaction.

const (
	OutcomeSucceeded Outcome = "succeeded"
	OutcomeFailed    Outcome = "failed"
	OutcomeUnknown   Outcome = "unknown"
)

The three outcomes. There are three rather than two because "we do not know" is a real state and collapsing it into Failed is how money moves twice.

func (Outcome) Terminal

func (o Outcome) Terminal() bool

Terminal reports whether the outcome needs no further resolution.

type OutcomeKind

type OutcomeKind string

OutcomeKind is the class of an asynchronous result, so an operator can act on it without reading Safaricom's description.

const (
	// OutcomeSuccess is ResultCode "0" and nothing else.
	OutcomeSuccess OutcomeKind = "success"

	// OutcomeTransient is a failure that may succeed if retried after a wait —
	// throttling, overload, maintenance. It is never a reason to change the
	// request.
	OutcomeTransient OutcomeKind = "transient"

	// OutcomeConfig is a request we built wrong or sent to the wrong product —
	// a bad parameter, a missing field, a shortcode without the product. It
	// never succeeds by retrying unchanged.
	OutcomeConfig OutcomeKind = "config"

	// OutcomePermission is a missing initiator role or product assignment on
	// the M-PESA Org portal. It is granted out of band and never resolves by
	// retrying.
	OutcomePermission OutcomeKind = "permission"

	// OutcomeCredential is a wrong, unresolvable, or locked initiator
	// credential. It stops every Initiator-bearing endpoint at once.
	OutcomeCredential OutcomeKind = "credential"

	// OutcomeOperational is a real condition on our accounts — insufficient
	// float, an inactive shortcode, a transaction already reversed.
	OutcomeOperational OutcomeKind = "operational"
)

The outcome kinds.

type Parameters

type Parameters map[string][]string

Parameters is a decoded Daraja key/value list. It is a multi-map because Daraja repeats keys — an Account Balance result carries one entry per account under the same key.

func (Parameters) All

func (p Parameters) All(key string) []string

All returns every value for key, in arrival order.

func (Parameters) Balances

func (p Parameters) Balances(key string) ([]AccountBalance, bool)

Balances returns the first value for key parsed as Daraja's pipe-delimited balance encoding.

func (Parameters) Get

func (p Parameters) Get(key string) (string, bool)

Get returns the first value for key.

func (Parameters) Int

func (p Parameters) Int(key string) (int64, bool)

Int returns the first value for key as an integer.

func (Parameters) Minor

func (p Parameters) Minor(key string) (int64, bool)

Minor returns the first value for key as minor units — cents — so money is never held as a float.

func (Parameters) Time

func (p Parameters) Time(key string) (time.Time, bool)

Time returns the first value for key as a timestamp, trying each layout Daraja is known to use.

func (Parameters) WrappedAmount

func (p Parameters) WrappedAmount(key string) (WrappedAmount, bool)

WrappedAmount returns the first value for key parsed as the brace encoding.

type PartyIdentifierType

type PartyIdentifierType string

PartyIdentifierType identifies a party on Transaction Status and Account Balance.

const (
	IdentifierMSISDN    PartyIdentifierType = "1"
	IdentifierTillOwner PartyIdentifierType = "2"
	IdentifierShortcode PartyIdentifierType = "4"
)

The party identifier types.

type PayBillPayload

type PayBillPayload struct {
	Shortcode        uint
	AccountReference string
}

PayBillPayload is the instruction set for a passive paybill collection.

func (PayBillPayload) ProviderID

func (PayBillPayload) ProviderID() cashin.ProviderID

type PayoutCommand

type PayoutCommand string

PayoutCommand is an envelope-A command ID. Typed so a caller cannot pass a B2B command where a B2C one belongs.

const (
	CommandBusinessPayment  PayoutCommand = "BusinessPayment"
	CommandSalaryPayment    PayoutCommand = "SalaryPayment"
	CommandPromotionPayment PayoutCommand = "PromotionPayment"
)

The envelope-A commands. CommandBusinessPayment is the one this package recommends. SalaryPayment no longer reaches unregistered numbers despite its name — Safaricom's own table says so — and PromotionPayment's congratulatory SMS is the wrong tone for a loan; both are exposed because Daraja documents them, not because anything here sends them.

type PullRegisterRequest

type PullRegisterRequest struct {
	// Shortcode defaults to the configured collection shortcode.
	Shortcode uint

	// NominatedNumber is the number Safaricom associates with the
	// registration.
	NominatedNumber string

	CallbackURL string
}

PullRegisterRequest binds a shortcode to the Pull API.

type PullRegisterResponse

type PullRegisterResponse struct {
	ResponseRefID       string `json:"ResponseRefID"`
	ResponseStatus      string `json:"ResponseStatus"`
	ShortCode           string `json:"ShortCode"`
	ResponseDescription string `json:"ResponseDescription"`
}

PullRegisterResponse acknowledges a Pull registration.

func (PullRegisterResponse) AlreadyRegistered

func (r PullRegisterResponse) AlreadyRegistered() bool

AlreadyRegistered reports whether the shortcode was already bound, which is a success for our purposes rather than a failure.

type PulledTransaction

type PulledTransaction struct {
	TransactionID   string
	CompletedAt     time.Time
	MSISDN          string
	TransactionType string
	BillReference   string
	AmountMinor     int64
	Organization    string
}

PulledTransaction is one settled payment as Pull reports it.

Unlike a C2B confirmation, msisdn here is not masked. That makes Pull the only route to the payer's actual number, and therefore the compliance path rather than a reconciliation convenience.

type QRRequest

type QRRequest struct {
	MerchantName string
	ReferenceNo  string
	AmountKES    int64
	TrxCode      TrxCode

	// CreditPartyIdentifier is the paybill, till or number being paid.
	CreditPartyIdentifier string

	// Size is the image edge in pixels. Defaults to 300.
	Size string
}

QRRequest asks Daraja for a scannable code. The Go field names are readable; the wire names are Safaricom's.

type QRResponse

type QRResponse struct {
	ResponseCode        string `json:"ResponseCode"`
	RequestID           string `json:"RequestID"`
	ResponseDescription string `json:"ResponseDescription"`

	// QRCode is a base64-encoded PNG. It is returned as it arrived: a library
	// has no business writing files, deriving paths from data, or depending on
	// the process working directory.
	QRCode string `json:"QRCode"`
}

QRResponse carries the generated code.

func (QRResponse) DecodePNG

func (q QRResponse) DecodePNG() ([]byte, error)

DecodePNG decodes the returned image to bytes. It touches no filesystem; if a caller wants a file, a caller can write one.

type RawResult

type RawResult struct {
	// ResultType is part of Daraja's envelope; this package does not
	// interpret it, only ResultCode.
	ResultType FlexibleInt64 `json:"ResultType" example:"0"`
	// ResultCode is 0/"0" on success; non-zero (numeric or, for reversals, "R000001"/"R000002") on failure.
	ResultCode json.RawMessage `json:"ResultCode" example:"0"`
	// ResultDesc is Daraja's human-readable outcome description.
	ResultDesc string `json:"ResultDesc" example:"The service request has been accepted successfully."`
	// OriginatorConversationID is the ID this codebase generated when initiating the request.
	OriginatorConversationID string `json:"OriginatorConversationID" example:"29112-34801843-1"`
	// ConversationID is Daraja's own ID for this conversation.
	ConversationID string `json:"ConversationID" example:"AG_20260911_1234567890"`
	// TransactionID is Safaricom's receipt number for the underlying transaction, when one exists.
	TransactionID string `json:"TransactionID" example:"OEI2AK4Q16"`

	// ResultParameters holds the endpoint-specific key/value payload (e.g. balances, receipt details) as a raw array or object; shape depends on the endpoint, decoded by decodeParameters.
	ResultParameters *struct {
		ResultParameter json.RawMessage `json:"ResultParameter"`
	} `json:"ResultParameters"`

	// ReferenceData holds endpoint-specific reference items alongside ResultParameters; same raw array-or-object ambiguity.
	ReferenceData *struct {
		ReferenceItem json.RawMessage `json:"ReferenceItem"`
	} `json:"ReferenceData"`
}

RawResult is the Result object as it arrives on the wire. ResultCode is held raw because Daraja sends it as a number on some endpoints and a string on others.

type RegisterURLRequest

type RegisterURLRequest struct {
	// Shortcode defaults to the configured collection shortcode.
	Shortcode uint

	ResponseType ResponseType

	// ValidationURL is called only when external validation is enabled on the
	// shortcode, which it is not by default. Enabling it is a request to
	// Safaricom, not a setting.
	ValidationURL string

	ConfirmationURL string
}

RegisterURLRequest points a shortcode at our callback URLs.

type RegisterURLResponse

type RegisterURLResponse struct {
	OriginatorConversationID string `json:"OriginatorCoversationID"`
	ResponseCode             string `json:"ResponseCode"`
	ResponseDescription      string `json:"ResponseDescription"`
}

RegisterURLResponse acknowledges a registration.

type ResponseType

type ResponseType string

ResponseType is what M-Pesa does when our validation URL is unreachable.

const (
	ResponseTypeCompleted ResponseType = "Completed"
	ResponseTypeCancelled ResponseType = "Cancelled"
)

The two fallback behaviours.

Completed accepts payments we never saw and therefore cannot attribute. Cancelled refuses payments during any outage of ours. For a lender the second is the safer default — an unattributable payment is worse than a payment that did not happen — but it trades collection availability for reconciliation safety and is a business decision, not an engineering one.

Safaricom's own documentation spells the second value both "Cancelled" and "Canceled" on different pages while warning that it must be well-spelled. Registration is a one-time production call, so confirm the spelling with Safaricom before making it.

type Result

type Result struct {
	ResultType int64

	// ResultCode is the code as Daraja sent it, held as a string rather than an
	// integer because the namespace is not numeric. Reversal failure results
	// carry R000001 and R000002, and folding those into an integer would make
	// an unmappable failure read as success 0. Use ResultCodeInt for the
	// numeric cases and the per-family outcome lookups to classify it.
	ResultCode               string
	ResultDesc               string
	OriginatorConversationID string
	ConversationID           string
	TransactionID            string

	// Parameters holds ResultParameters.ResultParameter.
	Parameters Parameters

	// Reference holds ReferenceData.ReferenceItem.
	Reference Parameters
}

Result is the asynchronous envelope every Initiator-bearing endpoint posts to a ResultURL or a QueueTimeOutURL.

func ParseResult

func ParseResult(raw []byte) (*Result, error)

ParseResult decodes an asynchronous result envelope.

func (Result) Outcome

func (r Result) Outcome(family ResultFamily) AsyncOutcome

Outcome classifies this result for the given family.

func (Result) ResultCodeInt

func (r Result) ResultCodeInt() (int64, bool)

ResultCodeInt reports the result code as an integer when it is one.

func (Result) Succeeded

func (r Result) Succeeded() bool

Succeeded reports whether Daraja processed the request. "0" is the only success; every other code, numeric or not, means something else happened.

type ResultEnvelope

type ResultEnvelope struct {
	// Result is the outcome payload; always nested one level under "Result" by Daraja.
	Result RawResult `json:"Result"`
}

ResultEnvelope is the wire shape every Initiator-bearing endpoint posts to a ResultURL or QueueTimeOutURL. Exported so the receiving routes can name it in their OpenAPI definitions.

type ResultError

type ResultError struct {
	ResultCode               string
	ResultDesc               string
	ConversationID           string
	OriginatorConversationID string
	TransactionID            string
}

ResultError is an asynchronous result that reported failure. The result code namespace is per-API — 2001 is an invalid initiator on Reversal and a wrong customer PIN on M-Pesa Express — so each endpoint maps its own codes to outcomes and this type carries the raw values for the ones that do not.

func (*ResultError) Error

func (e *ResultError) Error() string

type ResultFamily

type ResultFamily string

ResultFamily identifies which API produced an asynchronous result. Result codes are a per-family namespace — 2001 is a credential failure on Reversal and a wrong customer PIN on M-Pesa Express — so classifying requires knowing which endpoint the result came from.

const (
	FamilyReversal ResultFamily = "reversal"
	FamilyStatus   ResultFamily = "transaction_status"
	FamilyBalance  ResultFamily = "account_balance"
)

The result families with package-level outcome maps.

type ReversalIdentifierType

type ReversalIdentifierType string

ReversalIdentifierType identifies the receiving party on a Reversal, where the only accepted value is 11 rather than the 4 used everywhere else.

const ReversalIdentifierShortcode ReversalIdentifierType = "11"

The reversal identifier type.

type ReversalPayload

type ReversalPayload struct {
	TransactionID         string
	OriginalTransactionID string
	AmountMinor           int64
	ChargeMinor           int64

	// CreditPartyPublicName arrives unmasked and with a full name. It belongs
	// in an audit record, never in a log line.
	CreditPartyPublicName string
}

ReversalPayload is what a reversal result carries.

func (ReversalPayload) ProviderID

func (ReversalPayload) ProviderID() cashin.ProviderID

type ReversalRequest

type ReversalRequest struct {
	// TransactionID is the M-Pesa receipt being reversed.
	TransactionID string

	// AmountKES must equal the amount originally received.
	AmountKES int64

	// ReceiverParty defaults to the configured collection shortcode: the
	// shortcode that received the money and will give it back.
	ReceiverParty uint

	Remarks  string
	Occasion string

	URLs AsyncURLs
}

ReversalRequest returns a received payment to the payer.

This is the most dangerous call in the package: it moves real money and cannot be undone. The package has no database and therefore cannot check the preconditions that make a reversal safe. Before calling this the caller must have established, in one place:

  1. The transaction was observed from Safaricom, not merely asserted by a callback. Daraja signs nothing, so an unconfirmed callback is not evidence that a payment exists.
  2. It resolves to no open loan, or to one outside our domain.
  3. It has not credited a loan. If it has, the correct action is a refund decision, not a reversal.
  4. No reversal is already in flight or complete for it, enforced by a unique index rather than a read.
  5. The amount matches the observed amount exactly.
  6. We hold the initiator role on the receiving shortcode.

Reversals should not be automatic. The failure modes of an automated reversal loop are unbounded and irreversible; the cost of a human in the loop is hours of delay on an already exceptional case.

type SimulateRequest

type SimulateRequest struct {
	Shortcode     uint
	CommandID     TransactionType
	AmountKES     int64
	Payer         string
	BillRefNumber string
}

SimulateRequest drives a sandbox payment. It is rejected outright in production, where the only way to move money is for a customer to move it.

type SimulateResponse

type SimulateResponse struct {
	OriginatorConversationID string `json:"OriginatorCoversationID"`
	ConversationID           string `json:"ConversationID"`
	ResponseCode             string `json:"ResponseCode"`
	ResponseDescription      string `json:"ResponseDescription"`
}

SimulateResponse acknowledges a simulated payment.

type TokenStore

type TokenStore interface {
	Get(ctx context.Context, key string) (token string, expiresAt time.Time, ok bool)
	Set(ctx context.Context, key, token string, expiresAt time.Time) error
	Delete(ctx context.Context, key string) error
}

TokenStore caches access tokens. Implementations must be safe for concurrent use.

Daraja invalidates the previous access token whenever a new one is minted, so two replicas each holding a private cache will evict each other and spend most of their calls recovering. A shared store makes the mint happen once for the whole cluster.

type TransactionStatusRequest

type TransactionStatusRequest struct {
	// TransactionID is the M-Pesa receipt. Supply this or
	// OriginalConversationID.
	TransactionID string

	// OriginalConversationID identifies a request whose receipt we never saw.
	OriginalConversationID string

	// PartyA defaults to the configured collection shortcode.
	PartyA         uint
	IdentifierType PartyIdentifierType

	Remarks  string
	Occasion string

	URLs AsyncURLs
}

TransactionStatusRequest asks Daraja what became of a transaction.

This is the resolution path for anything a queue timeout left unknown. It is the only way to turn "we do not know" into a fact without guessing, and it is the reason a timeout must never be retried.

type TransactionType

type TransactionType string

TransactionType selects the paybill or till flavour of the prompt. Sending the wrong one for the shortcode yields result code 2028.

const (
	CustomerPayBillOnline  TransactionType = "CustomerPayBillOnline"
	CustomerBuyGoodsOnline TransactionType = "CustomerBuyGoodsOnline"
)

The two prompt flavours.

type TrxCode

type TrxCode string

TrxCode selects what a dynamic QR code does when scanned.

const (
	TrxBuyGoods       TrxCode = "BG"
	TrxPayBill        TrxCode = "PB"
	TrxWithdrawAgent  TrxCode = "WA"
	TrxSendMoney      TrxCode = "SM"
	TrxSendToBusiness TrxCode = "SB"
)

The QR transaction types.

type ValidationPolicy

type ValidationPolicy string

ValidationPolicy is how a caller intends to act on a verdict. The package records it and acts on nothing; enforcement belongs to whoever owns the registration and payment flows.

const (
	// ValidationDisabled does not call at all. Correct when an upstream
	// platform already validates.
	ValidationDisabled ValidationPolicy = "disabled"

	// ValidationAdvisory calls, records, and never blocks.
	ValidationAdvisory ValidationPolicy = "advisory"

	// ValidationEnforcing blocks on a mismatch.
	ValidationEnforcing ValidationPolicy = "enforcing"
)

The three settings.

Advisory is the one to run first whatever the eventual answer: it records mismatches without blocking anyone, which measures whether an upstream platform is already validating rather than assuming it.

type ValidationResponse

type ValidationResponse struct {
	// ResultCode is ValidationAccepted (0) to accept the payment, non-zero to reject it.
	ResultCode ValidationResultCode `json:"ResultCode" example:"0"`
	// ResultDesc is a short human-readable reason, echoed back to Safaricom.
	ResultDesc string `json:"ResultDesc" example:"Accepted"`

	// ThirdPartyTransID is echoed back on the matching confirmation, which is
	// the only way to correlate the two callbacks.
	ThirdPartyTransID string `json:"ThirdPartyTransID,omitempty"`
}

ValidationResponse is the body we return to a validation request.

func AcceptPayment

func AcceptPayment(thirdPartyTransID string) ValidationResponse

AcceptPayment builds an accepting validation response.

func RejectPayment

func RejectPayment(code ValidationResultCode) ValidationResponse

RejectPayment builds a rejecting validation response.

Rejecting with code 0 accepts the payment, so a caller that forgets to set a code accepts money it meant to refuse. This exists so the code is never implicit.

type ValidationResultCode

type ValidationResultCode string

ValidationResultCode is what we answer a validation request with.

const (
	ValidationAccepted             ValidationResultCode = "0"
	ValidationInvalidMSISDN        ValidationResultCode = "C2B00011"
	ValidationInvalidAccountNumber ValidationResultCode = "C2B00012"
	ValidationInvalidAmount        ValidationResultCode = "C2B00013"
	ValidationInvalidKYC           ValidationResultCode = "C2B00014"
	ValidationInvalidShortcode     ValidationResultCode = "C2B00015"
	ValidationOtherError           ValidationResultCode = "C2B00016"
)

Accept, and the documented rejection codes. Rejecting with the right code gives the customer a message that matches what went wrong, which is the difference between "wrong account number" and a generic failure on a feature phone.

type WrappedAmount

type WrappedAmount struct {
	CurrencyCode string
	Minor        int64
}

WrappedAmount is Daraja's other non-JSON encoding:

{Amount={CurrencyCode=KES, MinimumAmount=618683, BasicAmount=6186.83}}

MinimumAmount is the value in minor units and BasicAmount the same figure with a decimal point, so the two disagree by a factor of a hundred by design.

func ParseWrappedAmount

func ParseWrappedAmount(value string) (WrappedAmount, bool)

ParseWrappedAmount decodes the brace-and-equals encoding. Lenient for the same reason as ParseBalances.

Directories

Path Synopsis
Package darajastub is an in-process Daraja.
Package darajastub is an in-process Daraja.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL