Sylius processes every payment through Payum, the abstraction layer that converts a Sylius order into gateway-specific API calls and then interprets the asynchronous notification each provider sends back. When a customer clicks "Pay", Sylius creates a payment in a new state, redirects the browser to the gateway (or posts an API request for off-site capture), and waits for two signals: the shopper returning to a captureToken URL, and the gateway POSTing to a notifyToken webhook URL. If either leg breaks, the order sits in "Payment: processing" or "authorized" indefinitely even though money may have changed hands. On shared hosting the breakage almost always lives in one of four places: outbound HTTPS from PHP to the gateway, the public reachability of the notify URL, incorrect or sandbox API credentials, or a base URL that Sylius builds wrong because it cannot see the real HTTPS scheme behind the LiteSpeed proxy.

How Payum tokens and callbacks actually flow

Understanding the token lifecycle prevents hours of guessing. Payum stores short-lived tokens in your database (the sylius_payment and payum_token tables) and embeds them in two generated URLs. The capture URL looks like /payment/capture/{hash} and is where the customer is sent back after approving a card or wallet payment. The notify URL looks like /payment/notify/{hash} or /payment/notify/unsafe/{gatewayName} and is the server-to-server webhook the gateway calls independently of the browser. Both URLs are absolute, meaning Payum bakes the full scheme and host into them at creation time. If Sylius thinks it is running on http:// or on the wrong hostname, it will hand the gateway a callback address that the gateway either refuses (mixed content, non-HTTPS webhook) or cannot resolve.

The host and scheme come from Symfony's request context. On LiteSpeed shared hosting your site terminates TLS at the server and passes the request to PHP, so PHP's $_SERVER['HTTPS'] is usually populated correctly, but the canonical base URL Sylius uses for CLI-generated and queued URLs comes from configuration. Check that your production environment file has the right values. Open config/packages/ and your project root .env.local through cPanel File Manager (set it to show hidden dotfiles via Settings) and confirm these keys resolve to your live domain over HTTPS:

# .env.local in the Sylius project root
APP_ENV=prod
APP_SECRET=your-existing-secret
# Used by Symfony router when building absolute URLs outside a web request
DEFAULT_URI=https://shop.example.com
# Database / messenger left intact
DATABASE_URL=mysql://user:pass@127.0.0.1:3306/db

In newer Sylius/Symfony versions the router default is read from framework.router.default_uri. If callback URLs are being emitted as http://, confirm your document root .htaccess forwards the HTTPS flag so Symfony trusts the scheme. Because you cannot touch trusted-proxy settings in /etc/, do it at the application edge instead:

# public/.htaccess (or the domain root .htaccess) — normalize HTTPS awareness
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# Make sure the HTTPS variable is passed to PHP-FPM/LSAPI
SetEnvIf X-Forwarded-Proto https HTTPS=on

After editing environment configuration, clear the compiled cache so the new base URI takes effect. You do not need SSH for a simple cache drop on shared hosting: delete the contents of var/cache/prod/ through File Manager, which forces Sylius to rebuild on the next request. If your plan permits scheduled or terminal commands via the cPanel "Terminal" feature you may run php bin/console cache:clear --env=prod, but manual deletion is equivalent for configuration changes.

Diagnosing outbound cURL and TLS handshake failures

Off-site gateways such as Stripe, PayPal, Mollie, and Braintree require Sylius to open an outbound HTTPS connection to their API endpoints during capture and refund. On shared hosting these calls fail for two recurring reasons: the PHP build is missing a current CA bundle, or outbound ports are filtered. The symptom in the Sylius logs is a Payum\Core\Reply\HttpResponse exception or a cURL error like cURL error 60: SSL certificate problem: unable to get local issuer certificate or cURL error 7: couldn't connect to host. Find these in var/log/prod.log inside your Sylius installation, not in the server-wide logs you cannot read.

First confirm the PHP extensions the gateway client needs are active. In cPanel open Select PHP Version (the PHP Selector) and verify curl, openssl, intl, and json are ticked under Extensions. In DirectAdmin this lives under PHP Selector > Extensions. Error 60 means cURL cannot verify the gateway's certificate because the CA bundle path is empty or stale. You cannot edit the server php.ini, but you can point PHP at a bundle you control. Download the current Mozilla CA bundle (cacert.pem) into a private folder outside the web root, then set the path in .user.ini, which CloudLinux honors per-account:

; .user.ini in the Sylius project public root
curl.cainfo="/home/username/certs/cacert.pem"
openssl.cafile="/home/username/certs/cacert.pem"

; Give slow gateway handshakes room without hanging the site
max_execution_time=120
default_socket_timeout=60

Changes to .user.ini are cached for the duration of user_ini.cache_ttl (commonly 300 seconds), so wait a few minutes or touch the file again before retesting. Error 7 or a connection timeout points to egress filtering: some shared plans restrict outbound 443 to approved hosts. This is not something an unprivileged user can override through configuration, so open a ticket with Hostiso support listing the exact gateway hostnames (for example api.stripe.com, api.mollie.com, api-m.paypal.com) and ask for them to be allowed through the outbound firewall. Always test with the gateway's sandbox endpoint first, because live credentials against a sandbox URL produce authentication errors that masquerade as TLS problems.

Inbound webhook delivery and gateway credentials

The notify URL must be reachable by the gateway's own servers with no authentication wall in front of it. A frequent self-inflicted failure is a password-protected staging directory or an overly aggressive bot-blocking rule in .htaccess that returns 403 to the gateway's POST. Review your rewrite rules and remove any user-agent or IP blocks that would catch Stripe/1.0, PayPal, or similar. The notify path should pass straight through to index.php:

# Ensure Payum notify/capture routes are never blocked
RewriteCond %{REQUEST_URI} ^/payment/(notify|capture)/ [NC]
RewriteRule ^ - [L]

Confirm each gateway's dashboard webhook endpoint is set to the exact live HTTPS notify URL Sylius expects, and that signing secrets match. In the Sylius admin, go to Configuration > Payment methods, open the method, and verify the gateway configuration fields: for Stripe the publishable key, secret key, and webhook signing secret; for PayPal the client ID and secret plus the correct sandbox/live toggle; for Mollie the API key prefixed live_ or test_. A credential in test mode while the method is set to live is the single most common reason transactions appear to "process" but never complete. After saving, place a sandbox order and watch var/log/prod.log grow in real time through File Manager to confirm the notify request arrives and the payment transitions to completed.

For durable troubleshooting, raise Payum's logging verbosity so every gateway exchange is recorded. In config/packages/prod/monolog.yaml you can add a dedicated channel handler that writes payment events without SSH, then inspect the file in File Manager:

monolog:
    handlers:
        payum:
            type: stream
            path: "%kernel.logs_dir%/payum.log"
            level: debug
            channels: ["payum"]

If the gateway dashboard shows delivered webhooks but Sylius never advances the order, the notify request is reaching PHP but failing verification — almost always a mismatched signing secret or a clock skew on failed signature timestamps. If the dashboard shows failed deliveries with timeouts, the problem is inbound reachability or a slow response because the handler is blocked on an outbound call. Shared hosts that follow proxy-awareness patterns similar to our Bagisto SSL and APP_URL guide benefit from the same scheme-detection fixes, since a wrong base URL breaks signature-bearing callbacks across most PHP e-commerce stacks. Work through scheme, outbound TLS, reachability, and credentials in that order and the stuck-order pattern resolves predictably.