Signature Verification
Learn how to securely verify signatures of webhook events that you receive from ClickFunnels.
To verify ClickFunnels event signatures, both parties need to have a shared secret. In ClickFunnels, you will usually first set up an endpoint (via the UI or API) that points to the URL of the app that will be receiving them.
Getting the secret
You can get the secret when you create a webhook endpoint via the UI or API.
In the UI, you can get it in Workspace Settings > Webhooks > Webhook Endpoint:
Here is how it would look like after you create a Webhooks::Outgoing::Endpoint
via the API:
{
"id": 2,
"public_id": "QqYxtu",
"workspace_id": 42000,
"url": "https://example.com/some-endpoint-url",
"name": "Example Endpoint",
"event_type_ids": [
"contact.identified",
"one-time-order.completed"
],
"api_version": 2,
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:00:00.000Z",
"page_ids": [],
"funnel_ids": [],
"webhook_secret": "839d0d645bedefbaf76e3269d1b1141ef07683c179cc3bb0aee9348c90517a7"
}
After the endpoint is created, you'll get a webhook_secret
value. You should retrieve that value and set it as an environment variable in the app that will receive the webhook events.
The API only displays the
webhook_secret
once after it is created. If you didn't catch the secret here, you will need to retrieve it from the UI.
Verification example
Here is an example script of how you would verify a signature in your app's code. To go through the example, do the following.
- Set up a webhooks endpoint in ClickFunnels.
- Send an event to https://webhook.site .
- Follow the instructions inside the script and fill the data to see how this works:
#!/usr/bin/env ruby
# Webhook Signature Verification Script for ClickFunnels Webhooks
#
# Usage: ruby signature-verification.rb
require "openssl"
require "json"
require "time"
def verify_request
# Get this from your ClickFunnels webhook endpoint, either from the initial POST request or in the ClickFunnels UI.
secret = "Fill in"
signature = "Fill in" # Get this from the X-Webhook-ClickFunnels-Signature header.
timestamp = "Fill in" # Get this from the X-Webhook-ClickFunnels-Timestamp header.
payload = "Fill in" # Get this as a raw string from the event payload.
verify(payload, signature, timestamp, secret)
end
def verify(payload, signature, timestamp, secret)
tolerance_seconds = 600
timestamp_int = timestamp.to_i
now = Time.now.to_i
if (now - timestamp_int).abs > tolerance_seconds
return false # Webhook is too old or timestamp is from the future
end
# Compute the expected signature
signature_payload = "#{timestamp}.#{payload}"
expected_signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_payload)
# Make sure you compare this with a library that prevents timing attacks.
expected_signature == signature
end
if verify_request
# Process the event as you would usually do.
puts "Successfully verified webhook event!"
else
# Respond with an error code and message that you usually have for non-existent endpoints.
puts "Verification failed!"
end
# Your Rails controller receiving the incoming webhook events:
before_action :verify_authenticity, only: [:create]
def verify_authenticity
# Create an ENV variable to hold the webhook_secret.
unless IncomingWebhooks.verify_request(request, ENV["CLICKFUNNELS_WEBHOOK_SECRET"])
# Respond with an error code and message that you usually have for non-existent endpoints.`
render json: {error: "Not found"}, status: :not_found
end
end
# incoming_webhooks.rb
class IncomingWebhooks
def self.verify(payload, signature, timestamp, secret)
return false if payload.blank? || signature.blank? || timestamp.blank? || secret.blank?
tolerance = 600 # The default time after which an event signature expires.
# Check if the timestamp is too old.
timestamp_int = timestamp.to_i
now = Time.now.to_i
if (now - timestamp_int).abs > tolerance
return false # Webhook is too old or timestamp is from the future
end
# Compute the expected signature
signature_payload = "#{timestamp}.#{payload}"
expected_signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_payload)
# Compare signatures using constant-time comparison to prevent timing attacks
ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end
# A Rails controller helper example to verify webhook requests.
#
# @param request [ActionDispatch::Request] The Rails request object.
# @param secret [String] The webhook secret shared with the sender.
# @return [Boolean] True if the signature is valid, false otherwise.
def self.verify_request(request, secret)
return false if request.blank? || secret.blank?
signature = request.headers["#{webhook_headers_namespace}-Signature"]
timestamp = request.headers["#{webhook_headers_namespace}-Timestamp"]
payload = request.raw_post
return false if signature.blank? || timestamp.blank?
verify(payload, signature, timestamp, secret)
end
end
// TBD
Notes:
- The default expiration time is 600 seconds, after this time the verification will fail.
Updated about 3 hours ago
Now you can implement this verification inside your app. Check out our Integrate With Us page and let us know if you want to talk!