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
- func AssertCallbackURL(value string) error
- func ExpressRejection(resp *ExpressResponse) error
- func NormalizeMSISDN(value string) (string, error)
- func ParseTimestamp(value string) (time.Time, bool)
- type APIError
- type AccountBalance
- type AccountBalanceRequest
- type AccountResolver
- type AsyncAck
- type AsyncOutcome
- type AsyncURLs
- type B2BRequest
- type B2CRequest
- type C2BNotification
- type C2BNotificationWire
- type C2BPayload
- type Callback
- type CallbackKind
- type Client
- func (c *Client) AccessToken(ctx context.Context) (string, error)
- func (c *Client) AccountBalance(ctx context.Context, req AccountBalanceRequest) (*AsyncAck, error)
- func (c *Client) B2C(ctx context.Context, req B2CRequest) (*AsyncAck, error)
- func (c *Client) B2Pochi(ctx context.Context, req B2CRequest) (*AsyncAck, error)
- func (c *Client) BusinessBuyGoods(ctx context.Context, req B2BRequest) (*AsyncAck, error)
- func (c *Client) BusinessPayBill(ctx context.Context, req B2BRequest) (*AsyncAck, error)
- func (c *Client) BusinessPayToBulk(ctx context.Context, req B2BRequest) (*AsyncAck, error)
- func (c *Client) CollectionShortcode() uint
- func (c *Client) DisbursementShortcode() uint
- func (c *Client) DynamicQR(ctx context.Context, req QRRequest) (*QRResponse, error)
- func (c *Client) Environment() Environment
- func (c *Client) Express(ctx context.Context, req ExpressRequest) (*ExpressResponse, error)
- func (c *Client) ExpressQuery(ctx context.Context, checkoutRequestID string, shortcode uint) (*ExpressQueryResponse, error)
- func (c *Client) PayTaxToKRA(ctx context.Context, req B2BRequest) (*AsyncAck, error)
- func (c *Client) PullAll(ctx context.Context, from, to time.Time, shortcode uint) ([]PulledTransaction, error)
- func (c *Client) PullRegister(ctx context.Context, req PullRegisterRequest) (*PullRegisterResponse, error)
- func (c *Client) PullTransactions(ctx context.Context, from, to time.Time, offset int, shortcode uint) ([]PulledTransaction, error)
- func (c *Client) QueryOrgInfo(ctx context.Context, req OrgInfoRequest) (*OrgInfoResponse, error)
- func (c *Client) RegisterURL(ctx context.Context, req RegisterURLRequest) (*RegisterURLResponse, error)
- func (c *Client) Reverse(ctx context.Context, req ReversalRequest) (*AsyncAck, error)
- func (c *Client) SecurityCredential() (string, error)
- func (c *Client) Simulate(ctx context.Context, req SimulateRequest) (*SimulateResponse, error)
- func (c *Client) TransactionStatus(ctx context.Context, req TransactionStatusRequest) (*AsyncAck, error)
- func (c *Client) ValidateMobileNumber(ctx context.Context, msisdn string, idType IDType, idNumber string, ...) (*NumberValidation, error)
- type Config
- type DarajaError
- type Environment
- type ExpressCallback
- type ExpressCallbackBody
- type ExpressCallbackEnvelope
- type ExpressCallbackMetadataHolder
- type ExpressCallbackMetadataItem
- type ExpressCallbackResult
- type ExpressOutcome
- type ExpressPayload
- type ExpressQueryResponse
- type ExpressRequest
- type ExpressResponse
- type FlexibleInt64
- type HakikishaRequest
- type HakikishaResponse
- type HttpClient
- type IDType
- type MaskedMSISDN
- type MemoryTokenStore
- type NumberValidation
- type Options
- type OrgIdentifierType
- type OrgInfoRequest
- type OrgInfoResponse
- type Outcome
- type OutcomeKind
- type Parameters
- func (p Parameters) All(key string) []string
- func (p Parameters) Balances(key string) ([]AccountBalance, bool)
- func (p Parameters) Get(key string) (string, bool)
- func (p Parameters) Int(key string) (int64, bool)
- func (p Parameters) Minor(key string) (int64, bool)
- func (p Parameters) Time(key string) (time.Time, bool)
- func (p Parameters) WrappedAmount(key string) (WrappedAmount, bool)
- type PartyIdentifierType
- type PayBillPayload
- type PayoutCommand
- type PullRegisterRequest
- type PullRegisterResponse
- type PulledTransaction
- type QRRequest
- type QRResponse
- type RawResult
- type RegisterURLRequest
- type RegisterURLResponse
- type ResponseType
- type Result
- type ResultEnvelope
- type ResultError
- type ResultFamily
- type ReversalIdentifierType
- type ReversalPayload
- type ReversalRequest
- type SimulateRequest
- type SimulateResponse
- type TokenStore
- type TransactionStatusRequest
- type TransactionType
- type TrxCode
- type ValidationPolicy
- type ValidationResponse
- type ValidationResultCode
- type WrappedAmount
Constants ¶
const ( HakikishaFound = "0" HakikishaNotFound = "1" )
Hakikisha response codes.
Variables ¶
This section is empty.
Functions ¶
func AssertCallbackURL ¶
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 ¶
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.
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.
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 ¶
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 ¶
New builds a Client. It validates configuration eagerly so a misconfiguration is a boot failure rather than a failed payment.
func (*Client) AccessToken ¶
AccessToken returns a cached token, minting one if none is live.
func (*Client) AccountBalance ¶
AccountBalance asks Daraja for the shortcode balances. The figures arrive at the result URL as a pipe-delimited string; see ParseBalances.
func (*Client) B2Pochi ¶
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 ¶
BusinessBuyGoods pays a till.
func (*Client) BusinessPayBill ¶
BusinessPayBill pays a paybill.
func (*Client) BusinessPayToBulk ¶
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 ¶
CollectionShortcode reports the configured collection shortcode.
func (*Client) DisbursementShortcode ¶
DisbursementShortcode reports the configured disbursement shortcode.
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 ¶
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 ¶
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 ¶
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 ¶
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) 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 ¶
HttpClient is the outbound seam. *http.Client satisfies it.
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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
ResultCodeInt reports the result code as an integer when it is one.
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:
- 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.
- It resolves to no open loan, or to one outside our domain.
- It has not credited a loan. If it has, the correct action is a refund decision, not a reversal.
- No reversal is already in flight or complete for it, enforced by a unique index rather than a read.
- The amount matches the observed amount exactly.
- 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 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 ¶
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.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package darajastub is an in-process Daraja.
|
Package darajastub is an in-process Daraja. |