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.
We recommend using https for your webhook URL because it is more secure. We support either http or https, but we cannot guarantee security with http.
If you use https, your SSL/TLS certificate must be validated — self-signed certificates will not work. We may introduce an option to use self-signed certificates in the future, so if this is something you're interested in, please let us know.
Check out our example Webhook payload or head to the Webhooks reference for endpoint information.