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. None is trusted for money.
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 only to identify the order, then settle by re-reading the Status API.
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. Two things stand in for one:
- Only CCAvenue holds the working key needed to produce a body that decrypts.
- Decisively — settlement re-reads the Status API regardless of what the body says.
The second is what actually makes this safe. The body cannot move money in any direction; at most a forged one causes a redundant status lookup for an order the attacker already knows the internal UUID of. Do not "harden" these endpoints by trusting order_status or amount from the payload.
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.
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 |
| Reconcile threw | 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.
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.
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.