What FreeScout actually sends, and what it does not

Before chasing a "payment" problem, set expectations about scope. FreeScout is a help desk and shared inbox, not a storefront. It has no checkout page, no gateway integration, no order table, and no concept of a transaction. There is no Stripe or PayPal callback handler built into the core application or any first‑party module. If you landed here looking for a failed card charge, that logic lives in whatever external system you connected FreeScout to, not in FreeScout itself.

What FreeScout does have, through the official API & Webhooks module, is an outbound event system that behaves exactly like a payment provider's webhook sender. When a conversation is created, assigned, replied to, or a custom Workflow event fires, FreeScout issues an HTTP POST to a URL you register under Manage » API & Webhooks. That POST carries a JSON body and an X-FreeScout-Signature header, and the receiving endpoint is expected to verify the signature and return a 2xx response. The failure modes of this flow are identical to gateway integrations: TLS handshake errors, cURL timeouts, mismatched secrets, blocked requests, and silent background‑job stalls. So "payments & webhooks" for FreeScout translates cleanly into webhook delivery, API credential, and callback‑logging troubleshooting, which is what the rest of this applies to.

One structural detail drives almost every diagnosis: webhooks are delivered by background jobs, not inline with the browser request that triggered the event. If your queue worker or cron is not running, the event is recorded but the POST never leaves the server. That is the single most common reason a webhook "disappears" with no error at all, and it is worth confirming before touching TLS or signatures.

Reading the webhook and API logs first

FreeScout keeps per‑webhook delivery records, and you should always read those before editing anything. Open Manage » API & Webhooks in the admin dashboard and look at each webhook's lastRunTime and lastRunError fields. A populated lastRunError tells you the request left the server and something answered badly; an empty error with a stale run time usually means the job never executed. The same page lists your API keys, their allowed‑IP restrictions, and the shared secret used to sign requests.

Application‑level problems land in the Laravel log rather than the webhook list. On shared hosting you reach it through cPanel File Manager or DirectAdmin File Manager at:

/home/USERNAME/freescout/storage/logs/laravel.log

Open the most recent entries and search for cURL error, SSL, Maximum execution time, or GuzzleHttp. These strings map directly to fixable causes. A cURL error 28 is a connection or response timeout; cURL error 35 or error 60 point at TLS negotiation and certificate validation; a 401 or 403 recorded against an inbound API call means the key or its IP allow‑list is rejecting the caller. If the log is empty or not updating while events clearly fire, that reinforces the background‑job theory rather than a delivery error.

Also confirm the module is actually enabled under Manage » Modules. A deactivated API & Webhooks module silently stops both inbound API authentication and outbound webhook dispatch, and the symptom looks indistinguishable from a broken endpoint.

Fixing cURL, TLS, and outbound connectivity

Outbound webhook delivery depends on PHP's cURL extension and a current CA bundle. On a managed LiteSpeed/CloudLinux stack you control the PHP build through the control panel, and the two settings that matter most are the active PHP version and the enabled extensions. In cPanel open Select PHP Version (the PHP Selector under the Software section) and confirm curl and openssl are checked. In DirectAdmin Evolution the same controls live under Select PHP Version / PHP Selector. If cURL is disabled, every outbound POST fails before it starts, and FreeScout logs a generic transport error.

TLS failures (cURL error 60: SSL certificate problem) usually mean the receiving endpoint presents a certificate your PHP build cannot validate, or the remote host only negotiates a protocol your outdated PHP refuses. The correct fix is never to disable verification. Instead, move the account to a current PHP branch in the PHP Selector so it ships a modern OpenSSL and an up‑to‑date CA store, then retry the webhook from the admin page. If the destination is an internal or self‑signed service, give it a publicly trusted certificate rather than weakening FreeScout. The module changelog notes default cURL options were added specifically to prevent delivery breakage on certain systems, so running the latest module version through the FreeScout updater also removes a class of handshake bugs.

Timeouts (cURL error 28) have two causes worth separating. Either the receiving script is slow, or FreeScout's background worker is being killed by PHP limits before the request completes. You cannot edit php.ini or my.cnf on a shared plan, but you can raise per‑account limits with a .user.ini file placed in the FreeScout document root:

; /home/USERNAME/freescout/public/.user.ini
max_execution_time = 120
default_socket_timeout = 60
memory_limit = 256M

Changes to .user.ini are picked up after the file's cache TTL (commonly 300 seconds) or after an LSPHP restart triggered by the control panel's PHP version save. Keep in mind the receiving endpoint still has to answer within FreeScout's own request timeout, so a cooperating remote server matters as much as your limits.

Credentials, signature verification, and blocked requests

When the POST arrives but the receiver rejects it, the problem is authentication or signature verification rather than connectivity. FreeScout signs each request with an HMAC‑SHA1 digest of the raw body, base64‑encoded, using the secret shown under Manage » API & Webhooks. The receiving endpoint must compute the same digest over the exact raw request body, never a re‑encoded or pretty‑printed copy, and compare it to the X-FreeScout-Signature header. The official verification pattern is straightforward:

$secret = 'YOUR_WEBHOOK_SECRET';
$data = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_FREESCOUT_SIGNATURE'] ?? '';
$calculated = base64_encode(hash_hmac('sha1', $data, $secret, true));
if (hash_equals($calculated, $signature)) {
    // trusted request
}

The usual mistake is reading parsed JSON instead of the raw stream, which changes whitespace and breaks the hash even though the secret is correct. If verification fails intermittently, confirm the secret was copied without a trailing space and that you are not regenerating it on the FreeScout side while an old value is still deployed.

Inbound API calls to FreeScout use the X-FreeScout-API-Key header. A 401 means the key is wrong or revoked; a 403 very often means the request's source IP is not in the key's allowed‑IP list, which supports comma‑separated CIDR ranges. On shared hosting, an aggressive ModSecurity rule or a proxy such as Cloudflare in front of your domain can also strip or block POSTs carrying JSON payloads. Check whether a .htaccess rewrite or security directive in the FreeScout root is intercepting the path, and make sure your rules do not rewrite or redirect the API and webhook endpoints:

# keep API and webhook callbacks out of custom rewrites
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/api/ [OR]
RewriteCond %{REQUEST_URI} ^/modules/apiwebhooks/
RewriteRule ^ - [L]

Finally, treat the background queue as a first‑class dependency. If deliveries are logged with stale timestamps, verify the FreeScout cron entry (the scheduler plus the queue worker) is active in cPanel's Cron Jobs or DirectAdmin's Cron Jobs panel. A webhook that never runs produces no TLS error and no signature mismatch precisely because the request was never attempted, and no amount of endpoint tuning will surface it until the worker is alive.