Webhook Troubleshooting
This guide helps you diagnose why webhooks are failing and how to fix them. Check the Webhook Logs tab in your dashboard for the HTTP response code and response body — these are your best starting point.
How Webhooks Work
When a shipment status changes, TrackLogy sends a POST request to your store's configured webhook_url:
POST https://your-server.com/your-webhook-path
Content-Type: application/json
X-TrackLogy-Signature: sha256=<hmac>
{ "event": "tracking.updated", "tracking_number": "...", "status": "delivered", ... }Your server must:
- Receive the request
- Verify the
X-TrackLogy-Signatureheader - Return a
2xxHTTP status within 10 seconds
If any step fails, the delivery is recorded as failed in your webhook logs.
Quick Checklist
Before diving into specific errors, run through this list:
-
webhook_urlis a publicly reachable HTTPS endpoint - The URL does not have a trailing slash mismatch or redirect
- Your server returns a
2xxstatus for the endpoint - Signature verification is using the raw request body (not re-serialized JSON)
-
webhook_secretin your dashboard matches the one in your server code - Your server responds within 10 seconds
- No firewall or security middleware is blocking POST requests from TrackLogy IPs
- SSL certificate is valid and not expired
HTTP Error Code Reference
No Response / Timeout
Cause: Your server did not respond within 10 seconds, or the connection was refused.
Fix:
- Acknowledge the webhook immediately (
200 OK) and process asynchronously using a queue or background job - Check that the URL is publicly reachable — test from outside your local network
- Ensure no firewall rule is blocking inbound POST requests
// Acknowledge immediately, then process
http_response_code(200);
echo 'OK';
fastcgi_finish_request(); // or flush() + ob_flush()
// Now do your slow processing
processWebhookAsync($payload);400 Bad Request
Cause: Your server rejected the request body as malformed.
Fix:
- Check that your handler is not trying to parse the body with assumptions about encoding
- Do not apply
json_decodebefore signature verification — use the raw body - Confirm your endpoint accepts
Content-Type: application/json
401 Unauthorized
Cause: Signature verification failed — your computed HMAC does not match X-TrackLogy-Signature.
Fix:
Always use the raw request body for HMAC computation, not a re-encoded or re-serialized version of the JSON. Even a single extra space or character order change will produce a different hash.
Common mistakes:
- Reading
$_POSTinstead offile_get_contents('php://input') - Using
json_decodethenjson_encodebefore hashing - Wrong
webhook_secret— copy it exactly from Settings → Stores → Edit - Whitespace or newline appended to the secret
$raw = file_get_contents('php://input'); // raw bytes
$secret = getenv('WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
$received = $_SERVER['HTTP_X_TRACKLOGY_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Signature mismatch');
}
$data = json_decode($raw, true);403 Forbidden
Cause: A firewall, WAF, middleware, or authentication layer is blocking the request before it reaches your handler.
Fix:
- Whitelist the TrackLogy webhook IP range or bypass IP-based auth for the webhook endpoint
- Check for framework-level CSRF protection — exempt the webhook route
- Review WAF rules that may reject
POSTrequests withapplication/jsonbodies
404 Not Found
Cause: The URL path does not exist on your server.
Fix:
- Double-check the
webhook_urlin Settings → Stores → Edit - Verify the route is registered and deployed (not just on your local machine)
- Check for missing trailing slash or wrong subdomain
405 Method Not Allowed
Cause: Your endpoint only accepts GET requests, not POST.
Fix: Register the endpoint to handle POST requests:
// Laravel
Route::post('/webhook', [WebhookController::class, 'handle']);
// Symfony
#[Route('/webhook', methods: ['POST'])]408 / Timeout
Cause: Your handler took longer than 10 seconds to return a response.
Fix: Return 200 OK immediately, then process asynchronously (queue, job worker, etc.). See the No Response section above for an example.
422 Unprocessable Entity
Cause: Your handler received the body but rejected it during validation.
Fix:
- Review your validation logic — TrackLogy sends a well-formed JSON payload
- Check the
eventfield istracking.updatedand handle unknown events gracefully - Return
200 OKfor events you don't handle (don't 422 them)
429 Too Many Requests
Cause: Your server is rate-limiting inbound webhook requests.
Fix:
- Increase your server's rate limit for the webhook endpoint
- If using a WAF or reverse proxy, tune its rate limit rules
- Process webhooks via a queue to handle burst traffic without overloading your handler
500 Internal Server Error
Cause: An unhandled exception in your webhook handler.
Fix:
- Check your server error logs for the actual exception
- Add
try/catcharound your handler logic - Ensure database connections are healthy at the time of the webhook
502 / 503 / 504
Cause: Your server or upstream service is down, overloaded, or a gateway is timing out.
Fix:
- Check your server's health and resource usage (CPU, memory, disk)
- If using a reverse proxy (nginx, Cloudflare), check its timeout settings
- A 504 from a proxy usually means your app server is slow — see timeout fix above
Step-by-Step Debugging
Check the webhook log
Open Dashboard → Webhook Logs, find the failed delivery, and expand the row to see:
- HTTP status code returned by your server
- Request payload that was sent
- Response body from your server
Test your endpoint manually
Send a test payload to your endpoint with curl:
curl -X POST https://your-server.com/webhook \
-H "Content-Type: application/json" \
-H "X-TrackLogy-Signature: sha256=test" \
-d '{"event":"tracking.updated","status":"delivered"}' \
-vWatch for redirect chains (301/302), auth prompts, or unexpected status codes.
Verify signature computation
Log both sides before comparing:
error_log('Received: ' . ($received ?? 'none'));
error_log('Expected: ' . $expected);Confirm webhook_secret matches exactly what is in your dashboard (no spaces, no newlines).
Check server logs
Look at your web server error log (/var/log/nginx/error.log, Laravel storage/logs/laravel.log, etc.) for exceptions that may be causing the 500 or causing the connection to close before a response is sent.
Use Dashboard test tool
Go to Settings → Stores → Edit and use Test Webhook to fire a sample payload. This is the fastest way to confirm your endpoint is reachable and responding correctly.
Signature Verification Deep Dive
The signature is sha256= + HMAC-SHA256 of the raw request body using your store's webhook_secret.
The most common cause of signature mismatch is using a parsed/re-serialized JSON object instead of the raw bytes:
// WRONG — re-serialized JSON may have different key order or spacing
$body = json_encode(json_decode(file_get_contents('php://input')));
$sig = hash_hmac('sha256', $body, $secret); // will NOT match
// CORRECT — raw bytes exactly as received
$body = file_get_contents('php://input');
$sig = hash_hmac('sha256', $body, $secret); // matchesTimeout & Performance
TrackLogy waits up to 10 seconds for your endpoint to respond. If your handler needs to:
- Write to a database
- Call a third-party API
- Send an email or notification
...do it asynchronously. Acknowledge the webhook immediately and push the work to a queue:
// Good pattern
http_response_code(200);
echo json_encode(['received' => true]);
fastcgi_finish_request(); // flush response to client
// Then do slow work
MyQueue::dispatch($payload);Popular queue options: Laravel Queues, BullMQ (Node), Celery (Python), RabbitMQ, SQS.
Retry Behaviour
TrackLogy does not currently retry failed webhook deliveries automatically. You can manually retry a failed delivery from the Webhook Logs page in your dashboard.
Because retries are manual, your webhook handler should be idempotent — processing the same payload twice should not create duplicate records. Use the shipment_id or tracking_number + status combination as a deduplication key:
// Idempotent upsert example (Laravel)
ShipmentStatus::updateOrCreate(
['shipment_id' => $data['shipment_id'], 'status' => $data['status']],
['updated_at' => $data['pushed_at']]
);FAQ
My endpoint works in local testing but fails in production. Why?
Your local machine is not publicly reachable by TrackLogy. Webhooks must be delivered to a public URL. Use a tunneling tool like ngrok for local testing, and ensure your production URL is the one configured in Settings → Stores → Edit.
I'm getting 401 but my secret looks correct. What else could it be?
Check for invisible characters — copy-paste from a password manager can introduce zero-width spaces. Try setting the secret by typing it manually. Also confirm you're reading the raw body with file_get_contents('php://input') (PHP) or express.raw() (Node.js).
My server returns 200 but the log still shows "failed". Is that possible?
No — if your server returns any 2xx code, the delivery is recorded as successful. If you see failed, the HTTP response code was non-2xx, or the connection timed out before a response was sent.
Can I use HTTP instead of HTTPS?
We strongly recommend HTTPS. HTTP endpoints are accepted but the payload will be transmitted in plaintext. Always use HTTPS in production.
How do I test without a real shipment status change?
Use Settings → Stores → Test Webhook to send a sample tracking.updated payload to your configured endpoint immediately.
What IP addresses does TrackLogy use to send webhooks?
Contact [email protected] for the current IP range if you need to whitelist TrackLogy in a firewall or WAF.
I want to verify the webhook is not replayed (replay attack protection). How?
Include a timestamp in your verification logic. TrackLogy's payload includes pushed_at — reject any payload where pushed_at is more than 5 minutes in the past:
$data = json_decode($body, true);
$age = time() - strtotime($data['pushed_at']);
if ($age > 300) {
http_response_code(400);
exit('Stale webhook — possible replay');
}Still stuck? Email [email protected] and include the webhook log entry (tracking number, timestamp, HTTP status, and response body) so we can help quickly.