Markdown

Shopping cart plugins

Tokenized direct-post card checkout for WooCommerce, Magento 2, PrestaShop 9 and OpenCart 4, on the Inovio gateway.

Inovio ships four first-party cart plugins. They are separate codebases, one per platform, but they are the same integration: the same PHP SDK, the same tokenized direct-post card entry, the same refund contract, and the same five credentials from Inovio.

Note

The plugins are functional pre-release. Every verb documented here has been exercised end to end against the gateway, and each plugin ships a Playwright suite that drives a real browser through the real storefront and admin. They are not yet hardened for production.

Platform Page Package
WooCommerce WooCommerce inovio-payment-gateway
Magento 2 / Mage-OS Magento 2 inovio/module-payment-gateway
PrestaShop 9 PrestaShop inoviopayment
OpenCart 4 OpenCart inovio.ocmod.zip

What they have in common

All four use tokenized direct post. The card number never reaches the store server.

  1. Card fields render in the store's own checkout page, in the merchant's own DOM. There is no hosted payment page and no Inovio-hosted iframe to redirect to.
  2. The plugin's checkout JavaScript asks the store server for an HMAC signature. The store mints it from the Site Key and returns it. The store never sees the card.
  3. The browser POSTs the PAN, expiry and CVV directly to Inovio's token_service.cfm, with that signature in the X-timestamp and X-signature headers, and gets back a single-use TOKEN_GUID.
  4. Only the TOKEN_GUID (plus the brand and last four digits, which are display-only fields) is submitted to the store.
  5. The store server sends the transaction to pmt_service.cfm through the PHP SDK, carrying the token instead of a card number.

The signature covers timestamp + uniqueId + siteId. The PAN is not part of the signed message. The v4.14 PDF says it is, and its worked example agrees; both are wrong. Verified against the gateway: the token service validates the signature without the PAN, and including it yields error 121, "signature match fail". The SDK's Tokenize::signRequest() is the single source of truth. Never hand-roll this hash.

No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to. In each plugin this was verified rather than assumed: a full database dump taken after a live test run contains zero occurrences of the test card number in any representation.

All four consume the same PHP SDK (inovio/gateway-sdk). None of them touch raw REQUEST_ACTION wire fields. Magento requires it through Composer; PrestaShop and OpenCart vendor a verbatim copy into the shipped package, because the SDK is not on Packagist and both platforms install as a self-contained archive through the admin UI with no Composer step.

Capability matrix

Values come from each plugin's own README. A blank behaviour is not implied anywhere: where a platform does something differently, the cell says so.

WooCommerce Magento 2 PrestaShop 9 OpenCart 4
Sale Yes Yes Yes Yes
Authorize only Yes Yes Yes Yes
Capture, full Yes Yes Yes Yes
Capture, partial No, full capture only (WooCommerce has no capture-amount field) Yes, via Magento invoicing Yes Yes
Void Yes Yes Yes Yes
Refund, full Yes Yes Yes Yes
Refund, partial Yes Yes Yes Yes
Saved cards Native WC_Payment_Tokens Magento Vault Module's own table plus a customer-account UI Module's own table, rendered into OpenCart's native account hook
3D Secure Yes, two-token flow Yes, frictionless and challenge Yes, with a module-created pending order state Yes, enrollment / DDC / challenge
Block or modern checkout Both the classic shortcode and the Block Checkout Standard Magento checkout Standard PrestaShop 9 checkout Standard OpenCart 4 checkout
Timeout reconciliation Yes Yes, via status() Yes, via status() Yes, via CCSTATUS

Three details the matrix flattens:

  • WooCommerce saved cards are checkout-type dependent. A shopper can use an existing saved card on either the classic checkout or the Block Checkout, but can only save a new card on the classic checkout. See WooCommerce known gaps.
  • Magento does not hide itself on incomplete credentials. WooCommerce, PrestaShop and OpenCart all withhold the payment method at checkout until all five required fields are filled in. Magento will show the method and then fail.
  • Wallets are out of scope in all four. Apple Pay and Google Pay are gateway features (see Apple Pay and Google Pay) but are not wired into any cart plugin yet, and neither are LatAm rails or raw-card admin MOTO orders.

Requirements

WooCommerce Magento 2 PrestaShop 9 OpenCart 4
Platform WordPress 6.0+, WooCommerce 7.0+ (Block Checkout needs 8.3+) Magento 2.4.4+, or Mage-OS PrestaShop 9.0+ OpenCart 4.1.x, verified against 4.1.0.4
PHP 8.0+ 8.1+ 8.1+ 8.1+
Extensions bcmath, curl, json bcmath, curl, json bcmath, curl, json bcmath, curl

bcmath is required, not optional, in every one of them. The SDK's Money type does every decimal calculation through it so amounts never touch a binary float. Installation fails without it. Magento 2.4 requires bcmath in its own right, so a compliant Magento install already has it; OpenCart's settings screen refuses to enable the payment method without it.

PrestaShop is 9.x only. PrestaShop 8.0 through 8.2 supports PHP 7.2.5 through 8.1 and no higher, while 9.x requires 8.1+; the two lines overlap at exactly PHP 8.1 and maintain separate documentation branches. PrestaShop 1.7 is end of life.

What you need from Inovio

The same five values on every platform.

Credential Gateway field Required
API username REQ_USERNAME Yes
API password REQ_PASSWORD Yes
Site ID SITE_ID Yes
Site Key (HMAC signing secret, not sent as a field) Yes
Gateway Product ID LI_PROD_ID Yes
Merchant Account ID MERCH_ACCT_ID No

The five credentials explained

API username and API password are your gateway API credentials, sent as REQ_USERNAME and REQ_PASSWORD on every transaction. See Authentication.

Site ID is SITE_ID, the gateway site the transactions belong to.

Site Key is a separate per-site HMAC secret issued by Inovio support. It is not the API password, and it is not something you can generate. It is used for exactly one thing: signing the browser's tokenization request. Without it the browser cannot tokenize and checkout fails with error 121, "Get CCtoken GUID signature match fail". This is the single most common misconfiguration, and the symptom is a card form that never produces a token rather than an obvious credential error.

Gateway Product ID is LI_PROD_ID, a product registered on the gateway. It is not a SKU from your store catalogue. The whole order bills as one line item under it, regardless of how many products the cart contains. See Line items.

Merchant Account ID is MERCH_ACCT_ID and is optional in all four plugins. Leave it empty to let the gateway distribute by currency and country.

Every plugin also exposes a gateway endpoint setting. In all four, the tokenization endpoint (token_service.cfm) and the 3-D Secure endpoint (3dsrequest.cfm) are derived from the pmt_service.cfm URL by suffix rewrite, exactly as the SDK derives them, so the three cannot drift apart. There is one setting, not three. Leave it at the default for production.

Refunds are settlement-aware

This is a shared design across all four plugins, and it is the one place where the obvious implementation loses money.

The gateway rejects a credit (CCCREDIT) against an order that has not settled yet with SERVICE 536, "Order not settled: Please reverse". Before settlement the correct undo verb is a reversal (CCREVERSECAP), not a credit. A naive refund-only button therefore fails on every same-day refund.

The plugins do not pre-check settlement and pick a verb themselves. The gateway owns that decision:

Full refund issues reverseCapture($ref, creditOnFail: true), which sends CCREVERSECAP with CREDIT_ON_FAIL=1. When the transaction is already settled and cannot be reversed, the gateway itself re-routes to CCCREDIT. One call, one round trip, and the plugin never has to know the settlement state. This is documented in v4.14 §5.4.2, confirmed in the gateway's own order handling, and used by the gateway internally on system auto-voids.

Verified live on both paths:

Case Request Response
Unsettled capture CCREVERSECAP with CREDIT_ON_FAIL=1 REQUEST_ACTION=CCREVERSECAP, APPROVED
Settled capture CCREVERSECAP with CREDIT_ON_FAIL=1 REQUEST_ACTION=CCCREDIT, APPROVED
Control: settled, no flag CCREVERSECAP SERVICE 515, "Order fully credited"

Partial refund issues refund($ref, $money), which is always CCCREDIT, because a reversal takes no amount and voids the entire capture. Against an unsettled order the gateway returns 536, and the plugin fails loudly: "order not settled, partial refunds available after settlement". The merchant sees the error, no money moves, and the refund is retried after settlement.

There is no module-side fallback, and you must not add one

Every plugin previously carried a settlement pre-check and a 536 fallback branch. Both are deleted. The pre-check dropped the requested amount on the unsettled path, so a partial refund of $10 against an unsettled $100 order reversed the full $100 while the store recorded $10. The fallback branch was dead code: it tested for status FAILED, but a real 536 arrives as DECLINED. If you are extending a plugin, let the gateway route the verb.

See Reversal and Credit for the underlying request types, and service codes for 536.

The statement descriptor warning

The statement descriptor must not contain a space, underscore or slash

The gateway rejects the entire transaction with Invalid Data if PMT_DESCRIPTOR contains a space, an underscore or a forward slash. Every sale fails, not just the descriptor.

Descriptor Result
ACME STORE Rejected
ACME_STORE Rejected
ACME/STORE Rejected
ACME-STORE Accepted
ACMESTORE Accepted
ACME.STORE Accepted

The full allowed set is A-Z, a-z, 0-9, and . - * + & @. This was mapped empirically by sending each candidate character in an otherwise-identical approved request. A multi-word descriptor is the natural thing for a merchant to type, and it kills every sale. The SDK now rejects an invalid descriptor rather than letting the gateway kill the sale. If you are unsure, leave the field empty.

Two-token 3D Secure

When 3-D Secure is enabled, checkout mints two TOKEN_GUIDs from the same card entry. Gateway tokens are single-use and the enrollment leg consumes one, so token A drives enrollment and token B completes the transaction after the cardholder finishes the challenge. Both resolve to the same card at the gateway, so the authentication carries across the two legs.

Reusing token A on the completion leg fails with API 401 Invalid TOKEN_GUID, verified against the live gateway. This is the most tempting thing in any of the four codebases to collapse into one token, and doing so produces the worst possible failure mode: it appears to work for every frictionless card, which never reaches the completion leg at all, and fails only for cards that are actually challenged.

See 3-D Secure through the gateway.

Testing

The same test cards and decline triggers apply on all four platforms, against the Inovio test gateway only.

Card Behaviour
4111111111111111 Approves, no 3-D Secure challenge
4000000000002503 Triggers a 3-D Secure challenge. Sandbox OTP 1234

Any future expiry date and any CVV are accepted.

The sandbox decides declines from the whole order total, matched exactly. The PAN, expiry and CVV are not consulted at all.

Order total Result
6.35 Declined, Insufficient Funds
5.06 Declined, Fraud

The total must land on the trigger amount exactly, including tax and shipping, not the product price. This is also why a test order can decline unexpectedly: check the total before assuming the card or the credentials are at fault.

See Testing.

Source

All four repositories are private. They are available from Inovio.