Why webhook security matters
Webhooks expose your application to the public internet. Without proper security:
Attackers could forge events - Trigger unauthorized actions in your system
Data could be compromised - Sensitive customer information could be intercepted
Replay attacks - Old webhooks could be re-sent to cause duplicate actions
DDoS attacks - Your endpoint could be overwhelmed with fake requests
Pocketsflow provides robust signature verification to protect against these threats.
Webhook signing secrets
When you create a webhook in Pocketsflow, you receive a signing secret :
Format : whsec_ followed by a random string
Example : whsec_1a2b3c4d5e6f7g8h9i0j
Critical : Save this secret immediately. It’s only shown once during creation.
Storing secrets securely
✅ DO :
// Store in environment variables
const WEBHOOK_SECRET = process . env . POCKETSFLOW_WEBHOOK_SECRET ;
❌ DON’T :
// Never hardcode secrets in your code
const WEBHOOK_SECRET = 'whsec_1a2b3c4d5e6f7g8h9i0j' ; // WRONG!
Best practices :
Use environment variables or secret managers (AWS Secrets Manager, Google Secret Manager, etc.)
Never commit secrets to version control
Add *.env to your .gitignore
Use different secrets for staging and production
Rotate secrets if compromised
Signature verification
Every Pocketsflow webhook includes cryptographic signatures to verify authenticity.
How it works
Pocketsflow signs the webhook
Before sending, we create an HMAC SHA256 signature using:
Webhook payload (JSON body)
Your webhook secret
Signature sent in headers
The signature is included in the X-Pocketsflow-Signature header
Your server verifies
Recompute the signature using the payload and your secret, then compare
Accept or reject
If signatures match → process the webhook. If not → reject (401)
Every webhook request includes these headers:
Header Description Example X-Pocketsflow-SignatureHMAC SHA256 signature a1b2c3d4e5f6...X-Pocketsflow-EventEvent type order.completedX-Pocketsflow-TimestampUnix timestamp (ms) 1703174400000Content-TypeAlways JSON application/json
Implementation examples
Node.js / Express
const express = require ( 'express' );
const crypto = require ( 'crypto' );
const app = express ();
const WEBHOOK_SECRET = process . env . POCKETSFLOW_WEBHOOK_SECRET ;
// IMPORTANT: Use express.raw() to get the raw body
app . post ( '/webhooks/pocketsflow' ,
express . raw ({ type: 'application/json' }),
( req , res ) => {
const signature = req . headers [ 'x-pocketsflow-signature' ];
const payload = req . body ;
// Verify signature
const expectedSignature = crypto
. createHmac ( 'sha256' , WEBHOOK_SECRET )
. update ( payload )
. digest ( 'hex' );
if ( signature !== expectedSignature ) {
console . error ( 'Invalid signature' );
return res . status ( 401 ). send ( 'Unauthorized' );
}
// Signature verified - safe to process
const event = JSON . parse ( payload );
console . log ( 'Verified event:' , event . event );
// Process the webhook...
processWebhook ( event ). catch ( console . error );
res . status ( 200 ). send ( 'OK' );
}
);
function processWebhook ( event ) {
// Your business logic here
switch ( event . event ) {
case 'order.completed' :
return grantAccess ( event . customer . email , event . product . id );
case 'customer.subscription.deleted' :
return revokeAccess ( event . customer . email );
}
}
Python / Flask
import hmac
import hashlib
import os
from flask import Flask, request
app = Flask( __name__ )
WEBHOOK_SECRET = os.environ[ 'POCKETSFLOW_WEBHOOK_SECRET' ]
@app.route ( '/webhooks/pocketsflow' , methods = [ 'POST' ])
def webhook ():
signature = request.headers.get( 'X-Pocketsflow-Signature' )
payload = request.data
# Verify signature
expected_signature = hmac.new(
WEBHOOK_SECRET .encode( 'utf-8' ),
payload,
hashlib.sha256
).hexdigest()
if signature != expected_signature:
return 'Unauthorized' , 401
# Signature verified
event = request.get_json()
print ( f 'Verified event: { event[ "event" ] } ' )
# Process webhook...
process_webhook(event)
return 'OK' , 200
def process_webhook ( event ):
if event[ 'event' ] == 'order.completed' :
grant_access(event[ 'customer' ][ 'email' ], event[ 'product' ][ 'id' ])
PHP
<? php
$webhookSecret = getenv ( 'POCKETSFLOW_WEBHOOK_SECRET' );
$signature = $_SERVER [ 'HTTP_X_POCKETSFLOW_SIGNATURE' ];
$payload = file_get_contents ( 'php://input' );
// Verify signature
$expectedSignature = hash_hmac ( 'sha256' , $payload , $webhookSecret );
if ( $signature !== $expectedSignature ) {
http_response_code ( 401 );
die ( 'Unauthorized' );
}
// Signature verified
$event = json_decode ( $payload , true );
error_log ( 'Verified event: ' . $event [ 'event' ]);
// Process webhook...
processWebhook ( $event );
http_response_code ( 200 );
echo 'OK' ;
function processWebhook ( $event ) {
switch ( $event [ 'event' ]) {
case 'order.completed' :
grantAccess ( $event [ 'customer' ][ 'email' ], $event [ 'product' ][ 'id' ]);
break ;
}
}
?>
Ruby / Sinatra
require 'sinatra'
require 'openssl'
require 'json'
WEBHOOK_SECRET = ENV [ 'POCKETSFLOW_WEBHOOK_SECRET' ]
post '/webhooks/pocketsflow' do
signature = request. env [ 'HTTP_X_POCKETSFLOW_SIGNATURE' ]
payload = request. body . read
# Verify signature
expected_signature = OpenSSL :: HMAC . hexdigest (
'SHA256' ,
WEBHOOK_SECRET ,
payload
)
if signature != expected_signature
halt 401 , 'Unauthorized'
end
# Signature verified
event = JSON . parse (payload)
puts "Verified event: #{ event[ 'event' ] } "
# Process webhook...
process_webhook (event)
status 200
body 'OK'
end
def process_webhook ( event )
case event[ 'event' ]
when 'order.completed'
grant_access (event[ 'customer' ][ 'email' ], event[ 'product' ][ 'id' ])
end
end
package main
import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" encoding/json "
" io/ioutil "
" log "
" net/http "
" os "
)
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
webhookSecret := os . Getenv ( "POCKETSFLOW_WEBHOOK_SECRET" )
signature := r . Header . Get ( "X-Pocketsflow-Signature" )
payload , _ := ioutil . ReadAll ( r . Body )
// Verify signature
mac := hmac . New ( sha256 . New , [] byte ( webhookSecret ))
mac . Write ( payload )
expectedSignature := hex . EncodeToString ( mac . Sum ( nil ))
if signature != expectedSignature {
http . Error ( w , "Unauthorized" , http . StatusUnauthorized )
return
}
// Signature verified
var event map [ string ] interface {}
json . Unmarshal ( payload , & event )
log . Printf ( "Verified event: %s " , event [ "event" ])
// Process webhook...
processWebhook ( event )
w . WriteHeader ( http . StatusOK )
w . Write ([] byte ( "OK" ))
}
func main () {
http . HandleFunc ( "/webhooks/pocketsflow" , webhookHandler )
log . Fatal ( http . ListenAndServe ( ":3000" , nil ))
}
Timestamp validation
For additional security, validate that webhooks aren’t too old:
const timestamp = parseInt ( req . headers [ 'x-pocketsflow-timestamp' ]);
const currentTime = Date . now ();
const fiveMinutes = 5 * 60 * 1000 ;
if ( Math . abs ( currentTime - timestamp ) > fiveMinutes ) {
return res . status ( 401 ). send ( 'Webhook too old' );
}
This prevents replay attacks where old webhooks are re-sent.
Common security mistakes
❌ Parsing before verifying
Wrong :
app . post ( '/webhooks' , express . json (), ( req , res ) => {
// Body already parsed - can't verify signature!
const signature = req . headers [ 'x-pocketsflow-signature' ];
// Too late - req.body is an object, not raw bytes
});
Correct :
app . post ( '/webhooks' , express . raw ({ type: 'application/json' }), ( req , res ) => {
// Verify FIRST with raw body
verifySignature ( req . body , signature );
// THEN parse
const event = JSON . parse ( req . body );
});
❌ Using timing-unsafe comparison
Wrong :
if ( signature === expectedSignature ) { // Vulnerable to timing attacks
Better :
const crypto = require ( 'crypto' );
if ( crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
)) {
// Safer comparison
}
❌ Not using HTTPS
Wrong :
http://yoursite.com/webhooks // Unencrypted - can be intercepted
Correct :
https://yoursite.com/webhooks // Encrypted with TLS
Pocketsflow only sends webhooks to HTTPS endpoints.
❌ Logging sensitive data
Wrong :
console . log ( 'Webhook received:' , JSON . stringify ( event )); // Contains PII
Better :
console . log ( 'Event received:' , event . event , 'order:' , event . order . id );
// Don't log customer emails, addresses, payment details
Testing signature verification
Manual test with curl
# Your webhook payload
PAYLOAD = '{"event":"test.event","data":{}}'
# Generate signature
SIGNATURE = $( echo -n " $PAYLOAD " | openssl dgst -sha256 -hmac "whsec_YOUR_SECRET" | cut -d ' ' -f2 )
# Send test webhook
curl -X POST https://yoursite.com/webhooks/pocketsflow \
-H "Content-Type: application/json" \
-H "X-Pocketsflow-Signature: $SIGNATURE " \
-H "X-Pocketsflow-Event: test.event" \
-H "X-Pocketsflow-Timestamp: $( date +%s)000" \
-d " $PAYLOAD "
Unit test example (Jest)
const crypto = require ( 'crypto' );
describe ( 'Webhook verification' , () => {
it ( 'should accept valid signatures' , async () => {
const payload = JSON . stringify ({ event: 'test' });
const secret = 'whsec_test123' ;
const signature = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
const response = await request ( app )
. post ( '/webhooks' )
. set ( 'X-Pocketsflow-Signature' , signature )
. set ( 'Content-Type' , 'application/json' )
. send ( payload );
expect ( response . status ). toBe ( 200 );
});
it ( 'should reject invalid signatures' , async () => {
const payload = JSON . stringify ({ event: 'test' });
const badSignature = 'invalid_signature' ;
const response = await request ( app )
. post ( '/webhooks' )
. set ( 'X-Pocketsflow-Signature' , badSignature )
. send ( payload );
expect ( response . status ). toBe ( 401 );
});
});
IP whitelisting (optional)
For extra security, you can restrict webhook traffic to Pocketsflow’s IP addresses:
Nginx example :
location /webhooks/pocketsflow {
allow 1.2.3.4 ; # Pocketsflow IP
allow 5.6.7.8 ; # Pocketsflow IP
deny all ;
proxy_pass http://localhost:3000;
}
Secret rotation
Rotate your webhook secret if:
It’s been exposed (committed to git, logged, etc.)
A team member with access leaves
You suspect compromise
As part of regular security maintenance
How to rotate :
Create new webhook with new secret in dashboard
Update your code to accept both old and new secrets temporarily
Monitor that new webhook is working
Delete old webhook after confirming
Remove old secret from your code
Dual-verification during rotation :
const OLD_SECRET = process . env . OLD_WEBHOOK_SECRET ;
const NEW_SECRET = process . env . NEW_WEBHOOK_SECRET ;
function verifySignature ( payload , signature ) {
const oldValid = crypto
. createHmac ( 'sha256' , OLD_SECRET )
. update ( payload )
. digest ( 'hex' ) === signature ;
const newValid = crypto
. createHmac ( 'sha256' , NEW_SECRET )
. update ( payload )
. digest ( 'hex' ) === signature ;
return oldValid || newValid ;
}
Security checklist
Before going live, ensure:
✅ Webhook secret stored in environment variables (not code)
✅ Signature verification implemented correctly
✅ Using raw body for verification (not parsed JSON)
✅ HTTPS endpoint with valid SSL certificate
✅ Timestamp validation to prevent replay attacks
✅ Rate limiting on webhook endpoint
✅ Error handling doesn’t expose sensitive info
✅ Logging excludes PII (customer emails, addresses)
✅ Idempotency keys used to prevent duplicate processing
✅ Webhook failures monitored and alerted
Troubleshooting
Signature verification always fails
Check :
Are you using the raw request body (not parsed)?
Is the secret correct (including any prefixes like whsec_)?
Are you using the exact same payload bytes (no whitespace changes)?
Is the HMAC algorithm SHA256 (not SHA1 or MD5)?
Debug :
console . log ( 'Received signature:' , signature );
console . log ( 'Expected signature:' , expectedSignature );
console . log ( 'Payload length:' , payload . length );
console . log ( 'Secret length:' , WEBHOOK_SECRET . length );
Getting replay attack warnings
If you’re validating timestamps and seeing failures:
Check server time is synchronized (NTP)
Verify timestamp is in milliseconds (not seconds)
Allow reasonable clock skew (5-10 minutes)
Webhook Overview Learn how to set up webhooks
Webhook Events See all available event types
Code Examples More implementation examples
Consuming Webhooks Best practices for processing