29 Sept 2026 · 4 min read
The payment gateway API: command pools and value handlers
How the Gateway CommandPoolInterface and value handlers turn one authorize call into a vault-ready payment flow
Part 3 of the Checkout & Payments series — one gateway API, many processors behind it.
Every PSP speaks differently, checkout should not
Authorize, capture, void, refund, sale. Five verbs the checkout needs, dozens of dialects the payment providers speak. Without an abstraction, each new provider rewrites the checkout integration. Magento answers with the payment gateway API: a command pool that maps those five verbs to provider-specific command classes, plus value handlers that resolve per-store configuration at runtime. Learn this layer once and adding a provider becomes wiring, not surgery.
Why the abstraction matters
Payment code fails in specific ways. Credentials differ per website, sandbox and production need different endpoints, vault tokens need different payloads than raw cards, and a refund months later must reuse the original transaction reference. Scattering that logic across observers and helpers produces a module where changing the sandbox URL breaks refunds.
The gateway API separates three concerns. Commands encapsulate one provider call. The command pool routes a verb like authorize to the right command. Value handlers resolve values like merchant ID, API key, or vault usage from scope configuration at call time. The checkout calls verbs; the pool and handlers translate.
How it works
The contract is Magento\Payment\Gateway\Command\CommandPoolInterface with a single method: get($commandCode) returns the command for verbs such as authorize, capture, sale, void, refund, or vault_authorize. The pool is configured in DI:
<virtualType name="MyPayCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
<arguments>
<argument name="commands" xsi:type="array">
<item name="authorize" xsi:type="string">MyVendor\MyPay\Gateway\Command\AuthorizeCommand</item>
<item name="capture" xsi:type="string">MyVendor\MyPay\Gateway\Command\CaptureCommand</item>
<item name="refund" xsi:type="string">MyVendor\MyPay\Gateway\Command\RefundCommand</item>
<item name="vault_authorize" xsi:type="string">MyVendor\MyPay\Gateway\Command\VaultAuthorizeCommand</item>
</argument>
</arguments>
</virtualType>
Each command receives a payment data object wrapping order, payment, and amount, plus a ConfigInterface value handler for scoped reads. A typical authorize command builds the provider request, calls the client, then maps the response into transaction data with transaction_id, is_transaction_closed, and parent linkage. Capture and refund reuse the stored parent transaction ID rather than asking the caller to remember it.
Value handlers deserve attention because they are where multi-store breaks. Instead of reading scope config directly, commands call $this->config->getValue('merchant_id', $storeId), where the config class implements Magento\Payment\Gateway\ConfigInterface and prefixes the active method code. Sandbox flags, endpoint URLs, and vault enablement all flow through this path, so one command class serves every website with different credentials.
Vault is a second pool layered on the first. Magento\Vault\Model\PaymentTokenManagement stores the provider token after a successful authorize with vault consent, and vault_authorize charges the token instead of the card. The checkout payment renderer asks the vault component whether a stored token exists; if yes, the pool routes to the vault command. Test both paths explicitly: raw authorize, vault store, vault authorize, and vault refund each exercise different commands.
Response handling closes the loop. Magento\Payment\Gateway\Response\HandlerChain runs per-command handlers that set additional_information, update cc_last_4 and expiry, and mark is_transaction_pending for asynchronous providers. A missing handler does not fail the payment; it silently drops the metadata the admin refund flow needs three months later.
Pitfalls
- Calling the provider client directly from the payment method model. It bypasses the pool, duplicates credential logic, and makes vault impossible. Keep the method model thin; put every provider call in a command.
- Hardcoding credentials or endpoints. Sandbox works, production fails, and nobody knows why until the first real charge. Every provider value belongs behind a value handler with website scope.
- Forgetting parent transaction linkage. Capture and refund need the original
transaction_idas parent. Commands that generate a fresh ID withoutsetParentTransactionId()create orphan transactions the invoice and credit memo grids cannot reconcile. - Treating vault as a flag instead of a flow. Enabling vault in config without wiring
vault_authorizeand the token UI means consent is collected and never used, or cards are tokenized but charged raw every time. Implement the full quartet: store, list, charge, refund. - Swallowing gateway errors into generic messages. Mapping every provider decline to "payment failed" hides 3-D Secure challenges, soft declines, and fraud holds that need distinct handling. Preserve the provider result code in
additional_informationand map only the display message.
Field check on a live store
Pick a failed capture. Load the order payment additional_information and transaction ID: a missing parent ID points at the authorize handler, a present but wrong one points at capture. Then run the same verb against sandbox with logging on the command input subject; the subject keys (amount, customer, store ID) reveal whether the pool passed scoped config or hardcoded defaults. On one integration, refunds failed only for one website because the value handler read default scope while the merchant ID differed per site. Scoping the handler fixed every refund without touching a command.
Closing
Commands do one provider call well, the pool routes verbs to commands, value handlers supply scoped truth, and vault reuses tokens through the same verbs. Keep provider knowledge inside commands and checkout code stays provider agnostic. The hardest provider interaction, though, is not authorize or capture. It is the redirect to 3-D Secure, where the customer leaves checkout and SCA decides whether they come back.
Next in this series:
magento2-payments-3ds-sca