Webhook signature verification is essential for security. It ensures that webhook requests are actually from Pooler and haven’t been tampered with. This guide explains how to verify webhook signatures.
Why Verify Signatures?
Security Benefits
Authentication - Verify requests are from Pooler
Integrity - Ensure payload hasn’t been modified
Prevent Spoofing - Prevent attackers from sending fake webhooks
Compliance - Meet security and compliance requirements
Never process webhooks without verifying signatures. Unverified webhooks are a security risk.
Signature Generation
Pooler generates signatures using HMAC-SHA256 with your API key:
Create Payload String - Stringify the webhook JSON payload
Generate HMAC - Create HMAC-SHA256 using your API key
Encode Signature - Hex encode the HMAC
Include in Header - Send signature in x-swim-token header
Signatures are sent in the x-swim-token header as a hex-encoded HMAC-SHA256:
x-swim-token: a1b2c3d4e5f6...
The signature is a simple hex string - no timestamp or version prefix.
Getting Your API Key
Log in to your Pooler Dashboard
Navigate to Settings → Developers
Copy your API Key (use the same key you use for API requests)
Keep your API key secure. Never commit it to version control or expose it in client-side code.
Store API Key Securely
Store the API key as an environment variable:
# .env file
POOLER_API_KEY = your_api_key_here
Implementation
Node.js Implementation
const crypto = require ( 'crypto' );
function verifyWebhookSignature ( req , apiKey ) {
const signature = req . headers [ 'x-swim-token' ];
if ( ! signature ) {
return false ;
}
// Create expected signature
const payload = JSON . stringify ( req . body );
const expectedSignature = crypto
. createHmac ( 'sha256' , apiKey )
. update ( payload )
. digest ( 'hex' );
// Compare signatures using constant-time comparison
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
}
// Usage in endpoint
app . post ( '/webhooks/pooler' , express . json (), ( req , res ) => {
const apiKey = process . env . POOLER_API_KEY ;
if ( ! verifyWebhookSignature ( req , apiKey )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Process webhook
processWebhook ( req . body );
res . status ( 200 ). send ( 'OK' );
});
Python Implementation
import hmac
import hashlib
import json
import os
from flask import request
def verify_webhook_signature ( request , api_key ):
signature_header = request.headers.get( 'x-swim-token' )
if not signature_header:
return False
# Create expected signature
payload = json.dumps(request.json, sort_keys = True , separators = ( ',' , ':' ))
expected_signature = hmac.new(
api_key.encode( 'utf-8' ),
payload.encode( 'utf-8' ),
hashlib.sha256
).hexdigest()
# Compare signatures using constant-time comparison
return hmac.compare_digest(signature_header, expected_signature)
# Usage in endpoint
@app.route ( '/webhooks/pooler' , methods = [ 'POST' ])
def handle_webhook ():
api_key = os.getenv( 'POOLER_API_KEY' )
if not verify_webhook_signature(request, api_key):
return 'Invalid signature' , 401
# Process webhook
process_webhook(request.json)
return 'OK' , 200
Go Implementation
package main
import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" encoding/json "
" net/http "
" os "
" github.com/gin-gonic/gin "
)
func verifyWebhookSignature ( c * gin . Context , apiKey string ) bool {
signature := c . GetHeader ( "x-swim-token" )
if signature == "" {
return false
}
// Get request body
var body map [ string ] interface {}
if err := c . ShouldBindJSON ( & body ); err != nil {
return false
}
// Create expected signature
payload , _ := json . Marshal ( body )
mac := hmac . New ( sha256 . New , [] byte ( apiKey ))
mac . Write ( payload )
expectedSignature := hex . EncodeToString ( mac . Sum ( nil ))
// Compare signatures using constant-time comparison
return hmac . Equal ([] byte ( signature ), [] byte ( expectedSignature ))
}
func handleWebhook ( c * gin . Context ) {
apiKey := os . Getenv ( "POOLER_API_KEY" )
if ! verifyWebhookSignature ( c , apiKey ) {
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "Invalid signature" })
return
}
// Process webhook
var body map [ string ] interface {}
c . ShouldBindJSON ( & body )
processWebhook ( body )
c . JSON ( http . StatusOK , gin . H { "status" : "OK" })
}
PHP Implementation
<? php
function verifyWebhookSignature ( $request , $apiKey ) {
$signature = $request -> header ( 'x-swim-token' );
if ( ! $signature ) {
return false ;
}
// Create expected signature
$payload = json_encode ( $request -> all ());
$expectedSignature = hash_hmac ( 'sha256' , $payload , $apiKey );
// Compare signatures using constant-time comparison
return hash_equals ( $signature , $expectedSignature );
}
// Usage in route
Route :: post ( '/webhooks/pooler' , function ( Request $request ) {
$apiKey = env ( 'POOLER_API_KEY' );
if ( ! verifyWebhookSignature ( $request , $apiKey )) {
return response ( 'Invalid signature' , 401 );
}
// Process webhook
processWebhook ( $request -> all ());
return response ( 'OK' , 200 );
});
Ruby Implementation
require 'openssl'
require 'json'
def verify_webhook_signature ( request , api_key )
signature = request. headers [ 'x-swim-token' ]
return false unless signature
# Create expected signature
payload = request. body . read
request. body . rewind
expected_signature = OpenSSL :: HMAC . hexdigest (
OpenSSL :: Digest . new ( 'sha256' ),
api_key,
payload
)
# Compare signatures using constant-time comparison
ActiveSupport :: SecurityUtils . secure_compare (signature, expected_signature)
end
# Usage in controller
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def pooler
api_key = ENV [ 'POOLER_API_KEY' ]
unless verify_webhook_signature (request, api_key)
return render json: { error: 'Invalid signature' }, status: :unauthorized
end
# Process webhook
process_webhook ( JSON . parse (request. body . read ))
render json: { status: 'OK' }, status: :ok
end
end
Testing Signature Verification
Test with Sample Webhook
const crypto = require ( 'crypto' );
const testWebhook = {
event: 'payment.completed' ,
data: { id: 'payment_123' }
};
const apiKey = 'test_api_key' ;
const payload = JSON . stringify ( testWebhook );
const signature = crypto
. createHmac ( 'sha256' , apiKey )
. update ( payload )
. digest ( 'hex' );
// Simulate request
const req = {
body: testWebhook ,
headers: {
'x-swim-token' : signature
}
};
const isValid = verifyWebhookSignature ( req , apiKey );
console . log ( 'Signature valid:' , isValid );
Issue: JSON Stringification Differences
Different JSON stringification methods can produce different strings:
Wrong - Different Output
Correct - Consistent Output
// This may produce different string than expected
JSON . stringify ( body ) // May include spaces
Security Best Practices
Never skip signature verification, even in development.
Use timing-safe comparison functions to prevent timing attacks.
Use environment variables or secret management services. Never hardcode API keys.
Log failed verifications for security monitoring.
Always use timing-safe comparison functions (e.g., crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to prevent timing attacks.