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.
| 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 Shared 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. 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. No plugin stores the card number.
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 (built 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 |
| Shared 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.
Shared Key is a separate client-level HMAC secret that you generate in the Inovio portal under Dev Tools → Key Manager. One key covers every site on your Inovio client. It is not the API password. 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 3D 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.
Behavior 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.
The plugins do not attempt a module-side settlement fallback. The gateway
decides how to route the refund. If you extend a plugin, do not add one. A
pre-check that reverses on the unsettled path over-refunds: a $10 partial
refund against an unsettled $100 order reverses the full $100 while the store
records $10. And a 536 comes back as DECLINED, not FAILED, so hand-rolled
fallback logic misfires.
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 3D 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.
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 3D Secure challenge |
4000000000002503 |
Triggers a 3D 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.
