</>CodeWithKarani

M-Pesa error 2001 invalid initiator: the stale certificate trap

Karani GeoffreyKarani Geoffrey8 min read

It is 9pm on a Friday and payroll has to go out. The B2C calls return an HTTP 200 with a ConversationID, everyone relaxes, and then the result callbacks start landing: ResultCode: 2001. Nobody has been paid. The same code worked perfectly in sandbox two hours ago.

Here is the thing about 2001 that costs people an entire night: it almost never means what it says. "The initiator information is invalid" reads like your InitiatorName is misspelled, so that is what everyone checks first, twenty times. In practice, the overwhelmingly common cause is that the SecurityCredential you sent could not be decrypted on Safaricom's side, because you encrypted your initiator password with the wrong public certificate. And there is no error code for "wrong certificate". It all collapses into 2001.

The wrong certificate usually arrives the same way: someone copies a .cer file out of a GitHub repo or a tutorial zip because it was easier than logging into the portal. Those files are frequently the old G2 certificate, or a sandbox certificate being used in production. They encrypt fine. They just produce a credential the M-Pesa side cannot open.

download the public certificate from your own Daraja portal (the production certificate for production, the sandbox certificate for sandbox), re-encrypt your initiator password with it, and replace SecurityCredential.

openssl x509 -in ProductionCertificate.cer -pubkey -noout > mpesa_pub.pem
printf '%s' 'YourInitiatorPassword' \
  | openssl pkeyutl -encrypt -pubin -inkey mpesa_pub.pem \
      -pkeyopt rsa_padding_mode:pkcs1 \
  | base64 -w 0

Then confirm InitiatorName matches the portal exactly, and that the initiator is attached to the shortcode you are calling (for a till, that is the Head Office number, not the store number).

The exact error: ResultCode 2001, "The initiator information is invalid."

For B2C and B2B this normally arrives on your ResultURL, not in the immediate HTTP response, which is why it is easy to miss during a rushed deploy:

{
  "Result": {
    "ResultType": 0,
    "ResultCode": 2001,
    "ResultDesc": "The initiator information is invalid.",
    "OriginatorConversationID": "8521-4297822-1",
    "ConversationID": "AG_20260714_20402a1b3c4d5e6f7890",
    "TransactionID": "SGH0000000"
  }
}

You may also see it phrased as Invalid Initiator Information in the Daraja error tables, and occasionally surfaced synchronously inside a 500.001.1001 wrapper. Same root cause, same fix.

Why this happens: what SecurityCredential actually is

SecurityCredential is not a token, not a hash and not something you generate once and own forever. It is your initiator password, RSA-encrypted with Safaricom's public key and base64-encoded. Safaricom holds the matching private key. On every B2C, B2B, reversal or transaction status call, they decrypt what you sent and compare the plaintext to the password stored against that initiator.

That gives you three separate ways to fail, and only one error code to distinguish them:

  • Decryption fails outright - you used a certificate whose private key they do not hold for this environment, such as the old G2 certificate or the sandbox certificate. The ciphertext is mathematically valid, it simply does not open with the key they try.
  • Decryption succeeds but the plaintext is wrong - right certificate, wrong or stale initiator password.
  • Everything decrypts and matches, but the initiator is not authorised for the shortcode in the request, or does not have the API role for that operation.

All three come back as 2001. That is the whole reason this error burns so much time: the API is deliberately vague about credential failures, which is defensible security practice and miserable developer experience.

Your side Initiator password (plain text) issued by Safaricom, never sent as-is RSA encrypt (PKCS#1 v1.5) + base64 uses the public certificate you supply SecurityCredential (344 chars) sent in the B2C / B2B request body Safaricom side Correct certificate for this environment private key decrypts, password matches ResultCode 0 - request accepted Stale G2 cert, or wrong environment private key does not open the ciphertext no "bad certificate" error code exists ResultCode 2001 - initiator information is invalid The encryption step always succeeds locally. You only find out the certificate was wrong after the money did not move, which is why this error is usually discovered in production.
Encryption never fails on your machine. That is the trap: a stale certificate produces a perfectly well-formed credential that nobody can open.

The five causes, ranked by how often I actually find them

CauseTell-tale signFix
Certificate copied from a repo or blog post, often the old G2 file The .cer arrived with an SDK or a tutorial zip, not from your portal Download from your own Daraja portal and re-encrypt
Sandbox certificate used against production, or the reverse Worked in sandbox, failed the moment you switched base URLs Keep two credentials, one per environment, in separate secrets
Initiator password changed or was reset It worked last month and nobody touched the code Re-encrypt with the current password
InitiatorName mismatch Trailing space, wrong case, or the app name used instead of the API initiator Copy it from the portal, byte for byte
Initiator not attached to the shortcode Calling with a till or store number instead of the Head Office number Call with the shortcode the initiator is registered against

Step 1: Get the certificate from your own portal

Log into the Daraja portal, open your app, and download the certificate from the API documentation section. There are two files, and they are not interchangeable: a sandbox certificate and a production certificate. Save them with names you cannot confuse later.

Do not accept a certificate from any other source. Not a colleague's laptop, not a Stack Overflow answer, not the sample folder of an SDK. This single rule prevents most 2001 tickets I have seen.

Step 2: Inspect the certificate before you trust it

openssl x509 -in ProductionCertificate.cer -noout -subject -issuer -dates -serial

You should get a subject and issuer that clearly belong to Safaricom, and a validity window that includes today. If openssl refuses to read the file, it is DER-encoded rather than PEM, which is normal for a .cer extension:

openssl x509 -inform DER -in ProductionCertificate.cer -noout -subject -dates

An expired certificate is a hard signal that you are holding a stale copy. The reverse is not true: a certificate can sit comfortably inside its validity window and still be the wrong one for the environment you are calling.

Step 3: Re-encrypt the initiator password

# Extract the public key from the certificate
openssl x509 -in ProductionCertificate.cer -pubkey -noout > mpesa_pub.pem

# Encrypt the initiator password and base64 it on a single line
printf '%s' 'YourInitiatorPassword' \
  | openssl pkeyutl -encrypt -pubin -inkey mpesa_pub.pem \
      -pkeyopt rsa_padding_mode:pkcs1 \
  | base64 -w 0 > security_credential.txt

Two details matter more than they look:

  • printf '%s', not echo. echo appends a newline, that newline gets encrypted, and the decrypted password will not match. This alone causes a lot of 2001s.
  • rsa_padding_mode:pkcs1. Daraja expects PKCS#1 v1.5 padding. If your language's crypto library defaults to OAEP, as several do, the ciphertext will be the right length and still be rejected. Set the padding explicitly rather than relying on a default.

Check the length of what you produced:

wc -c < security_credential.txt

With a 2048-bit key the ciphertext is 256 bytes, so the base64 form is 344 characters. If you got something dramatically shorter, you encrypted with a smaller key or truncated the output. If it contains newlines, -w 0 was missing and your JSON body is now malformed in a way that looks exactly like a credential problem.

Step 4: Verify InitiatorName byte for byte

printf '%s' "$MPESA_INITIATOR" | wc -c
printf '%s' "$MPESA_INITIATOR" | od -c | head -2

You are looking for an invisible trailing space or a carriage return that came from a Windows .env file. The initiator name is case sensitive and is not your portal login, your app name or your consumer key. It is the API operator username created for the shortcode.

Step 5: Confirm the initiator belongs to the shortcode you are calling

This is the cause people never reach, because it is not a code problem. An initiator is registered against a specific organisation shortcode. If you are paying out from a till, the API call is made with the Head Office number that the till sits under, not the store number printed on the counter. Call with the wrong one and the initiator genuinely is invalid for that organisation, so 2001 is, for once, telling you the truth.

Verification: how you know it is actually fixed

Send one small B2C payment to a number you control. A successful result callback looks like this:

{
  "Result": {
    "ResultType": 0,
    "ResultCode": 0,
    "ResultDesc": "The service request is processed successfully.",
    "TransactionID": "SGH4XY9ABC"
  }
}

Three further checks before you call it done:

  1. Run the same credential against the other environment and confirm it fails. If one credential works in both, you have not actually separated your environments and you will be back here.
  2. Grep your repository and your CI variables for any .cer file. Delete the ones that did not come from your portal, so nobody re-encrypts against them next quarter.
  3. Store the credential in your secret manager alongside the fingerprint of the certificate you generated it from, so future-you can prove which certificate produced it: openssl x509 -in ProductionCertificate.cer -noout -fingerprint -sha256.

What people get wrong

"Just regenerate the credential." People regenerate ten times with the same stale certificate and conclude the API is broken. Regeneration only helps if the input certificate changed. Change the certificate first, then regenerate.

Online "M-Pesa security credential generators". There are several web pages where you paste your initiator password and get a credential back. You have just handed a production payout password to a stranger's server, and those pages are usually built on the same stale certificate that caused your problem. Never use one. Encrypting locally with openssl takes ten seconds.

Treating the credential as environment-neutral. A credential is bound to one certificate, and each environment has its own. Ship two secrets, named unambiguously. If you are building this properly, that environment pair belongs behind an interface rather than scattered through your service, which I have written about in building a payments port instead of a Daraja wrapper.

Assuming the credential expires on a schedule. It does not rotate on its own. It becomes invalid when the initiator password is changed, when the initiator is reset by Safaricom, or when the certificate behind it is retired. If it stops working "for no reason", ask your Safaricom contact whether the initiator was reset.

When it is still broken

  1. Confirm you are not fighting a different error entirely. If the failure happens at the OAuth step rather than in the result callback, you have a token problem, not a credential problem. The two easily confused "invalid access token" codes are covered in 401.003.01 versus 404.001.03.
  2. Log the outbound request body with the credential redacted and compare it field by field against the documented schema. A misspelled CommandID or a PartyA that is not the shortcode can surface as a credential complaint.
  3. Ask Safaricom to confirm the initiator's role. An initiator can exist, belong to the right shortcode, and still lack the API role for the command you are sending. Only they can see that mapping.
  4. Check whether your organisation has more than one shortcode. In multi-branch setups it is common for staging to point at a decommissioned shortcode that still authenticates but has no valid initiator behind it.

If you are still stitching the wider integration together, the callback and reconciliation side is where the next round of pain lives: see Daraja in production and why your M-Pesa callback will fire twice.

Frequently asked questions

What does M-Pesa ResultCode 2001 actually mean?
It means Safaricom could not authenticate the initiator on your B2C, B2B, reversal or transaction status request. In practice that usually means your SecurityCredential could not be decrypted with their private key, because you encrypted the initiator password with a stale or wrong-environment certificate. It can also mean the initiator name is wrong, the initiator password changed, or the initiator does not belong to the shortcode you called.
How do I generate an M-Pesa SecurityCredential correctly?
Download the public certificate from your own Daraja portal for the environment you are targeting, extract the public key with 'openssl x509 -in cert.cer -pubkey -noout', then encrypt the initiator password with PKCS#1 v1.5 padding and base64 encode it on one line. Use printf rather than echo so no trailing newline is encrypted. With a 2048-bit key the result is 344 base64 characters.
Can I use the same SecurityCredential in sandbox and production?
No. Sandbox and production use different certificates and different key pairs, so a credential generated for one environment fails to decrypt in the other and comes back as error 2001. Generate two credentials and store them as separate secrets with names that cannot be confused.
Why does my M-Pesa credential work in sandbox but fail live?
The most common reason is that the production request is still carrying the sandbox credential, or that the certificate used for production came from a GitHub repository rather than your own portal and is an outdated G2 certificate. Re-download the production certificate from the Daraja portal, re-encrypt the production initiator password, and confirm the initiator is registered against the shortcode you are calling.
#M-Pesa#Daraja#OpenSSL#B2C#API Integration
Keep reading

Related articles