okConfig {
domain: string;
callbackUrl: string;
apiKey: string;
}
async function registerSubscription(config: WebhookConfig) {
const response = await axios.post(
'https://detectzestack.p.rapidapi.com/webhooks',
{
domain: config.domain,
webhook_url: config.callbackUrl
},
{
headers: {
'x-rapidapi-key': config.apiKey,
'x-rapidapi-host': 'detectzestack.p.rapidapi.com',
'Content-Type': 'application/json'
}
}
);
return response.data as {
id: number;
domain: string;
webhook_url: string;
hmac_secret: string;
};
}
**Architecture Decision:** We isolate the registration logic into a dedicated async function rather than embedding it in route handlers. This allows you to run provisioning as a one-time setup script or CI step, keeping runtime code clean. The `hmac_secret` must be persisted in a secure vault or environment configuration immediately upon receipt, as the API never returns it again.
### Step 2: Activate Scheduled Monitoring
Registration alone doesn't trigger automatic scans. You must explicitly set the monitoring interval via a PATCH request. The API supports `daily` or `weekly` cycles. Choosing the right interval depends on your use case: daily for security compliance and active competitive tracking, weekly for long-term trend analysis.
```typescript
async function activateMonitoring(subscriptionId: number, interval: 'daily' | 'weekly', apiKey: string) {
await axios.patch(
`https://detectzestack.p.rapidapi.com/webhooks/${subscriptionId}`,
{ monitor_interval: interval },
{
headers: {
'x-rapidapi-key': apiKey,
'x-rapidapi-host': 'detectzestack.p.rapidapi.com',
'Content-Type': 'application/json'
}
}
);
}
Step 3: Build the Verified Event Pipeline
The receiver endpoint must validate the HMAC signature before processing any payload. Parsing JSON prematurely will alter the raw byte sequence, causing signature verification to fail. The implementation below uses Express middleware to handle verification, then routes the event to a processor.
import express, { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
function verifyHmacMiddleware(req: Request, res: Response, next: NextFunction) {
const signatureHeader = req.headers['x-webhook-signature'] as string;
const secret = process.env.WEBHOOK_HMAC_SECRET;
if (!signatureHeader || !secret) {
return res.status(401).json({ error: 'Missing authentication headers' });
}
const expectedPrefix = 'sha256=';
if (!signatureHeader.startsWith(expectedPrefix)) {
return res.status(401).json({ error: 'Invalid signature format' });
}
const providedSignature = signatureHeader.slice(expectedPrefix.length);
const computedSignature = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(providedSignature, 'hex'),
Buffer.from(computedSignature, 'hex')
)) {
return res.status(403).json({ error: 'Signature mismatch' });
}
next();
}
app.post('/api/techstack-events', verifyHmacMiddleware, async (req: Request, res: Response) => {
res.status(200).send('OK');
const payload = JSON.parse(req.body.toString());
await processTechStackEvent(payload);
});
Architecture Decision: We use express.raw() instead of express.json() to preserve the exact byte sequence for HMAC verification. The crypto.timingSafeEqual method prevents timing attacks. Responding with 200 OK immediately before processing ensures the scanning service marks the delivery as successful, preventing unnecessary retries.
Step 4: Enrich Events with the Change Feed
Webhook payloads contain the current state and metadata, but structured diffs require a separate API call. The /changes endpoint returns added, removed, or version-shifted technologies. We fetch this context asynchronously after acknowledging the webhook.
async function processTechStackEvent(payload: any) {
const { domain, event } = payload;
if (event === 'tech_stack.analyzed') {
const changes = await fetchDomainChanges(domain);
await routeAlertToSlack(domain, changes);
}
}
async function fetchDomainChanges(domain: string) {
const response = await axios.get(
`https://detectzestack.p.rapidapi.com/changes`,
{
params: { domain },
headers: {
'x-rapidapi-key': process.env.RAPIDAPI_KEY!,
'x-rapidapi-host': 'detectzestack.p.rapidapi.com'
}
}
);
return response.data;
}
Pitfall Guide
Building webhook pipelines seems straightforward until production conditions expose edge cases. The following mistakes are common in early implementations and their production-grade fixes.
1. Parsing JSON Before HMAC Verification
Explanation: Express's json() middleware parses the request body into an object, which alters whitespace, key ordering, and encoding. The HMAC is computed against the raw byte stream, so verification will always fail if you parse first.
Fix: Use express.raw() or express.text() for the webhook route, verify the signature against the raw buffer, then parse JSON only after validation passes.
2. Ignoring Idempotency Requirements
Explanation: The scanning service retries failed deliveries up to three times with exponential backoff (2 seconds, then 4 seconds). If your endpoint crashes mid-processing or returns a 5xx status, you'll receive duplicate payloads. Processing them twice can trigger duplicate Slack alerts or database writes.
Fix: Implement an idempotency layer using the event timestamp, domain, and a hash of the payload. Store processed event IDs in Redis or a database with a short TTL, and skip processing if the key already exists.
3. Hardcoding or Committing HMAC Secrets
Explanation: Developers often paste the hmac_secret directly into source code or .env files committed to version control. This exposes cryptographic verification keys to anyone with repository access.
Fix: Inject secrets via a dedicated vault (AWS Secrets Manager, HashiCorp Vault, or CI/CD secret stores). Load them at runtime and never log or expose them in error responses.
4. Confusing tech_stack.analyzed and tech_stack.changed Events
Explanation: Every scan fires tech_stack.analyzed with the full technology list. Only material shifts trigger tech_stack.changed. Treating both identically causes noise and redundant processing.
Fix: Route analyzed events to logging or baseline storage. Reserve changed events for alerting pipelines. If you need diffs, always query the /changes endpoint regardless of event type.
5. Using Private or Localhost Callback URLs
Explanation: The API explicitly rejects HTTP endpoints and private IP ranges (127.0.0.1, 10.x.x.x, 192.168.x.x). Webhooks require publicly routable HTTPS URLs to prevent credential leakage and ensure delivery.
Fix: Deploy the receiver behind a public load balancer or use a tunneling service like ngrok or Cloudflare Tunnel during development. Ensure TLS termination is properly configured.
6. Blocking the Response Thread with Heavy Processing
Explanation: If your route handler performs database writes, API calls, or Slack notifications synchronously, the response takes longer than the scanning service's timeout window. This triggers retries and degrades delivery reliability.
Fix: Acknowledge the webhook immediately with 200 OK, then offload processing to a message queue (RabbitMQ, SQS, BullMQ) or an async worker pool.
7. Over-Provisioning Monitoring Intervals
Explanation: Setting every domain to daily monitoring when only a few require real-time tracking wastes API quota and increases noise. The pricing tiers cap subscriptions (Basic: 5, Pro: 20, Ultra: 50, Mega: 100), so inefficient allocation hits limits faster.
Fix: Classify domains by sensitivity. Use daily for security-critical assets and active competitors. Use weekly for long-term trend tracking. Disable automatic scanning for low-priority domains and trigger manual scans via API when needed.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Security compliance tracking | Daily monitoring + immediate Slack alerts | Vulnerabilities like dropped CSP headers require <24h response | Higher API usage, justified by risk reduction |
| Competitive intelligence | Daily monitoring + change feed enrichment | Framework shifts and CDN migrations signal strategic moves | Moderate cost, high strategic value |
| Long-term trend analysis | Weekly monitoring + database logging | Infrastructure evolution rarely requires daily granularity | Lower API consumption, minimal alert noise |
| Budget-constrained tracking | Manual triggers + Basic tier ($0/mo) | Free tier supports 5 subscriptions but lacks auto-scanning | Zero recurring cost, requires manual intervention |
| High-volume portfolio | Mega tier ($79/mo) + async event queue | 100 subscription limit accommodates large tracking lists | Higher tier cost, scales efficiently with queue architecture |
Configuration Template
// webhook-server.ts
import express from 'express';
import crypto from 'crypto';
import { processEvent } from './event-processor';
const app = express();
const PORT = process.env.PORT || 3000;
// Preserve raw body for HMAC verification
app.use(express.raw({ type: 'application/json', limit: '1mb' }));
app.post('/hooks/techstack', async (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const secret = process.env.WEBHOOK_HMAC_SECRET;
if (!signature?.startsWith('sha256=') || !secret) {
return res.status(401).json({ error: 'Unauthorized' });
}
const provided = signature.slice(7);
const computed = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(provided, 'hex'), Buffer.from(computed, 'hex'))) {
return res.status(403).json({ error: 'Invalid signature' });
}
// Acknowledge immediately to prevent retries
res.status(200).send('Accepted');
// Process asynchronously
const payload = JSON.parse(req.body.toString());
await processEvent(payload);
});
app.listen(PORT, () => {
console.log(`Webhook receiver listening on port ${PORT}`);
});
Quick Start Guide
- Create an account on DetectZeStack and generate an API key. Select a tier that matches your domain count (Pro at $9/mo covers 20 domains with daily/weekly scanning).
- Register your first domain by sending a
POST request to /webhooks with your target domain and a publicly routable HTTPS callback URL. Save the returned hmac_secret in your environment variables.
- Deploy the receiver using the configuration template above. Ensure your server is accessible over HTTPS and responds to
POST requests at the registered path.
- Activate monitoring by patching the subscription with
{"monitor_interval": "daily"} or {"monitor_interval": "weekly"}. The service will begin scanning according to your schedule.
- Test the pipeline by manually triggering a scan via the API or waiting for the first scheduled run. Verify that your endpoint receives the payload, passes HMAC validation, and routes the event to your alerting system.