Supercommerce API Docs
Full Module Docs

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 via CCAvenuePaymentModule.forRoot() in apps/api/src/app.module.ts).

Registers a PaymentProvider with id ccavenue into the platform PaymentRegistry. 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, and ccavenue drops 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:

CCAvenueRazorpay / PhonePe
TransportAES-128-CBC encrypted key=value& stringsJSON over HTTPS
AuthA shared working key, no per-request signatureOAuth token / HMAC signature
Checkout handoffAn HTTP form POST to the billing pageA GET redirect or an SDK call
AmountsDecimal rupees ("1321.95")Integer subunits
RefundsSynchronous — success or failure on the same callAsynchronous, 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.

KeyRequiredPurpose
merchant_idNumeric merchant identifier.
access_codeSent with every billing-page request and handed to the Flutter SDK — not a secret.
working_keyEncrypts requests and authenticates callbacks. Secret.
api_access_codeAccess code for the status/refund API. Blank reuses access_code.
api_working_keyWorking key paired with the above. Secret. Blank reuses working_key.
environmenttest (default) or production. Selects both host pairs.
currencyINR (default), USD, SGD, GBP or EUR. Must match the store's own currency — the amount is never converted.
response_base_urlWEB onlyPublic origin of this API. The redirect and cancel URLs are built from it.
storefront_return_pathPath on the storefront the shopper lands on after settlement. Defaults to /orders.
allow_refundsMaster switch for gateway refunds. Off by default.
sdk_app_color, sdk_font_colorFlutter 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

EnvironmentBilling pageServer-to-server API
testtest.ccavenue.comapitest.ccavenue.com
productionsecure.ccavenue.comapi.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 output

The 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 encRequest for 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 no redirectUrl, concluded there was no client action, and confirmed the order locally while it sat pending_payment server-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 in localStorage before handing over, and the landing page settles from the stash, calls the verify endpoint, then forwards to /order/success/:id or /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_id is 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's order_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.
  • merchant_param1 carries the internal order UUID. This is the only field callbacks are resolved by — CCAvenue states it does not enforce uniqueness on order_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_statusVerdict
Successful, Shipped, Successpaid
Refunded, System refund, Chargebackpaid
Aborted, Cancelled, Auto-Cancelled, Auto-Reversed, Invalid, Unsuccessful, Failure, Fraud, Timeoutfailed
Initiated, Awaited, anything unrecognisedpending

Two rows deserve explanation:

  • Refunded / System refund / Chargeback map 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.

getRefundState is deliberately not implemented. CCAvenue's getRefundDetails is keyed by reference_no, not by our refund_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, the enc_response field 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.

On this page