PHP Example for V3 Signature Validation (with full explanation + working code)

Hey awesome HubSpot team & fellow devs.

After spending 4 intense days debugging signature validation for Webhooks V3 in PHP, I finally cracked it.

The current docs are helpful — but they miss a few critical implementation details for PHP developers. So I’m sharing a working PHP example + notes that could help others avoid my headache.

V3 Signature Validation in PHP — Fully Working:

//By Ramy Elkherbawy
//ramy.pro
$signature = $_SERVER['HTTP_X_HUBSPOT_SIGNATURE_V3'] ?? null;
$timestamp = $_SERVER['HTTP_X_HUBSPOT_REQUEST_TIMESTAMP'] ?? null;

if (!$signature || !$timestamp) {
 http_response_code(403);
 exit('Missing signature or timestamp.');
}

// Validate timestamp (within 5 minutes)
$maxSkew = 300;
if (abs(time() - ((int)($timestamp / 1000))) > $maxSkew) {
 http_response_code(403);
 exit('Expired timestamp.');
}

$method = $_SERVER['REQUEST_METHOD'];

// Very important! HubSpot signs the full Target URL including https://domain.com/path
// NOT just the path — use the exact URL you entered in your app's "Target URL" field.
$domain = 'https://' . $_SERVER['HTTP_HOST'];
$uri = $_SERVER['REQUEST_URI'];

// Decode URL-encoded characters (as per HubSpot docs)
$decodeMap = [
 '%3A' => ':', '%2F' => '/', '%40' => '@',
 '%21' => '!', '%24' => '$', '%27' => "'",
 '%28' => '(', '%29' => ')', '%2A' => '*',
 '%2C' => ',', '%3B' => ';',
];
$uri = strtr(rawurldecode($uri), $decodeMap);

// Final URI string used in the signature
$fullUri = $domain . $uri;

$body = file_get_contents('php://input'); // Raw JSON string
$clientSecret = 'YOUR_CLIENT_SECRET'; // Replace with yours

$rawString = $method . $fullUri . $body . $timestamp;

$expectedSignature = base64_encode(
 hash_hmac('sha256', $rawString, $clientSecret, true)
);

// Validate safely
if (!hash_equals($expectedSignature, $signature)) {
 http_response_code(403);
 exit('Invalid signature.');
}

http_response_code(200);
echo 'Valid webhook';

Key Notes:

  • HubSpot signs the exact Target URL you entered in the webhook settings, including the protocol (e.g., https://yourdomain.com/path). So make sure you’re reproducing it exactly.
  • Apply rawurldecode(), then replace special encodings manually.
  • Always use php://input for raw body (don’t use $_POST).
  • Use hash_equals() to prevent timing attacks.

This was a wild ride :joy:

HubSpot team — would love to see this kind of PHP example added to the docs. It would save a lot of us hours (and maybe some therapy bills :sweat_smile:).

Hi Ramy @RamyElkherbawy,

Wow, I love to see this, thank you so much for sharing this incredibly detailed PHP example and for documenting your process so thoroughly!
We know firsthand how tricky webhook signature validation can be, and this kind of community-contributed code and explanation is invaluable.

Your notes about handling the full Target URL and the specific decoding steps will definitely help other PHP developers avoid common pitfalls.

  • Great catch on using hash_equals() for safer signature comparison and validating the timestamp for improved security.

We really appreciate you taking the time, to share this with the HubSpot Community.

I’m also passing your feedback and example along to our documentation team.
Working PHP examples like this can make a world of difference for developers integrating with our APIs, and your suggestion is spot on.

Thanks again for being such a supportive and generous Community Member! :rocket:

Have a lovely day!
Bérangère

Thank you very much for your feedback. I truly appreciate it. Have a great day! :slightly_smiling_face:

Hi @RamyElkherbawy, I hope that you are well!
I am glad to let you know that we heard your feedback, and we’ve added an overview on validating requests originating from HubSpot to your backend service.
Here they are: “v3 request signature examples”.
Thanks again for sharing your feedback with us. It helps improve HubSpot for our whole Community.
Have a wonderful day :sun_with_face: and I look forward to seeing you around the Community!
Bérangère