Implementation
Code examples and best practices for implementing webhook handlers.
Framework Examples
Node.js (Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
// Capture raw body for signature verification
app.use(express.json({
verify: (req: any, _res, buf) => { req.rawBody = buf; }
}));
function isValidSignature(rawBody: Buffer, signature: string, secret: string) {
const computed = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return (
computed.length === (signature || '').length &&
crypto.timingSafeEqual(Buffer.from(computed, 'hex'), Buffer.from(signature || '', 'hex'))
);
}
app.post('/webhooks', async (req: any, res) => {
const signature = req.header('X-Zellify-Signature') || '';
const secret = process.env.ZELLIFY_WEBHOOK_SECRET || '';
if (!isValidSignature(req.rawBody, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
// Acknowledge quickly
res.status(200).json({ received: true });
// Process asynchronously
process.nextTick(async () => {
try {
await WebhookHandler.processEvent(event);
} catch (err) {
console.error('Webhook processing error:', err);
}
});
});
app.listen(3000, () => console.log('Webhook server running on 3000'));
Python (Flask)
from flask import Flask, request, jsonify
import hmac, hashlib, os, threading
app = Flask(__name__)
def valid_signature(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(secret.encode('utf-8'), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature or '')
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Zellify-Signature', '')
payload = request.get_data() # raw bytes
secret = os.environ.get('ZELLIFY_WEBHOOK_SECRET', '')
if not valid_signature(payload, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
event = request.json
# Acknowledge quickly
response = jsonify({'received': True})
# Process in background
threading.Thread(target=WebhookHandler.process_event, args=(event,)).start()
return response, 200
PHP (Laravel-style controller)
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handle(Request $request)
{
$payload = $request->getContent();
$signature = $request->header('X-Zellify-Signature', '');
$secret = env('ZELLIFY_WEBHOOK_SECRET', '');
$computed = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($computed, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
// Ack early
$response = response()->json(['received' => true]);
// Process in background (queue preferred)
$event = json_decode($payload, true);
dispatch(function () use ($event) {
WebhookHandler::processEvent($event);
});
return $response;
}
}
Java (Spring Boot)
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@RestController
public class WebhookController {
@PostMapping("/webhooks")
public ResponseEntity<?> handleWebhook(@RequestHeader("X-Zellify-Signature") String signature,
@RequestBody String payload) {
try {
String secret = System.getenv("ZELLIFY_WEBHOOK_SECRET");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] computed = hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String computedHex = bytesToHex(computed);
if (!MessageDigest.isEqual(computedHex.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8))) {
return ResponseEntity.status(401).body("Invalid signature");
}
// Ack early
ResponseEntity<?> ack = ResponseEntity.ok("{\"received\":true}");
// Async processing
new Thread(() -> {
try {
WebhookHandler.processEvent(payload);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
return ack;
} catch (Exception e) {
return ResponseEntity.status(500).body(e.getMessage());
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
String h = Integer.toHexString(0xff & b);
if (h.length() == 1) hex.append('0');
hex.append(h);
}
return hex.toString();
}
}
Go (net/http)
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
)
type resp struct { Received bool `json:"received"`; Error string `json:"error,omitempty"` }
func handleWebhook(w http.ResponseWriter, r *http.Request) {
sig := r.Header.Get("X-Zellify-Signature")
body, _ := io.ReadAll(r.Body)
secret := os.Getenv("ZELLIFY_WEBHOOK_SECRET")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
computed := hex.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(computed), []byte(sig)) != 1 {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(resp{Error: "Invalid signature"})
return
}
// Ack early
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp{Received: true})
// Async processing
go func(b []byte) {
var event map[string]any
if err := json.Unmarshal(b, &event); err != nil {
log.Println("Invalid JSON:", err)
return
}
if err := processEvent(event); err != nil { log.Println("Webhook error:", err) }
}(body)
}
func main() {
http.HandleFunc("/webhooks", handleWebhook)
log.Fatal(http.ListenAndServe(":3000", nil))
}
Last updated