Community port of NetPay's official Magento module (which shipped only as a ZIP built for Magento 2.4.6) to Magento Open Source 2.4.8 on PHP 8.4, hardened against NetPay's WooCommerce plugin as the reference implementation.
It adds NetPay card payments with 3-D Secure, saved cards (vault) and OXXO Pay (cash), and is
self-contained — NetPay's PHP SDK is vendored under Sdk/ and autoloaded by registration.php,
so there is no separate netpay/custom Composer dependency. Guzzle is not vendored (Magento ships it).
- Card payments with 3-D Secure (Cardinal/Songbird), frictionless + challenge flows.
- OXXO Pay (cash) with an OXXO-Pay checkout method and a reference/receipt block.
- Saved cards (vault) — save at checkout, manage in My Account, delete; client id scoped per store so cards never leak across stores / NetPay accounts.
- Webhook receiver — OXXO settlement + generic card reconciliation (re-verifies the transaction against the gateway; idempotent).
- Refunds — admin online credit memos refund through NetPay (full refunds).
- Pre-authorization (Check-in / Check-out) — optional mode where checkout only holds the
amount and the merchant captures the real total from the admin invoice (
PostAuth, ±20%); cancelling releases the hold. - Multi-store aware — every config read (keys / mode / gateway host) is scoped to the order's store, including the webhook (which carries no store context) and the charge.
- Friendly error messages — ~60 raw gateway responses mapped to friendly Spanish messages.
- Retry-safe 3-D Secure — sticky Cardinal/Songbird state is cleared between attempts.
- Self-contained SDK, declarative schema,
phpcs --standard=Magento2clean (excludingSdk/).
| Magento Open Source | 2.4.8 (tested on 2.4.8-p5) |
| PHP | 8.1 – 8.4 |
| HTTP client | guzzlehttp/guzzle ^7.5 (provided by Magento) |
| 3-D Secure | Cardinal Commerce (Songbird), sandbox + live |
composer require netpay/module-payment
bin/magento module:enable Netpay_Payment
bin/magento setup:upgrade
bin/magento cache:flushCopy this repository to app/code/Netpay/Payment, then:
bin/magento module:enable Netpay_Payment
bin/magento setup:upgrade
bin/magento cache:flushStores → Configuration → Sales → Payment Methods → NetPay
- Enable the gateway and the credit-card method.
- Set the mode (
test/live) and the matching public/secret keys. - (OXXO) Enable OXXO Pay and register the webhook from the config page button.
All fields are store-scoped (showInStore=1), so each store view can have its own NetPay account.
Config paths (if you script it with bin/magento config:set):
payment/netpay/enable 1
payment/netpay/active 1 # credit-card method
payment/netpay/payment_mode test # or live
payment/netpay/public_key_test pk_...
payment/netpay/secret_key_test sk_...
payment/netpay/public_key_live pk_...
payment/netpay/secret_key_live sk_...
payment/netpay/preauth_mode authorize_capture # or `authorize` (pre-authorization)
The secret key is only ever used server-side (to build the SDK
PaymentManager); it is never exposed to the browser. The checkout config only carries the public key.
Payment Methods → NetPay → NetPay Credit Card → Payment Action:
- Direct sale (charge immediately) — default (
authorize_capture). Unchanged behavior: the charge is captured at checkout and the order is invoiced automatically. - Pre-authorization (hold now, capture on invoice) —
authorize. The checkout only holds the order total (Check-in, sent astransactionType: "PreAuth"on the samePOST v3.5/charges); the order stays in Processing with an open authorization transaction and no invoice.
To charge the real amount (items missing or removed while picking the order): Sales → Orders → View → Invoice, adjust Qty to Invoice (0 for what is missing), set Amount: Capture Online and Submit Invoice → that fires Check-out / PostAuth for the invoice total. NetPay accepts a final amount between −20% and +20% of the held one; outside that range the capture is rejected with an explicit message — cancel the order (releasing the hold) and charge again.
Also in this mode:
- Cancelling the order releases the hold (the cancelation endpoint shared with refunds). An uncaptured hold expires on its own after 5 business days (Visa/Mastercard) or 7 (Amex); capturing after that fails, so cancel and re-charge instead.
- The customer gets the invoice email automatically when the capture succeeds — it lists only the invoiced items and the amount actually charged, i.e. the final receipt.
- The admin order view shows a banner with the state of the hold (held / captured / released / never confirmed) plus the capture instructions.
- Installments (MSI) are disabled: a plan fixed at PreAuth time is incompatible with capturing a different amount later.
- OXXO / cash (
netpaycash) is unaffected. - The NetPay account must have Check-in/Check-out enabled — confirm it with your account manager before going live.
Magento sends no email when an order is cancelled, and its "New Order" email does not mention holds, so two operational steps are recommended: add a line to the order-confirmation template explaining that the amount shown is a hold whose final charge may be lower, and when cancelling a pre-authorized order add a status comment with Notify Customer by Email checked.
The card flow follows NetPay's contract (aligned with NetPay's WooCommerce plugin):
- Tokenize the card client-side (
NetPay.token.create) — the PAN never reaches the server. - Device Data Collection: the checkout generates a Cardinal ReferenceId and blocks the charge until it is ready, then sends it on the charge.
POST /v3.5/charges. If the charge returnsstatus: review:- Challenge (response carries
acsUrl+paReq+authenticationTransactionID): run the Cardinal step-up, then confirm with the returnedprocessorTransactionId. - Frictionless (no
acsUrl): read the real state first (GET /v3/transactions/{id}) and branch:WAIT_THREEDS→ confirm (frictionless confirm sends the literalprocessorTransactionId=null).DONE/CHARGEABLE→ already approved.FAILED/REJECTED/REJECT→ surface a friendly reason and cancel the pending order.
- Challenge (response carries
Decision Manager: a charge can be sent to
reviewand thenFAILEDby NetPay's anti-fraud Decision Manager (responseCode 88, "Enviada a review por DM"). That is an account/gateway-side decision, not a client bug. This module surfaces that state instead of masking it (the rawconfirmreturns HTTP 409 for such transactions).
- Cards are saved via NetPay's client/vault API and shown in My Account → My Credit Cards and in the checkout saved-card selector.
- The link between a Magento customer and their NetPay client id lives in
netpay_customer(customer_id,store_id,netpay_id), unique on(customer_id, store_id). This makes the vault correct in multi-store setups whether stores share one NetPay account or use different ones.
The URL is registered once per NetPay account, not per transaction: the Update in Netpay
button in the admin config (OXXO section) calls POST/PUT /v3/webhooks/ with
{"webhook": "<url>"}, authenticated with the secret key. The charge request does not carry it
(merchantRedirectUrl on the charge is the 3DS browser return, a different thing). Remember to
Save Config afterwards — the button registers in NetPay, it does not persist the Magento value.
The receiver (Controller/Payment/ApiController, route netpay/payment/apicontroller) handles the
same two events as NetPay's WooCommerce plugin, which this module is ported from. Both are
asynchronous payment notifications — a card charge settles inline at checkout, not here. No
notification is trusted: each one is re-verified server-to-server against the gateway.
| Event | Verified against | Settles when | Order located by |
|---|---|---|---|
cep.paid (SPEI/CEP) |
GET /v3/transactions/{data.transactionId} |
status is DONE/CHARGEABLE and transactionTokenId and amount match the notification |
sales_order.token = data.transactionId |
oxxopay.paid |
GET /v3/oxxopay/transaction/{data.transactionId} |
status is C and transactionId and amount match |
sales_order.token = data.reference |
data.merchantRefCode(the order's entity id, sent asbilling.merchantReferenceCodeon the charge) is the fallback locator for both events — the WooCommerce plugin resolves OXXO through it.- Any other event is acknowledged with
200and logged with its name, never guessed at: a non-payment event (a refund, a chargeback) would still read as a settled transaction on the gateway and must not invoice the order. The log is where an unknown event name will surface. - The order's grand total is checked against the notification amount before any gateway call.
- Terminal declines (
FAILED/REJECT) cancel the still-pending order — an addition over the WooCommerce plugin, which leaves it untouched. - Invoicing and the order note are idempotent: a repeated notification does not duplicate either.
- Source IPs are checked against NetPay's ranges, anchored on the real TCP peer.
- Config (keys/host) is scoped to the order's store, not the default store.
Netpay/Payment/
├── registration.php # module + self-contained SDK PSR-4 autoloader
├── composer.json # magento2-module
├── etc/ # di, system.xml, config.xml, webapi, db_schema, routes
├── Api/ Model/ Block/ Controller/ Helper/ Setup/ Logger/ view/
└── Sdk/ # vendored NetPay PHP SDK (Swagger)
├── lib/ # Netpay\Client\ (API + models)
├── businessLayer/ # BusinessLayer\Netpay\ (features, mapping)
└── config/ # SDK .ini config (hosts, feature map)
- The payment charge runs out-of-band via the
POST /V1/payment/chargeswebapi (ChargesApiManagement), not through Magento's payment gateway commands. - Schema is declarative (
etc/db_schema.xml): atokencolumn onsales_orderand thenetpay_customertable.
# Coding standard (excluding the vendored Swagger SDK)
vendor/bin/phpcs --standard=Magento2 --severity=10 --ignore="*/Sdk/*" app/code/Netpay/Payment- Extends the deprecated
Magento\Payment\Model\Method\AbstractMethod(works on 2.4.8; charging is out-of-band). Migration to the Payment Provider Gateway is planned. - Refunds are full only (NetPay's refund endpoint takes no amount); partial refunds are not supported. Capture is online at charge time.
See CHANGELOG.md for the full list of fixes (~40 across 6 audit passes, ~6 fatal).
Licensed under PolyForm Noncommercial 1.0.0 — see LICENSE. In short:
- Free for noncommercial use, with attribution (keep the notice) and please let the author know you're using it (open an issue or contact @lordmacu).
- Commercial / for-profit use requires a separate paid commercial license — contact @lordmacu.
Provenance: this is a community port of NetPay's official Magento module and it bundles NetPay's PHP SDK (
Sdk/). Those underlying components are NetPay's proprietary property and are subject to NetPay's own terms. The license above is granted over the port contributions only — it does not grant rights to NetPay's software; obtain the appropriate authorization from NetPay for your use case. (Not legal advice.)