M-Pesa error 2001 invalid initiator: the stale certificate trap
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.
The five causes, ranked by how often I actually find them
| Cause | Tell-tale sign | Fix |
|---|---|---|
| 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', notecho.echoappends 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:
- 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.
- Grep your repository and your CI variables for any
.cerfile. Delete the ones that did not come from your portal, so nobody re-encrypts against them next quarter. - 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
- 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.
- Log the outbound request body with the credential redacted and compare it field
by field against the documented schema. A misspelled
CommandIDor aPartyAthat is not the shortcode can surface as a credential complaint. - 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.
- 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.