How MoMo callbacks work
When you send a Request to Pay with an X-Callback-Url header, MTN answers 202 Accepted straight away and pushes a prompt to the customer's phone. When the customer approves, declines or ignores it, MTN sends the result to your callback URL.
The callback body has the same shape as the status response fromGET /collection/v1_0/requesttopay/{X-Reference-Id}: status, amount, currency,externalId, payer, and on success a financialTransactionId, or on failure a reason.
Four rules for a safe handler
- Don't trust the body. Your callback URL is public. Treat the callback as "something changed", then confirm with the status endpoint before you release goods.
- Carry your own reference. Not every callback includes your
X-Reference-Id. Add it to the URL yourself:/webhooks/momo?ref=<X-Reference-Id>. - Accept POST and PUT, and reply fast. Return
200immediately and do the work afterwards or in a queue. - Be idempotent. The same callback can arrive more than once. If the order is already paid, do nothing.
Also keep a fallback: if no final callback arrives within a few minutes, poll the status endpoint with backoff.
MTN MoMo failure reason codes
Failed payments include a reason, either as a plain string ("APPROVAL_REJECTED") or as an object ({"code": "...", "message": "..."}). Handle both. MTN's own documentation writes a few codes with a space (NOT ENOUGH_FUNDS), so normalise codes before you compare them.
| Code | Meaning | Retry? | What to do |
|---|---|---|---|
APPROVAL_REJECTED | The customer declined the payment prompt on their phone. | Yes | Mark the order unpaid. Offer a "Try again" button; send a new Request to Pay with a new X-Reference-Id. |
EXPIRED | The customer did not approve the prompt before it timed out (about 5 minutes). | Yes | Mark the order unpaid and let the customer retry with a new X-Reference-Id. |
NOT_ENOUGH_FUNDS | The payer does not have enough money in their MoMo wallet, including fees. | Yes | Show the total amount including any fees before you send the request, so customers can top up first. |
PAYER_LIMIT_REACHED | The payer's transaction limit has been reached. Limits vary by country and account level. | Yes | Let the customer pay with another number, or split the payment if your business allows it. |
PAYEE_LIMIT_REACHED | The receiving account's limit has been reached. Limits vary by country. | Later | Check your merchant account limits with MTN, or reduce the amount. |
PAYER_NOT_FOUND | The phone number is not a registered, active MoMo account. | Yes | Check the number format (country code, no leading 0). Use "Validate account holder status" before charging. |
PAYEE_NOT_FOUND | Your merchant (payee) account is not active — for debits this usually means the business account is blocked. | No | Contact your MTN account manager. Retrying will keep failing until your account is fixed. |
PAYEE_NOT_ALLOWED_TO_RECEIVE | The receiving wallet is not allowed to receive funds (for example, it is not active). | No | Check the receiving account status with MTN. |
COULD_NOT_PERFORM_TRANSACTION | The transaction was not completed. Often the approval window passed or the network failed mid-way. | Yes | Retry with a new X-Reference-Id. If it repeats, check the status endpoint and contact MTN. |
TRANSACTION_CANCELED | The transaction was cancelled before completion. | Yes | Mark the order unpaid and allow a new attempt. |
PAYMENT_NOT_APPROVED | The payment was not approved by the payer. | Yes | Mark the order unpaid and allow a new attempt. |
INTERNAL_PROCESSING_ERROR | A generic error inside MTN's system. | Later | Check the status endpoint before retrying — the payment may still complete. Retry later with a new X-Reference-Id. |
SERVICE_UNAVAILABLE | The MoMo service is temporarily unavailable. | Later | Back off and retry later. Consider showing another payment method meanwhile. |
NOT_ALLOWED | Your API user is not authorised for this operation. | No | Check your product subscription (Collections) and API user permissions with MTN. |
NOT_ALLOWED_TARGET_ENVIRONMENT | Your credentials are not valid for the X-Target-Environment you sent (e.g. sandbox keys with mtnuganda). | No | Make the target environment match your credentials. This is a configuration bug, not a customer problem. |
INVALID_CALLBACK_URL_HOST | The X-Callback-Url host does not match the providerCallbackHost registered for your API user. | No | Use a callback URL on the host you registered, or update the registration. |
INVALID_CURRENCY | The currency is not supported for this target environment (the sandbox only accepts EUR). | No | Send EUR in the sandbox and the local currency in production (e.g. UGX for mtnuganda). |
RESOURCE_ALREADY_EXIST | The X-Reference-Id was already used. Every request needs a new UUID v4. | Yes | Generate a fresh UUID v4 for every Request to Pay. Never reuse one, even for a retry. |
RESOURCE_NOT_FOUND | MTN could not find the resource you referred to. | Yes | Check you are using the right X-Reference-Id and target environment. |
TRANSACTION_NOT_FOUND | The transaction could not be found. | Yes | Check the reference ID and target environment you used for the status lookup. |
VALIDATION_ERROR | The request failed validation: wrong data types, unsupported decimals, empty fields, or notes over 128 characters. | No | Fix the request: amount as a string, supported currency, non-empty payerMessage/payeeNote of 128 characters or fewer. |
Codes from MTN's published failure-reasons list and the MoMo Open API ErrorReason enum, checked October 2026.
For HTTP errors and full explanations with sources, see the MTN MoMo error code reference, or search every provider in the Payment Error Code Lookup.
Frequently asked questions
Can I mark an order as paid when the MoMo callback says SUCCESSFUL?
Not on the callback alone. Anyone who knows your callback URL can send it a fake request. Use the callback as a signal, then call the Request to Pay status endpoint with your X-Reference-Id and only fulfil the order if that response says SUCCESSFUL and the amount and currency match.
Does MTN send the MoMo callback as POST or PUT?
Reports differ between versions and countries, so accept both methods on your callback route. The handler code generated by this tool does that.
How do I match a MoMo callback to my order?
Don't rely on the body containing your X-Reference-Id. Put it in the callback URL yourself, for example https://yourdomain.com/webhooks/momo?ref=<X-Reference-Id>, and also check that externalId matches the order ID you sent.
Why am I not receiving MoMo callbacks?
The most common causes are a callback URL on a different host from the providerCallbackHost registered for your API user, a URL that is not publicly reachable (localhost), a non-HTTPS URL in production, or a firewall or CSRF check rejecting the request. Callbacks can also be delayed, so always have a status-polling fallback.
What does APPROVAL_REJECTED mean in MTN MoMo?
The customer saw the payment prompt on their phone and declined it. Mark the order unpaid and let them try again with a new X-Reference-Id.
Related
- MTN MoMo Request Builder: generate the Request to Pay and status-check code.
- MTN MoMo Collections API in Node.js: the full integration walkthrough.
- MTN MoMo Collections API in Laravel: service class, queued callback job and tests.
- MTN MoMo Collections API in Django: requests client, idempotent confirmation and tests.