Payment — CCAvenue Provider
CCAvenue billing-page provider covering web checkout and the official Flutter SDK, the customer-side verify endpoint, the billing-page return handlers, the dynamic event notification, and gateway-driven refunds.
CCAvenue non-seamless billing page provider that plugs into the platform-neutral payment module. Covers web checkout, the official ccavenue_india_sdk_flutter plugin for Android/iOS, the customer-side verify endpoint, CCAvenue's dynamic event notification, and gateway-driven refunds through the neutral refund surface.
Source:
api-modules/payment-ccavenue(registered viaCCAvenuePaymentModule.forRoot()inapps/api/src/app.module.ts).Registers a
PaymentProviderwith idccavenueinto the platformPaymentRegistry. The order module knows nothing about CCAvenue specifically — it dispatches through the registry.Removing the
forRoot()line is a clean kill-switch: the verify endpoint, the return handlers and the notification webhook disappear, andccavenuedrops out of the provider registry.
How it differs from the other gateways
CCAvenue predates the JSON-and-bearer-token era, and three of its conventions leak into any integration:
| CCAvenue | Razorpay / PhonePe | |
|---|---|---|
| Transport | AES-128-CBC encrypted key=value& strings | JSON over HTTPS |
| Auth | A shared working key, no per-request signature | OAuth token / HMAC signature |
| Checkout handoff | An HTTP form POST to the billing page | A GET redirect or an SDK call |
| Amounts | Decimal rupees ("1321.95") | Integer subunits |
| Refunds | Synchronous — success or failure on the same call | Asynchronous, resolved by webhook |
Everything internal stays in integer subunits; conversion happens only at the wire boundary.
Configuration
Settings group payment.ccavenue, scope admin. Read at call time on every place, verify and notification, so rotating a credential in the admin UI takes effect without a deploy. Copy the credentials from the CCAvenue MARS panel under Settings → API Keys.
| Key | Required | Purpose |
|---|---|---|
merchant_id | ✅ | Numeric merchant identifier. |
access_code | ✅ | Sent with every billing-page request and handed to the Flutter SDK — not a secret. |
working_key | ✅ | Encrypts requests and authenticates callbacks. Secret. |
api_access_code | Access code for the status/refund API. Blank reuses access_code. | |
api_working_key | Working key paired with the above. Secret. Blank reuses working_key. | |
environment | test (default) or production. Selects both host pairs. | |
currency | INR (default), USD, SGD, GBP or EUR. Must match the store's own currency — the amount is never converted. | |
response_base_url | WEB only | Public origin of this API. The redirect and cancel URLs are built from it. |
storefront_return_path | Path on the storefront the shopper lands on after settlement. Defaults to /orders. | |
allow_refunds | Master switch for gateway refunds. Off by default. | |
sdk_app_color, sdk_font_color | Flutter SDK theming. Default #1F46BD / #FFFFFF. |
Any empty required value leaves the provider misconfigured; it refuses to call out regardless of the enabled-providers list.
Hosts
| Environment | Billing page | Server-to-server API |
|---|---|---|
test | test.ccavenue.com | apitest.ccavenue.com |
production | secure.ccavenue.com | api.ccavenue.com |
Encryption
The scheme is not written down in CCAvenue's integration PDFs — they ship prebuilt libraries instead — so it is pinned in services/ccavenue-crypto.service.ts and covered by a known-vector test:
key = raw 16-byte MD5 digest of the working key (NOT its hex string)
iv = fixed 0x00..0x0f
aes-128-cbc, PKCS#7 padding, lowercase hex outputThe key derivation is the classic failure. createHash("md5").update(key).digest("hex") is also 32 characters and type-checks fine, but CCAvenue rejects every transaction produced with it. A round-trip test passes either way, which is why the suite asserts a literal ciphertext.
Placing an order
Place an order as normal with paymentProvider: "ccavenue" and paymentMethod: "ccavenue". The order comes back pending_payment with requiresClientAction: true and a clientPayload shaped by platform.
WEB
{
"provider": "ccavenue",
"redirectUrl": "https://api.example.com/store/orders/{orderId}/ccavenue/redirect",
"orderId": "ORD-2026-00000123",
"amount": 132195
}Navigate the browser to redirectUrl. Nothing else is required.
CCAvenue's billing page will not accept a GET redirect — it needs a form POST — so that URL serves a small self-submitting form which performs the POST for you. This is deliberate: it keeps CCAvenue indistinguishable from any other redirect gateway, so a storefront that already follows clientPayload.redirectUrl (as ours does, keyed on the field's presence rather than the provider id) needs no CCAvenue-specific code.
A client that returns the raw
encRequestfor the browser to post itself was the original design and is why an early build silently skipped the gateway and jumped straight to the success page: the storefront saw noredirectUrl, concluded there was no client action, and confirmed the order locally while it satpending_paymentserver-side.
The shopper is returned to
store.storefront_urls.store_url+storefront_return_path, which defaults to/checkout/ccavenue/return— the storefront's hosted-payment landing route, shared with PhonePe. That URL deliberately carries neither the order id nor the outcome: checkout stashes the order inlocalStoragebefore handing over, and the landing page settles from the stash, calls the verify endpoint, then forwards to/order/success/:idor/order/failure/:id.?order=and?status=are appended for logs and analytics; the page does not read them.
GET /store/orders/:id/ccavenue/redirect is unauthenticated by design — it is a top-level browser navigation from the storefront's origin, where a session cookie scoped to the api is not reliably sent. Exposure is bounded: the order id is an unguessable UUID, the page renders only while the order is still pending_payment, and the worst an attacker holding a valid id can do is pay someone else's order. It is served Cache-Control: no-store.
APP — the official Flutter SDK
place on the APP platform returns exactly the CCAvenueOrder constructor shape used by ccavenue_india_sdk_flutter (verified publisher ccavenue.com, Android + iOS):
{
"provider": "ccavenue",
"encRequest": "0a744ab8dfffb3e5…",
"accessCode": "AVNU55AY03UNYA",
"paymentEnvironment": "uat",
"encryptionMode": "aes128",
"appColor": "#1F46BD",
"fontColor": "#FFFFFF",
"orderId": "ORD-2026-00000123",
"amount": 132195
}final order = CCAvenueOrder(
accessCode: payload.accessCode,
encRequest: payload.encRequest,
paymentEnvironment: payload.paymentEnvironment,
encryptionMode: payload.encryptionMode,
appColor: payload.appColor,
fontColor: payload.fontColor,
);
final response = await CCAvenueSDK().initTransaction(order);
// → { statusCode, statusMessage, data: { orderStatus, accessCode, encResponse } }The encRequest is byte-identical to the web one — there is no separate mobile handshake, no RSA key exchange and no WebView shim. After initTransaction returns, the app calls the verify endpoint.
Note that paymentEnvironment spells the non-live environment "uat", while the settings group stores "test". The mapping happens server-side; clients should use the value as given.
Order identity
order_idis the human order number (ORD-2026-00000123), sanitized to[A-Za-z0-9_/-]and capped at 30 characters, so the MARS dashboard is readable and the Status API is keyed by something meaningful.- Payment retries append
-R<attempt>from the second attempt on (ORD-2026-00000123-R2). This value is also stored as the attempt'sorder_payment.external_reference, so reusing one would make two attempts indistinguishable to reconcile and refund. The first attempt stays unsuffixed, and the suffix budget is subtracted before the 30-character truncation.
- Payment retries append
merchant_param1carries the internal order UUID. This is the only field callbacks are resolved by — CCAvenue states it does not enforce uniqueness onorder_id, so the echoed merchant reference cannot identify an order on its own.- Billing and delivery fields are prefilled so the shopper does not retype what they already gave at checkout. On WEB the redirect endpoint reads the order's own address snapshot (the authoritative copy) plus the customer's email; on APP they come from the place-order
metadata. Either way values pass a fixed allowlist and are cleaned per CCAvenue's per-field character rules — a single malformed parameter fails the entire transaction, so anything unusable is dropped rather than sent. Phone numbers keep their digits (+91 98…→9198…); guest orders simply omit the email. An unrecognised parameter fails the whole CCAvenue transaction rather than being ignored, so unknown keys are dropped.
Settlement
Every surface — the browser return, the notification webhook and the verify endpoint — funnels through one CCAvenuePaymentService.reconcile, so order transitions are identical whichever observes the event first.
Reconcile always re-reads the CCAvenue Status API (orderStatusTracker, version 1.2). The posted callback body and the SDK's encResponse are used only to identify the order; neither is trusted for payment outcome or amount. Before settling, the service pins the callback to the order_payment row written at place time, and compares CCAvenue's captured amount against the order total.
Status mapping
CCAvenue order_status | Verdict |
|---|---|
Successful, Shipped, Success | paid |
Refunded, System refund, Chargeback | paid |
Aborted, Cancelled, Auto-Cancelled, Auto-Reversed, Invalid, Unsuccessful, Failure, Fraud, Timeout | failed |
Initiated, Awaited, anything unrecognised | pending |
Two rows deserve explanation:
Refunded/System refund/Chargebackmap to paid, not failed. The money did reach us; what happened afterwards belongs to the refund ledger, not the order lifecycle. Mapping them to failed would cancel an order that was genuinely captured and possibly already fulfilled.- Unknown maps to pending. A status CCAvenue adds later can never silently cancel an order.
Recovering a lost callback
The provider implements reconcilePending, so the platform's stale-pending sweep asks CCAvenue before cancelling an abandoned order. Without it, a shopper who paid and then lost their connection would have the order cancelled underneath them.
Refunds
Gateway refunds run through the provider-neutral surface on admin/orders — CCAvenue adds no refund endpoints of its own. Requires allow_refunds and an allowlisted server IP.
refundOrder is keyed by CCAvenue's own reference_no (the tracking id), which only exists once the shopper has transacted. Rather than storing a second identifier at place time, refund reads it off the Status API keyed by order_no.
CCAvenue answers synchronously with refund_status 0 (success) or 1 (failure), and ships no refund webhook — so the refund resolves to a terminal COMPLETED or FAILED on the same call.
getRefundStateis deliberately not implemented. CCAvenue'sgetRefundDetailsis keyed byreference_no, not by ourrefund_ref_no, so it structurally cannot answer "what happened to merchant refund X". The port marks the hook optional, and it is only meaningful for providers whose refunds resolve asynchronously.
Operational notes
- The status and refund APIs are IP-allowlisted in MARS. A perfectly correct integration still fails from an unregistered host, and CCAvenue's error text does not say so. The client logs a hint when it sees an access-class rejection.
- The API envelope lies about encryption on failure. When the reply carries
status=1, theenc_responsefield holds a plaintext error message. Decrypting it produces garbage and buries the real reason. - Instant Gratification is assumed. See the admin guide — without it, CCAvenue holds successful payments unconfirmed and auto-cancels them after 12 days.
Related
Order Module
HTTP surface for the order lifecycle — storefront place-order/list/detail/cancel, vendor sub-order fulfillment and delivery, and admin oversight (cancel, mark-paid, mark-refunded).
Payment — PhonePe Provider
PhonePe Standard Checkout v2 provider covering web hosted checkout and the mobile SDK, the customer-side verify endpoint, the credential-verified webhook, and gateway-driven refunds.