Listening to a webhook implies exposing a URL (the webhook endpoint) to the web. Because anyone can call the webhook endpoint, it is insecure. The solution is to request that Typeform signs each webhook payload with a secret. The resulting signature is included in the header of the request, which you can then use to verify that the webhook is from Typeform before continuing program execution.
This page shows you how to configure secrets in webhooks so that they get signed, and how to verify those signatures in your app to maintain the data integrity of your application.
It can be done by verifying the signature of the payload which will be sent in the request header Typeform-Signature.
Generate a random string (for example, via terminal: ruby -rsecurerandom -e 'puts SecureRandom.hex(20)' ).
Update the webhook setting secret by sending an update request to the Webhooks API.
To validate the signature you received from Typeform, you will generate the signature yourself using your secret and compare that signature with the signature you receive in the webhook payload.
secret as a key) of the entire received payload as binary.sha256= to the binary hash.Typeform-Signature header from Typeform.post '/webhook' do
request.body.rewind
payload_body = request.body.read
verify_signature(request.env['HTTP_TYPEFORM_SIGNATURE'], payload_body)
"Payload received: #{payload_body.inspect}"
end
def verify_signature(received_signature, payload_body)
hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
actual_signature = 'sha256=' + Base64.strict_encode64(hash)
return halt 500, "Signatures don't match!" unless Rack::Utils.secure_compare(actual_signature, received_signature)
endGet the whole example: source
const crypto = require('crypto')
app.use(express.raw({ type: 'application/json' }))
app.post('/webhook', async (request, response) => {
const signature = request.headers['typeform-signature']
const isValid = verifySignature(signature, request.body.toString())
})
const verifySignature = function (receivedSignature, payload) {
const hash = crypto
.createHmac('sha256', process.env.SECRET_TOKEN)
.update(payload)
.digest('base64')
return receivedSignature === `sha256=${hash}`
}Get the whole example: source
const crypto = require('crypto')
const fastify = require('fastify')()
// we need to use raw request body (as string)
await fastify.register(require('fastify-raw-body'))
fastify.post('/typeform/webhook', (request, reply) => {
const signature = request.headers['typeform-signature']
const isValid = verifySignature(signature, request.rawBody)
})
const verifySignature = function (receivedSignature, payload) {
const hash = crypto
.createHmac('sha256', process.env.SECRET_TOKEN)
.update(payload)
.digest('base64')
return receivedSignature === `sha256=${hash}`
}from fastapi import FastAPI,Request,HTTPException
import hashlib
import hmac
import json
import base64
import os
app = FastAPI()
@app.post("/hook")
async def recWebHook(req: Request):
body = await req.json()
raw = await req.body()
receivedSignature = req.headers.get("typeform-signature")
if receivedSignature is None:
return HTTPException(403, detail="Permission denied.")
sha_name, signature = receivedSignature.split('=', 1)
if sha_name != 'sha256':
return HTTPException(501, detail="Operation not supported.")
is_valid = verifySignature(signature, raw)
if(is_valid != True):
return HTTPException(403, detail="Invalid signature. Permission Denied.")
def verifySignature(receivedSignature: str, payload):
WEBHOOK_SECRET = os.environ.get('TYPEFORM_SECRET_KEY')
digest = hmac.new(WEBHOOK_SECRET.encode('utf-8'), payload, hashlib.sha256).digest()
e = base64.b64encode(digest).decode()
if(e == receivedSignature):
return True
return Falseimport CryptoKit
func verifySig(receivedSig: String, payload: Request.Body) -> Bool{
let secretString = "abc123" // replace with your own
let payloadString = payload.string ?? ""
let key = SymmetricKey(data: Data(secretString.utf8))
let regenSig = HMAC<SHA256>.authenticationCode(for: Data(payloadString.utf8), using: key)
let sigData = Data(regenSig)
let sigBase64 = sigData.base64EncodedString()
let final = "sha256=\(sigBase64)"
if(final == receivedSig){
return true
}
return false
}<?php
echo "php version: ".phpversion()."\n";
$headers = getallheaders();
$header_signature = $headers["Typeform-Signature"];
$secret = getenv("TYPEFORM_WEBHOOK_SECRET");
$payload = @file_get_contents("php://input");
$hashed_payload = hash_hmac("sha256", $payload, $secret, true);
$base64encoded = "sha256=".base64_encode($hashed_payload);
echo "header signature: ".$header_signature."\n";
echo "request signature: ".$base64encoded."\n";
if ($header_signature === $base64encoded) {
echo "success!\n";
}NOTE: We do not currently have designated IPs for webhook requests. Typeform.com is hosted on Amazon Web Services (AWS) servers, which uses dynamic IP addresses, so we cannot guarantee a static IP address or even a range of IP addresses.
All new webhook URLs must use https. If you try to create or update a webhook with an http URL, the API will return a 400 Bad Request with the error code webhook_url_https_required.
Your SSL/TLS certificate must be validated — self-signed certificates will not work.
If you created a webhook with an http URL before this requirement was introduced, it will continue to work. You can still update other fields on that webhook (like enabled, secret, or event_types) without changing the URL. However, you cannot change the URL to a different http address — only to an https one.
verify_ssl fieldThe verify_ssl field is now read-only. You no longer need to set it — it's automatically derived from the URL scheme:
https URLs → verify_ssl is truehttp URLs (legacy only) → verify_ssl is falseThe field is still included in API responses for backward compatibility, but if you include it in a request, it will be ignored.
Check out our example Webhook payload or head to the Webhooks reference for endpoint information.