Webhook — CCAvenue (Payments)
The three public CCAvenue ingest surfaces: the browser return and cancel handlers that 302 the shopper onward, and the server-to-server dynamic event notification. The notification settles from its own decrypted payload; the browser surfaces re-read the Status API.
Three public surfaces. Two are browser-driven form POSTs that redirect the shopper onward; one is a genuine server-to-server notification. All three decrypt CCAvenue's encResp. The browser surfaces use it only to identify the order and settle by re-reading the Status API; the notification settles from the decrypted payload itself, with no Status API call.
Source:
api-modules/payment-ccavenue/src/controllers/ccavenue-response.controller.tsandccavenue-notify.controller.ts
Authentication
CCAvenue signs nothing. There is no header MAC, no shared webhook secret and no per-request signature on any of these endpoints. What stands in for one is that only CCAvenue holds the working key needed to produce a body that decrypts.
The cipher is AES-128-CBC without a MAC, so an altered ciphertext still decrypts — but altering any byte scrambles the entire preceding 16-byte block. order_status= plus a status value never fits in one block, so a status cannot be rewritten without scrambling part of it, and the same holds for merchant_param1 and amount. The decoder therefore rejects any frame containing control or replacement characters, and the notification additionally requires:
- the echoed
order_idto equal the merchant reference of the payment attempt it claims, - a
paidstatus to carry anamountequal to the order total, and acurrency(when present) equal to the configured one, - a matching
order_paymentrow written at place time.
Because there is no signature header, nothing is added to WEBHOOK_SIGNATURE_HEADERS. The archived delivery body holds only ciphertext.
POST /webhooks/payments/ccavenue/response
CCAvenue's redirect_url. The shopper's browser posts here when the billing page finishes, on success and failure alike.
- Request:
application/x-www-form-urlencodedwith a singleencRespfield. - Response: always a
302, never JSON.
The handler decrypts encResp, resolves the order from merchant_param1, reconciles through the shared funnel, then redirects to the storefront:
The payment attempt is read off the echoed order_id (a -R<n> suffix, or attempt 1 when absent), so a late callback for an earlier attempt reconciles against that attempt. Order resolution still goes through merchant_param1 alone — the echoed order_id names the attempt, never the order.
{store_url}{storefront_return_path}?order=ORD-2026-00000123&status=paidstatus | Meaning |
|---|---|
paid | Settled. |
failed | CCAvenue reported a terminal failure; the order is cancelled. |
pending | CCAvenue has not finished. The order self-heals via the notification or the sweep. |
unknown | The callback could not be decrypted or attributed, or reconcile errored. Logged server-side. |
Every failure path still redirects. A shopper mid-payment must land somewhere sensible rather than on an error page, and the order recovers by other means.
POST /webhooks/payments/ccavenue/cancel
CCAvenue's cancel_url — where the shopper is sent if they abandon the billing page. Identical handling and identical redirect contract.
POST /webhooks/payments/ccavenue/notify
CCAvenue's dynamic event notification, configured in the MARS panel. This one is server-to-server, so a shopper who closes the browser after paying still gets their order settled in seconds rather than waiting for the stale-pending sweep.
- Request: same
encRespform field. - Response:
200with the standard envelope.
{
"data": { "accepted": true, "orderStatus": "Successful", "handled": true },
"message": "Success",
"statusCode": 200
}| Field | Meaning |
|---|---|
accepted | The delivery was well-formed. |
orderStatus | CCAvenue's order_status, echoed for the operator's benefit. |
handled | Whether this delivery caused a transition. false for duplicates, unattributable callbacks and non-terminal states. |
Deduplication
CCAvenue ships no event id, so one is composed as `${tracking_id ?? order_id}-${order_status}` and claimed through the shared webhook inbox. Retries of the same event collide; a later state change on the same transaction stays distinct.
A delivery that ended failed is the exception: the next retry re-admits it and settlement runs again. That is what makes a Status API outage self-healing — the deliveries that 5xx'd during it settle on CCAvenue's own retry rather than needing an operator replay.
Settlement
The notification settles from the decrypted payload: order_status maps through the status table, amount is checked against the order total, and tracking_id (falling back to bank_ref_no) becomes the payment's external reference. The Status API is not called, so this surface keeps working when the API rejects the host (for example code 51407, an outbound IP that is not allow-listed). Only non-personal fields of the payload are stored on the payment row — the billing and delivery details are dropped.
Outcomes
| Situation | Inbox outcome | HTTP |
|---|---|---|
| Settled or cancelled the order | processed | 200 |
Duplicate delivery (processed / skipped) | not re-claimed | 200 |
| Order not attributable | not claimed | 200 |
| CCAvenue reports a non-terminal state | skipped | 200 |
| Settlement threw (tampered frame, reference/amount/currency mismatch, database error) | failed | 5xx — CCAvenue retries, and the retry re-runs settlement |
A non-terminal state is not a delivery failure, so it acks 200 and is recorded as skipped rather than triggering a retry storm.
A skipped row records the reported order_status plus any failure_message / status_message.
Missing encResp
Returns 400. On this endpoint that indicates a misconfiguration on our side — the api's raw-body capture is scoped to /webhooks, so a missing body means the route moved out from under it — and is logged at error level.
GET /webhooks/payments/ccavenue/notify
A reachability probe. Opening the notification URL in a browser, or a panel's URL check, sends a bodyless GET; it answers 200 so the URL reads as live. Nothing is claimed in the inbox and no order is touched.
{
"data": { "accepted": false, "orderStatus": "unknown", "handled": false },
"message": "Success",
"statusCode": 200
}Setup
Register all three URLs in the MARS panel. They must be publicly reachable over HTTPS. The admin configuration page renders them with copy buttons.
{api_base}/webhooks/payments/ccavenue/response
{api_base}/webhooks/payments/ccavenue/cancel
{api_base}/webhooks/payments/ccavenue/notifyThe first two are also sent as redirect_url and cancel_url on every transaction, built from response_base_url, so a per-transaction value always overrides whatever is configured in MARS.
Related
Webhook — Klaviyo (Marketing)
Public HTTP surface that Klaviyo's servers call to deliver webhook events. The handler verifies an HMAC signature over the raw body, dedups on Klaviyo's event id, and mirrors…
Webhook — PhonePe (Payments)
Public endpoint PhonePe calls to deliver payment and refund lifecycle events. Verified with SHA256(username:password), deduplicated on a composed event id, and never trusted for money.