Skip to content

Validating Webhook Signatures

Developers integrating with Crewsum webhooks need to validate incoming requests to confirm they originate from Crewsum. This guide explains how to implement signature validation using the x-crewsum-signature and x-timestamp headers with your webhook secret key. By following these steps, you protect your application from unauthorized webhook requests and potential security threats.

Before implementing signature validation, ensure you have:

  • A Crewsum webhook secret key (available in your webhook configuration)
  • Node.js with Express.js or your preferred web framework
  • Access to cryptographic libraries (built-in crypto module for Node.js)

Crewsum signs each webhook request using HMAC-SHA256. The signature combines a timestamp and the request body, ensuring both integrity and timeliness.

  • Timestamp: Unix timestamp in the x-timestamp header prevents replay attacks
  • Signature: HMAC-SHA256 hash in the x-crewsum-signature header
  • Payload: Concatenation of timestamp and raw request body separated by a dot
  1. Extract timestamp and signature from headers
  2. Verify timestamp is within acceptable time window (5 minutes)
  3. Reconstruct payload using timestamp and raw body
  4. Generate expected signature using your webhook key
  5. Compare signatures using timing-safe equality
import express, { Request, Response } from "express";
import crypto from "crypto";
const app = express();
// Parse JSON bodies but keep raw body for signature validation
app.use(
express.json({
verify: (req: Request, res: Response, buf: Buffer) => {
(req as any).rawBody = buf;
},
})
);
const WEBHOOK_SECRET =
process.env.CREWSUM_WEBHOOK_SECRET || "your-webhook-secret";
function validateWebhookSignature(req: Request): boolean {
const timestamp = req.headers["x-timestamp"] as string;
const signature = req.headers["x-crewsum-signature"] as string;
// Validate required headers and body
if (!timestamp || !signature || !(req as any).rawBody) {
console.error("Missing required headers or body for signature validation");
return false;
}
const currentTime = Math.floor(Date.now() / 1000);
const requestTime = parseInt(timestamp, 10);
// Check timestamp is within 5-minute window
if (Math.abs(currentTime - requestTime) > 300) {
console.error("Webhook timestamp is outside acceptable time window");
return false;
}
// Reconstruct payload: timestamp.body
const payload = `${timestamp}.${(req as any).rawBody.toString()}`;
// Generate expected signature
const expectedSignature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post("/webhooks/crewsum", (req: Request, res: Response) => {
// Validate signature first
if (!validateWebhookSignature(req)) {
return res.status(401).json({
error: "Invalid webhook signature",
});
}
// Process validated webhook
const { event, data } = req.body;
// Handle different event types
switch (event) {
case "project.created":
// Handle project creation
break;
case "timesheet.submitted":
// Handle timesheet submission
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.status(200).json({ status: "ok" });
});
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});
  • Store webhook secrets securely: Use environment variables, never hardcode them
  • Validate timestamps: Prevents replay attacks by rejecting old requests
  • Use timing-safe comparison: Avoids timing attack vulnerabilities
  • Handle raw body correctly: Ensure you access the unmodified request body
  • Implement proper error handling: Don’t expose internal details in error responses
  • Monitor failed validations: Log and alert on repeated signature failures

Test your validation with sample webhooks from your Crewsum dashboard. Verify that:

  • Valid signatures are accepted
  • Invalid signatures are rejected with 401 status
  • Requests with expired timestamps are rejected
  • Requests missing headers are handled gracefully

This implementation ensures your webhook endpoints remain secure and only process legitimate requests from Crewsum.