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.
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 | inovio-payment-gateway |
|
| Magento 2 | inovio/module-payment-gateway |
|
| PrestaShop | inoviopayment |
|
| OpenCart | inovio.ocmod.zip |
What they have in common
All four use tokenized direct post. The card number never reaches the store server.
- 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.
- 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.
- The browser POSTs the PAN, expiry and CVV directly to Inovio's
token_service.cfm, with that signature in theX-timestampandX-signatureheaders, and gets back a single-useTOKEN_GUID. - Only the
TOKEN_GUID(plus the brand and last four digits, which are display-only fields) is submitted to the store. - The store server sends the transaction to
pmt_service.cfmthrough 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.
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 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.
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.
