sol-parts / payum-hutko
Payum gateway for Hutko acquiring (Fondy-compatible protocol): hosted checkout, preauth hold payments and refunds
Requires
- php: >=8.2
- ext-json: *
- ext-mbstring: *
- payum/core: ^1.7
- php-http/discovery: ^1.14
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- nyholm/psr7: ^1.8
- phpunit/phpunit: ^11.5
Suggests
- sol-parts/payum-contracts: Dispatch the shared DoCapture request to settle (capture) preauth hold payments
README
Provides Hutko acquiring integration for Payum: hosted checkout with instant debit or two-stage preauth hold payments, and refunds. Hutko speaks the Fondy-compatible protocol 1.0 (JSON, SHA1 merchant signature).
Supported operations:
Capture— create a payment (/api/checkout/url/) and redirect the customer to the Hutko payment page (checkout_url).Authorize— same flow withpreauth=Y: the funds are locked on the customer's card and the store settles the payment later.DoCapture(the shared request fromsol-parts/payum-contracts, when installed) — settle a preauth hold via/api/capture/order_id/, full or partial amount.Cancel— reversal of an active preauth hold via/api/reverse/order_id/. For anything else it is a no-op: a captured payment is returned byRefund, and a not-yet-paid order simply expires on its own.Refund— refund of a paid order via/api/reverse/order_id/, full transaction amount.Notify— the postback (server_callback_url) is treated as a sync trigger only: the state is always re-fetched server-to-server, so a forged callback can at most trigger an extra sync. Hutko considers a postback delivered only on HTTP 200 and retries otherwise — the action always answers 200.Sync— re-fetch the order state via/api/status/order_id/.GetStatus— maps Hutko order statuses to Payum ones (see the table below).
Payment flow
sequenceDiagram
autonumber
actor C as Customer
participant S as Store
participant H as Hutko
C->>S: Places an order
S->>H: POST /api/checkout/url/ (amount, redirect and callback URLs)
H-->>S: checkout_url
S-->>C: Redirect to checkout_url
C->>H: Pays (card, Apple/Google Pay)
H-->>S: Returns the customer to response_url
H->>S: Postback to server_callback_url
S->>H: POST /api/status/order_id/
S-->>C: Order result page
Note over S,H: The postback body is never trusted as state.<br/>The s2s status call is the single source of truth.
Loading
Authorize creates a preauth payment: the money is locked instead of
charged until the store settles or releases it:
sequenceDiagram
autonumber
actor C as Customer
participant S as Store
participant H as Hutko
C->>H: Confirms the payment
H->>S: order_status approved (preauth) — authorized
Note over S: Goods shipped, or the order is dropped
S->>H: DoCapture — /api/capture, or Cancel — /api/reverse
H-->>S: approved (captured) or reversed
Loading
While an order is still payable (created / processing) the capture flow
does not create a new payment: the returning customer is redirected back to
the same Hutko payment page (checkout_url) until the order is paid or
expires.
Status mapping
| Order status | Payum status |
|---|---|
created, processing |
pending |
approved with an unsettled preauth |
authorized — the funds are locked, the store settles via DoCapture |
approved |
captured |
reversed |
refunded — covers both a released hold and a refunded payment; the host application distinguishes them by the previous transaction state |
declined |
failed |
expired |
expired |
Hutko has no separate order status for a settled hold — both a successful
preauth and its capture report approved. The gateway keeps the stage in
the payment details (preauth, captured) and resolves the ambiguity
locally.
Installation
composer require sol-parts/payum-hutko
Usage with PayumBuilder
use Payum\Core\GatewayFactoryInterface; use Payum\Core\PayumBuilder; use SolParts\PayumHutko\HutkoGatewayFactory; $payum = (new PayumBuilder()) ->addGatewayFactory('hutko', static function (array $config, GatewayFactoryInterface $coreGatewayFactory) { return new HutkoGatewayFactory($config, $coreGatewayFactory); }) ->addGateway('hutko', [ 'factory' => 'hutko', 'merchant_id' => 'your-merchant-id', 'password' => 'your-merchant-password', ]) ->getPayum();
Usage with Symfony PayumBundle
Register the gateway factory:
# config/services.yaml services: app.hutko_gateway_factory: class: Payum\Core\Bridge\Symfony\Builder\GatewayFactoryBuilder arguments: [SolParts\PayumHutko\HutkoGatewayFactory] tags: - { name: payum.gateway_factory_builder, factory: hutko }
Configure a gateway:
# config/packages/payum.yaml payum: gateways: hutko: factory: hutko merchant_id: '%env(HUTKO_MERCHANT_ID)%' password: '%env(HUTKO_PASSWORD)%'
Options
| Option | Required | Description |
|---|---|---|
merchant_id |
yes | Merchant identifier issued by Hutko. |
password |
yes | Merchant password. Every request is signed with SHA1 over the pipe-joined non-empty request values, sorted by key, with the password prepended. |
There is no sandbox option: Hutko serves live and test traffic on the same
endpoint, and testing is done under the dedicated test merchant
(1700002 / password test in the official documentation).
Payment details
ConvertPaymentAction fills amount — in minor units (kopecks), as the
Fondy protocol expects — along with currency, order_desc and order_id
taken from the payment.
The gateway attaches response_url to the long-lived after-URL token and
server_callback_url to a notify token, so both survive repeated visits.
Caveats
- All amounts are in minor units (kopecks), including the partial
amountofDoCaptureand thereversecalls. approvedis ambiguous: Hutko reports it both for a successful preauth hold and for its settlement. The gateway tracks the stage with thepreauthandcapturedpayment details — do not strip them when manipulating the details externally./api/reverse/order_id/is a single endpoint for both reversal of a hold and refund of a captured payment. The gateway always reverses the full transaction amount; partial refunds are not implemented.- The postback body is deliberately not parsed and its signature is not
verified: the state is always re-fetched via
/api/status/order_id/under the merchant credentials. Answer anything but HTTP 200 and Hutko keeps redelivering the postback. - A
checkout/urlcall answered with HTTP 200 but a non-successresponse_statusis reported as an exception carrying the gateway error code and message together with the endpoint URL. - Error responses are reported with the HTTP status code and a truncated body snippet — balancer errors (429, 5xx) answer with HTML, not JSON.
License
Released under the MIT License.