Joomla itself is a content management framework, so payment handling always lives inside an extension: VirtueMart, HikaShop, J2Store, or a membership component like Akeeba Subscriptions or RSMembership. That distinction matters because the checkout, the gateway callback, and the webhook receiver are all controlled by the extension, while the transport layer that carries those requests is controlled by PHP and the web server. When a payment silently fails, the order gets stuck in a pending state even though the customer was charged. The money moved, but Joomla never received confirmation, so no invoice, no download token, and no subscription activation ever fires.

There are two distinct network directions that break for different reasons, and confusing them wastes hours. An outbound call happens when Joomla contacts the gateway API directly (Stripe charge creation, PayPal REST token exchange). An inbound callback or webhook happens when the gateway contacts your site (PayPal IPN, Stripe webhook events, redirect returns from a hosted checkout page). Outbound failures are almost always cURL, TLS, or credential problems on your account. Inbound failures are almost always URL, redirect, or blocking-rule problems that stop the gateway's request from reaching the extension's listener script. The sections below separate them cleanly so you fix the right layer.

Outbound API calls: cURL, TLS, and CA bundle failures

Most modern gateways require TLS 1.2 or higher and reject anything older. On CloudLinux the PHP version and its extensions are chosen per-account through the PHP Selector, so an outdated PHP branch or a disabled cURL extension breaks outbound requests before Joomla even reaches the gateway. Open cPanel and go to Software > Select PHP Version (DirectAdmin: Extra Features > Select PHP Version). Confirm the domain runs PHP 8.1 or newer, then check the Extensions tab and ensure curl and openssl are both ticked. A PHP 7.2 branch bundled with an old OpenSSL library will negotiate TLS 1.0 and get refused by Stripe or PayPal with a connection reset rather than a clean error.

When the gateway refuses the handshake, the extension usually logs a message resembling SSL certificate problem: unable to get local issuer certificate or error:0A000410:SSL routines::sslv3 alert handshake failure. The first message is a missing or stale CA bundle. On shared hosting you cannot edit php.ini at the server level, but you can point PHP at a current bundle through a per-account .user.ini placed in your document root (/home/USER/public_html/). Download the current CA bundle from a trusted source, upload it to a private folder, and reference it:

; public_html/.user.ini
curl.cainfo = "/home/USER/ssl/cacert.pem"
openssl.cafile = "/home/USER/ssl/cacert.pem"

Changes to .user.ini are cached, so wait for the user_ini.cache_ttl window (typically five minutes) or touch the calling PHP file to force a reload. Never disable verification with curl.verify_ssl = 0 or set an extension option to skip peer checks; that turns a payment channel into a man-in-the-middle target. If the extension exposes its own cURL options in the gateway plugin settings, leave verification enabled and instead fix the bundle path.

Confirm the extension is actually using cURL and not the deprecated allow_url_fopen stream. In Joomla's System > Global Configuration > Server tab there is no cURL toggle, but many payment plugins have a transport setting in Components > [gateway plugin] > Options. Force cURL where available, because stream wrappers ignore the CA bundle path above and fail unpredictably behind LiteSpeed.

Inbound callbacks and webhooks that never arrive

When the outbound charge succeeds but the order stays pending, the gateway's confirmation request is being lost. Every gateway posts to a fixed callback URL that the extension registers, for example https://yourdomain.com/index.php?option=com_hikashop&ctrl=notification&task=notify&notif_type=paypal. Three things commonly break that path on shared hosting.

First, SEF and HTTPS redirects mangle the request. Gateways send a single POST and do not follow redirects reliably, so a .htaccess rule that forces www or strips a query string turns the callback into a 301 that the gateway abandons. Inspect your document-root .htaccess and make sure any canonical redirect excludes the notification endpoint, or that your Joomla SEF settings and the gateway's registered URL agree on the exact host and scheme:

RewriteEngine On
# Skip canonical redirect for payment notifications
RewriteCond %{QUERY_STRING} option=com_hikashop [NC]
RewriteRule ^ - [L]
# Canonical HTTPS + non-www for everything else
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^(.*)$ https://yourdomain.com/$1 [R=301,L]

Second, security rules block the gateway's user agent or POST body. LiteSpeed and some Joomla security extensions filter requests with empty referrers or unfamiliar agents, which describes almost every server-to-server webhook. If you run a firewall extension like Admin Tools, temporarily add the endpoint to its exceptions list under Components > Admin Tools > Web Application Firewall > Exceptions rather than loosening rules globally. Third, the callback URL points at a maintenance page or a staging host left over from migration. Check the exact URL saved in the gateway's dashboard (Stripe webhook settings, PayPal IPN profile) against the live domain.

Verify delivery from the gateway side first: Stripe's dashboard shows each webhook attempt with the HTTP status your server returned, and PayPal's IPN history shows whether the resend succeeded. A recurring 500 means the extension received the request but failed to process it; a 404 means the URL is wrong; a timeout means a firewall or redirect swallowed it. This external log is the single fastest way to tell inbound from outbound problems.

Reading transaction logs and credential mismatches

Once you know which direction fails, the account-level error_log tells you why. Joomla writes PHP fatals to a file named error_log in the directory of the failing script, and to the path set under System > Global Configuration > System > Debug > Log Path (default /home/USER/public_html/administrator/logs/). Enable Log Almost Everything and Log Deprecated API temporarily, reproduce a test payment, then open administrator/logs/everything.php through File Manager. Payment extensions add their own logs there too, such as com_hikashop.error.php or J2Store's payment log, which record the raw gateway response and the parsed order state.

A large share of stuck orders come from credential mismatches rather than transport failures. Live keys used against a sandbox endpoint, or a test webhook secret validated against live events, produces a signature-verification failure that many extensions log as a generic decline. Confirm in Components > [gateway] > Payment Methods that the mode (Live vs Sandbox) matches the key set, and that the webhook signing secret matches the endpoint the gateway is actually calling. When you rotate a Stripe key or PayPal client secret, update it in both the Joomla plugin and, if the extension caches configuration, clear the cache under System > Clear Cache.

For orders already stuck, do not edit the database blindly. Use phpMyAdmin to inspect the relevant table (#__hikashop_order, #__j2store_orders, or the VirtueMart order tables) and confirm the order_status and stored transaction ID against what the gateway reports as captured. If the payment genuinely cleared, most extensions offer a manual status change in the admin order view that fires the same fulfillment logic the missed webhook would have. Trigger that rather than flipping a status column directly, so invoices and notification emails still send. Keep debug logging enabled only while diagnosing, then disable it, because verbose logs on a live checkout leak order data and grow quickly against your account's inode and disk quotas.