Skip to content
GatewayChanger
In build PrestaShop 8.0 – 9.1 · PHP 8.1+

GatewayChanger for PrestaShop

One module, both roles. A PrestaShop store can sell (route its checkout to a partner shop that charges the card) and charge (accept routed payments with its own processor accounts) — often both at the same time. It speaks the same protocol as the WooCommerce plugin, with the same field names and error codes, so a PrestaShop store can sell to a WooCommerce charger and the other way round.

Status: in build — not yet verified against a live store (2026-09-22). We are writing this module now and targeting version 1.0.0. It has not run on a production PrestaShop, there is no zip on the Plugins page yet, and it has not been through the end-to-end conformance suite that the WooCommerce and Magento plugins pass. This page is the build target — published early so you can see exactly what you would be getting and tell us now if it does not fit your store. It is not a release note. We would rather show you a date than a checkmark we cannot back: when the first build passes the gate, the date and the version appear here and in the changelog.

Requirements

PrestaShop 8.0 – 9.1, one codebase (ps_versions_compliancy 8.0.0 – 9.99.99)
PHP 8.1 or newer with ext-sodium; developed on 8.4
Checkout The standard PrestaShop checkout, through the paymentOptions hook. Embedded and redirect modes both.
Composer None. The module ships zero Composer runtime dependencies — nothing to conflict with your other modules.
Database Four of its own tables (ps_gatewaychanger_session, _nonce, _callback, _event). No core table is altered.
Cron A system cron every five minutes calling the module's token-protected cron controller — it sends the heartbeat.
Transport HTTPS on both stores, and the module's front controllers reachable from the internet without a redirect.

Install

  1. Download gatewaychanger.zip from Plugins in your GatewayChanger dashboard. The card shows the version and its SHA-256 checksum. (No package yet — see the status note above.)
  2. Modules → Module Manager → Upload a module, drop the zip in, and wait for PrestaShop to report it installed.
  3. Press Configure on the GatewayChanger card.

If you deploy by file copy instead, put the gatewaychanger/ directory into your shop's modules/ folder and then install it from the Module Manager — the install step is what creates the tables and the hooks, so the files alone do nothing.

Installing creates the four tables, registers the hooks, and adds two order states (Awaiting GatewayChanger payment and Paid by a partner shop) so a routed order never has to borrow a state that means something else in your reports. Uninstalling drops the tables and soft-deletes the states, which keeps historical orders readable.

Connect the shop

Register the shop in the dashboard first (Shops → Add shop, platform PrestaShop), then in the module's Connection panel:

  1. Leave the API base at https://gatewaychanger.com/api/v1 unless you were told otherwise.
  2. Paste the shop key from the dashboard — shk_test_… in the test environment, shk_live_… in live.
  3. Press Connect.

The module generates an Ed25519 key pair (the private key never leaves your server), registers the public key, and reports its roles and capabilities. You then see the shop name, a TEST MODE or LIVE badge, the verification status and the time of the last heartbeat.

The module's own REST surface lives on its api front controller, and we call it back at https://your-store.example/module/gatewaychanger/api/capabilities to verify the shop. Until that succeeds the shop is pending_verification and cannot be a charger. The probe does not follow redirects, so the base URL you report has to be the final one — no apex↔www hop, no http→https hop. If it fails, check for a WAF, Basic Auth, or a mismatch between the configured base URL and the store's real address, then press Re-verify.

Key rotation generates a new pair and keeps the previous public key valid for seven days, so in-flight sessions do not break.

Everything else is configured in the dashboard

The module's own screen is only about the connection. Roles, checkout texts, charger settings and payment processors live in the GatewayChanger dashboard (Shops → your shop), and the module pulls them: we ping it whenever you save, it fetches GET /api/v1/shops/self/config, applies the document atomically, tests every enabled processor and reports back. The configuration page shows the applied settings read-only with a Managed in the GatewayChanger dashboard note.

The shop detail page shows the state of that loop under Connection & plugin: the version you saved, the version the module applied, the last sync ping and its result, a Sync now button, and any errors the module reported.

Seller role

Turn on the seller role and the seller settings in the dashboard (Seller settings tab).

Setting Notes
Title / description What the customer sees in the payment list — for example "Credit / debit card".
Consent label Defaults to the template we send; keep who charges, on whose behalf, and the amount.
Terms page The CMS page linked from the consent line. It needs the payment-processing clause.
Payment mode embed (recommended — the customer stays on your checkout) or redirect.
When no route is available Hide the payment option, or charge on this shop itself.
Order state for authorizations Usually Awaiting GatewayChanger payment until the charge is captured.

At checkout the module renders the payment option, creates a session, shows the consent checkbox and the charger's iframe, re-routes on finalize if the total changed, and only then calls validateOrder()with the amount the charger actually took, never the recomputed cart total. One cart can produce several orders (one per shipping package), so the module works from the order reference and writes the routing on every order in it. The order's private message records the decision:

Charged €24.99 via Shop B Ltd (Stripe) — ref pi_3Q1… — routed by rule "EU cards over €50" (decision dec_01J8…)

Refunds started as a credit slip (Orders → the order → Standard refund / Partial refund) are forwarded to the charger, which refunds at the processor and reports the event back.

Charger role

In the dashboard, open the Charger & processors tab: turn on the charger role, set the legal name exactly as it should appear in the consent sentence and on receipts, then add at least one processor.

Credentials are end-to-end encrypted. On activation the module generates an X25519 key pair alongside its signing key and reports the public half. When you type a secret in the dashboard, your browser seals it to that key (crypto_box_seal) and only the ciphertext — sb1.<fingerprint>.<…> — is stored and synced. The module opens it on your server and keeps it in PrestaShop's Configuration under a second layer of sodium_crypto_secretbox, keyed to the shop's own cookie key. The API rejects any credential that is not sealed, and the value is never shown again.

Test processor

No credentials. Deterministic outcomes by card number — see test cards. Use it for the first end-to-end run and for continuous integration; it is free and never talks to a third party.

Stripe

  1. In the Stripe dashboard, switch to test mode and open Developers → API keys.
  2. In the GatewayChanger dashboard, Charger & processors → Add a processor → Stripe, and paste the publishable key (pk_test_…) and the secret key (sk_test_…).
  3. Add a webhook endpoint pointing at the URL the module's configuration page prints for you (it is served by the same api front controller), subscribe to payment_intent.*, charge.refunded and charge.dispute.*, and paste the signing secret (whsec_…) into the same dialog.
  4. Choose the capture method (automatic or manual) and, optionally, a statement descriptor suffix.
  5. Repeat with live keys when you go live — and only then.

Both keys are encrypted in your browser before they are sent and can only be decrypted inside your PrestaShop installation. GatewayChanger never receives them in readable form, never proxies a call to Stripe, and has no way to charge on your behalf. After you save, the module tests the keys and the dashboard shows connected or the exact error — and routing skips a processor that is not connected.

Mirror orders

Every charge creates an order in your own store with a single line — "Payment on behalf of {seller} order {number}" — so your bookkeeping, tax reporting and credit-slip workflow keep working the way they already do. The seller's order reference and the decision id are stored on it.

Testing the whole flow

  1. Connect two stores, one seller and one charger, both in the test environment.
  2. Enable the test processor at the charger.
  3. Create one catch-all rule pointing at the charger, with "seller charges itself" as a second candidate.
  4. Check out with 4111 1111 1111 1111 → captured.
  5. Retry with 4000 0000 0000 0002 → declined; the iframe resets and asks for another card, and no failover happens.
  6. Set the cart total to exactly 44.00 → processor_error; the seller re-routes with the charger excluded and the second candidate takes it.
  7. Refund half the order with a credit slip and watch the refund appear on the mirror order and in the ledger.

Run step 4 once with maintenance mode on as well. A shop in maintenance must still answer the charger API — that is the failure nobody catches in development.

Troubleshooting

The payment option does not appear. Four independent switches: the module installed, the shop connected, the seller role on, and a candidate that supports the cart currency. Check Payment → Preferences too — PrestaShop can restrict a payment module by currency, country and carrier, and a fresh install restricts nothing until you say so.

The API URL returns your theme, or a 404. The charger controllers run theme-less and bypass the canonical and SSL redirects, but a friendly-URL rewrite that does not reach index.php, a security module, or a redirect on the host will still break them. Fetch …/api/capabilities with curl and confirm you get JSON, not HTML.

"Session expired" at checkout. Grants live 30 minutes. A checkout left open longer needs a new session; reloading the payment step is enough.

The iframe is blank. The embed page restricts frame-ancestors to the seller origin. Mismatched www/apex or http/https between the configured base URL and the page the customer is on will block it.

Two orders for one cart. Cart::OrderExists() is a check, not a lock, so a browser return and a callback can race. The module takes a MySQL lock around the validate step; if you see a duplicate, send us the order reference and the decision id — that is a bug we want.

Heartbeats stop. The system cron is not calling the cron controller. PrestaShop has no scheduler of its own. Fifteen minutes without a heartbeat removes the shop from routing.

Where this stands

  • Written and reviewed against the PrestaShop 8/9 payment-module contract, the marketplace validator checklist and the same protocol document the other plugins implement.
  • Not yet installed on a live store, not yet through the four-part conformance gate: the independent signing client, the unit tests, a cross-platform end-to-end run (PrestaShop as seller against a WooCommerce charger, then the reverse), and a field-by-field diff of GET /capabilities against the WooCommerce document.
  • Out of scope for 1.0.0: self-update from the dashboard, and the dashboard's SSH auto-installer. The first release is download-and-upload.
  • If you run PrestaShop and want to be in the first round of real installs, mail [email protected] — we would rather find the surprises with you than after you.

Plugins marked “In build” are published here only once a real test store has routed a real order end to end.


Something wrong or missing on this page? Tell us and we will fix it.