View as Markdown llms.txt

Daraja's inconsistencies#

Daraja is not consistent with itself. The same concept has different field names in different products, one field name is misspelled in the specification and the corrected spelling is rejected, and one product changes both its envelope casing and its success code between the response and the callback.

This server reproduces all of it. The list below is useful whether or not you use this server: if you are writing your own integration, these are the things that will cost you an afternoon.

The initiator has two names#

B2C sends InitiatorName. B2B, transaction status, account balance and reversal all send Initiator. Same concept, same credential, different key.

json
{ "InitiatorName": "apiuser" }   // B2C only
{ "Initiator": "apiuser" }       // everything else

RecieverIdentifierType is misspelled#

The published specification spells it RecieverIdentifierType, with the i and e transposed. This is not a typo in the docs: the API rejects the correctly spelled ReceiverIdentifierType.

The value also changes by product. 4 identifies an organisation shortcode in most places, 2 a Buy Goods till, and reversal wants 11 for the receiving party.

Occasion has two spellings#

B2C expects Occassion, with a double s. Transaction status expects Occasion, with one. Both are optional, which means sending the wrong one fails quietly rather than loudly.

Ratiba disagrees with itself#

The published sample body names the field StandingOrderNameName, doubled, while the parameter table directly beneath it says StandingOrderName. The same page spells the tracking id CustomStoId in the sample and CustomstdoId in the table.

This server sends both spellings of both fields. Daraja ignores the one it does not recognise, and the request survives whichever spelling they eventually fix.

Ratiba also changes shape between its two messages:

Synchronous responseCallback
EnvelopeResponseHeader / ResponseBodyresponseHeader / responseBody
Success code"200""0"

Different casing and a different code space, from one product, on one page of documentation.

Success codes are not one value#

Most products treat ResponseCode: "0" as success. Dynamic QR returns "00". Pull Transactions returns "1000". A client that only accepts "0" raises an error on a perfectly successful QR generation.

Daraja also returns HTTP 200 with an error envelope, and occasionally an HTML gateway page with a 200 status. Neither is success.

ResultCode is sometimes a number#

STK push callbacks send ResultCode as a JSON number. Most other products send it as a string. If you compare with === against "0" you will silently treat successful STK payments as failures.

The two code spaces#

This is the one that causes real financial errors.

A synchronous ResponseCode of 0 means accepted for processing. It does not mean money moved. The callback's ResultCode of 0 means it did.

Treating the first as confirmation marks unpaid orders as paid. It is an easy mistake because both fields are called something-Code and both use 0 for success. See callbacks and waiting for how the *_and_wait tools avoid it.

Failure callbacks carry no metadata#

A successful STK callback includes CallbackMetadata holding the receipt number, amount, and phone number. A failed one omits the field entirely.

json
// Success
{
  "Body": {
    "stkCallback": {
      "ResultCode": 0,
      "CallbackMetadata": { "Item": [ ... ] }
    }
  }
}

// Failure: no CallbackMetadata at all
{
  "Body": {
    "stkCallback": {
      "ResultCode": 1032,
      "ResultDesc": "Request cancelled by user"
    }
  }
}

Code that reaches for the receipt number without checking crashes on the first declined payment. The simulator reproduces this, so you find it locally rather than at 2am.

Timestamps must be East Africa Time#

The Timestamp field on an STK push must be Nairobi local time, UTC+3, in YYYYMMDDHHmmss. Using UTC backdates the request by three hours and Daraja rejects it as expired.

The Password field is base64(shortcode + passkey + timestamp), and the timestamp inside it must be byte-identical to the one in the Timestamp field. Deriving them in two separate calls fails intermittently when a request happens to straddle a second boundary, which is a memorable way to spend a day.

One more typo, for completeness#

The C2B register-URL response returns OriginatorCoversationID, missing the n in Conversation. Not harmful, but worth knowing before you spend ten minutes wondering why your destructuring returns undefined.

Why reproduce them#

Because the API rejects the corrected forms. A client that tidies these up looks better and does not work.

The simulator preserves the quirks too. A mock that returns clean, consistent payloads lets you write code that passes its tests and fails against the real thing.