Receiving CicaPay-Momo webhooks
CicaPay sends a JSON POST request to the URL registered for the matching environment and event. Your endpoint must verify the request signature against the raw request body, handle the payload, and return a 2xx response when it has accepted the notification.
Register the receiver
Go to your dashbord, and click
environment is required and is either sandbox or live. Webhooks are isolated by environment: a sandbox transaction is delivered only to a webhook registered with "environment": "sandbox"; it can never be sent to a live webhook URL.
The URL must use https://, resolve publicly, and must not resolve to a private, loopback, or reserved IP address. The same normalized URL cannot be registered more than once on your account in the same environment. URL scheme and host are compared case-insensitively, and a trailing slash in the path is ignored for this duplicate check.
The create response includes the webhook secret. Keep a copy in your receiver configuration. Unlike an API key private key, this secret remains visible on the webhook because CicaPay needs it to sign outbound deliveries and you need it to verify them.
If it is compromised, regenerate it with
and update your receiver immediately.
Manage a registered webhook
All webhook-management routes require account authentication.
| Name | Effect |
|---|---|
Webhooks | Lists the account's webhooks, newest first. |
| Update | Updates url, events, or both. At least one must be supplied; supplied values are fully validated as on creation. The environment cannot be changed. |
| Disable | Sets the webhook to disabled; no new transaction deliveries are queued for it. |
| Enable | Sets the webhook to active and resets its consecutive-failure count. |
Regenerate Secret | Replaces the signing secret and returns the webhook with its new secret. |
Test | Sends the one-shot signed test notification described below. |
When a secret is regenerated, future send attempts—including retry attempts for already queued deliveries—use the new secret. Update the receiver before relying on further delivery.
Verify every request before parsing it
CicaPay sends these headers:
Content-Type: application/json
X-Webhook-Signature: sha256=<hex-hmac><hex-hmac> is the lowercase hexadecimal HMAC-SHA256 of the exact raw HTTP request body, using the webhook's secret as the key:
expected = "sha256=" + HMAC_SHA256_HEX(secret, raw_request_body)Compare the full header value with expected using a timing-safe comparison. Do this before JSON decoding or changing the body in any way; re-serializing parsed JSON produces a different byte sequence and will not verify. Reject a missing or invalid signature with a non-2xx response.
PHP example:
$rawBody = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expected, $received)) {
http_response_code(400);
exit;
}
$notification = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);Notification payload
Every real transaction notification has the following form. The event field identifies why it was sent. Fields whose transaction value is null are omitted, which is most relevant to the optional merchant_reference.
{
"reference": "PI_260819_ABCDEF123456",
"merchant_reference": "your-order-1042",
"environment": "live",
"type": "PAYIN",
"network": "MTN",
"currency": "XOF",
"amount": "1000.00",
"fee": "15.00",
"net_amount": "985.00",
"status": "SUCCESS",
"event": "transaction.success"
}| Field | Meaning |
|---|---|
reference | CicaPay transaction reference. Use this as the CicaPay transaction identifier. |
merchant_reference | Your optional custom string supplied when the payin or payout was created. It is omitted when no value was supplied. |
environment | sandbox or live, matching the webhook that received the notification. |
type | PAYIN or PAYOUT. |
network | Transaction network. |
currency | Wallet currency used by the transaction. |
amount | Transaction amount. |
fee | Transaction fee. |
net_amount | Transaction net amount. |
status | Transaction status at the time of this notification. |
event | Subscribed event that caused this delivery. |
Only transaction.failed adds these fields:
{
"reason": "Provider-provided failure description",
"failure_code": "provider_rejected"
}failure_code is always included for a failed notification. Current values emitted by the implementation include invalid_phone_number, provider_rejected, mtn_status_failed, and simulated_failure. Use failure_code for programmatic branching; reason is descriptive text.
Events and status flow
Subscribe to one or more of exactly these events:
| Event | When it is sent | Payload status |
|---|---|---|
transaction.created | The payin or payout has been created, moved to the queue, and is about to be processed. | PENDING |
transaction.processing | An MTN request has been submitted and is waiting for its asynchronous result. | PROCESSING |
transaction.success | The provider has confirmed completion. | SUCCESS |
transaction.failed | The provider rejected or failed the transaction. | FAILED |
transaction.processing is emitted only for MTN. A transaction may go from transaction.created directly to transaction.success or transaction.failed, so do not require a processing notification before accepting a final one.
Acknowledge, retry, and process safely
CicaPay treats any HTTP status from 200 through 299 as a successful delivery. Any other HTTP response, timeout, or transport error is a failed attempt. The sender waits up to 10 seconds for your response.
The first attempt is sent immediately when the event occurs. A real transaction delivery has at most 8 total HTTP attempts. After failed attempts 1 through 7, retries are scheduled after 30 seconds, 60 seconds, 120 seconds, 240 seconds, 480 seconds, 960 seconds, and 1920 seconds respectively. The retry calculation is capped at one hour, although this eight-attempt limit is reached before that cap would be used. After the eighth failed attempt, that delivery is permanently abandoned.
Your receiver should therefore:
- Verify the signature on the raw body.
- Record or queue the notification using
referenceandeventin a way that tolerates repeated delivery of the same transaction event. - Return a
2xxresponse promptly after accepting it. - Process any slower internal work asynchronously after acknowledgement.
A webhook is marked dead after five consecutive deliveries each exhaust their own eight-attempt budget. It then receives no new events. A successful delivery resets that consecutive-failure count. After fixing the receiver, reactivate the webhook on your dashboard. Re-enabling also resets the count.
Test the endpoint
Use
from the dashboard/API to send a request to the registered URL immediately. It uses the same WebhookSender, JSON serialization, Content-Type, and X-Webhook-Signature generation as a real delivery, so a successful test verifies the signature procedure above.
The test body is exactly this shape (with the current ISO-8601 timestamp):
{
"event": "test",
"message": "This is a test webhook notification.",
"sent_at": "2026-08-19T12:34:56+00:00"
}A test is a one-shot delivery. If it fails, it is recorded but it is not retried. Treat event: "test" as a connectivity/signature test only; it is not a transaction notification.
Updated about 2 months ago

