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)
end
Get 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 False
import 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.