Webhook Integration
Get instant HTTP notifications when changes are detected.
Overview
Webhooks allow you to receive real-time change notifications at your server endpoint. When a monitor detects a change, Need2Watch sends an HTTP POST request to your registered webhook URL.
Features:
- HMAC-SHA256 signed payloads for security
- Automatic retries with exponential backoff
- Detailed delivery logs
- Custom headers support
Creating a Webhook
curl -X POST https://api.need2.watch/v1/webhooks \
-H "X-API-Key: n2w_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/need2watch"
}'
Response:
{
"id": "wh-1706198400-abc123",
"url": "https://your-app.com/webhooks/need2watch",
"secret": "whsec_3a8f7b2c1d9e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
"active": true,
"created_at": 1706198400000
}
Important: The secret is only returned once during creation. Save it securely - you'll need it to verify webhook signatures.
Webhook Payload
When a change is detected, Need2Watch sends a POST request to your webhook URL:
{
"id": "evt-xyz789",
"type": "change.detected",
"created_at": 1706198400000,
"data": {
"change": {
"id": "chg-abc123",
"monitor_id": "mon-def456",
"monitor_name": "Monitor iPhone 16 Pro price on Amazon",
"detected_at": 1706198400000,
"type": "price",
"source": {
"url": "https://amazon.com/dp/B0DGHG3RGK",
"title": "Apple iPhone 16 Pro 256GB"
},
"before": {
"price": 999.99,
"currency": "USD",
"availability": "In Stock"
},
"after": {
"price": 899.99,
"currency": "USD",
"availability": "In Stock"
},
"relevance": {
"score": 0.95,
"reason": "Price decreased by $100 (10%)"
}
}
}
}
Headers
Each webhook request includes:
POST /webhooks/need2watch HTTP/1.1
Host: your-app.com
Content-Type: application/json
User-Agent: Need2Watch-Webhooks/1.0
X-Need2Watch-Signature: sha256=abc123...
X-Need2Watch-Event: change.detected
X-Need2Watch-Delivery: dlv-xyz789
Verifying Signatures
Always verify webhook signatures to ensure requests are authentic.
Node.js Example
import crypto from 'crypto';
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const receivedSignature = signature.replace('sha256=', '');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
// Express.js webhook handler
app.post('/webhooks/need2watch', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-need2watch-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process the change notification
if (event.type === 'change.detected') {
const change = event.data.change;
console.log(`Price changed from ${change.before.price} to ${change.after.price}`);
// Your business logic here
// - Send email notification
// - Update database
// - Trigger workflow
}
// Acknowledge receipt
res.status(200).send('OK');
});
Python Example
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected_signature = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
received_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, received_signature)
# Flask webhook handler
from flask import Flask, request
@app.route('/webhooks/need2watch', methods=['POST'])
def webhook():
signature = request.headers.get('X-Need2Watch-Signature')
secret = os.environ.get('WEBHOOK_SECRET')
if not verify_webhook_signature(request.data, signature, secret):
return 'Invalid signature', 401
event = request.json
if event['type'] == 'change.detected':
change = event['data']['change']
print(f"Price changed from {change['before']['price']} to {change['after']['price']}")
# Your business logic here
return 'OK', 200
Go Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
)
func verifyWebhookSignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expectedMAC := hex.EncodeToString(mac.Sum(nil))
receivedSignature := signature[7:] // Remove "sha256=" prefix
return hmac.Equal([]byte(expectedMAC), []byte(receivedSignature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Need2Watch-Signature")
secret := os.Getenv("WEBHOOK_SECRET")
if !verifyWebhookSignature(payload, signature, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event WebhookEvent
json.Unmarshal(payload, &event)
if event.Type == "change.detected" {
change := event.Data.Change
fmt.Printf("Price changed from %.2f to %.2f\n",
change.Before.Price, change.After.Price)
// Your business logic here
}
w.WriteHeader(http.StatusOK)
}
Event Types
Currently supported webhook events:
| Event Type | Description |
|---|---|
change.detected | A monitor detected a change on a monitored page |
More event types coming soon:
monitor.created- Monitor createdmonitor.paused- Monitor pausedmonitor.deleted- Monitor deleted
Retry Logic
If your endpoint is unreachable or returns a non-2xx status code, Need2Watch will retry the webhook:
- Retry schedule: 15s, 1m, 5m, 30m, 2h, 12h
- Maximum attempts: 6
- Exponential backoff: Yes
- Timeout: 30 seconds per request
After 6 failed attempts, the webhook is marked as failed and manual re-delivery is required.
Best Practices
Return 200 Quickly
Respond with 200 OK as soon as you receive the webhook. Process the event asynchronously:
app.post('/webhooks/need2watch', async (req, res) => {
// Verify signature first
if (!verifyWebhookSignature(req.body, req.headers['x-need2watch-signature'], secret)) {
return res.status(401).send('Invalid signature');
}
// Acknowledge immediately
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body).catch(err => {
console.error('Webhook processing failed:', err);
});
});
Use HTTPS
Webhooks must use HTTPS endpoints. HTTP URLs are rejected.
Handle Duplicate Events
Webhooks are at-least-once delivery. Use the id field to deduplicate:
const processedEvents = new Set();
function processWebhook(event) {
if (processedEvents.has(event.id)) {
console.log('Duplicate event, skipping');
return;
}
processedEvents.add(event.id);
// Process the event
// ...
}
Monitor Delivery
Check webhook delivery logs in your dashboard:
curl https://api.need2.watch/v1/webhooks/wh-123/deliveries \
-H "X-API-Key: n2w_live_xxxxx"
Response includes delivery attempts, status codes, and error messages.
Testing Webhooks
Local Development with ngrok
Expose your local server to the internet:
# Start your local server
node server.js
# In another terminal, start ngrok
ngrok http 3000
Use the ngrok URL as your webhook endpoint:
curl -X POST https://api.need2.watch/v1/webhooks \
-H "X-API-Key: n2w_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/need2watch"
}'
Test Payload
Trigger a test webhook from the dashboard or API:
curl -X POST https://api.need2.watch/v1/webhooks/wh-123/test \
-H "X-API-Key: n2w_live_xxxxx"
This sends a sample change.detected event to your endpoint.
Managing Webhooks
List Webhooks
curl https://api.need2.watch/v1/webhooks \
-H "X-API-Key: n2w_live_xxxxx"
Deactivate Webhook
curl -X DELETE https://api.need2.watch/v1/webhooks/wh-123 \
-H "X-API-Key: n2w_live_xxxxx"
Regenerate Secret
curl -X POST https://api.need2.watch/v1/webhooks/wh-123/regenerate \
-H "X-API-Key: n2w_live_xxxxx"
Troubleshooting
"Webhook signature verification failed"
- Ensure you're using the raw request body (not parsed JSON)
- Verify the secret matches exactly (no extra spaces)
- Check you're using HMAC-SHA256, not plain SHA256
"Webhook not receiving events"
- Verify webhook is active:
GET /webhooks - Check monitor is active and has detected changes
- Verify your endpoint is publicly accessible
- Check delivery logs for error details
"Connection timeout"
- Ensure your endpoint responds within 30 seconds
- Move processing to background job queue
- Return 200 immediately, process async
Rate Limits
Webhook delivery is not subject to API rate limits. However:
- Maximum 10 webhooks per account
- Maximum 100 deliveries per hour per webhook
Exceeding these limits will result in failed deliveries.
Security Considerations
- Always verify signatures - Prevents spoofed requests
- Use HTTPS - Protects payload in transit
- Rotate secrets - Periodically regenerate webhook secrets
- Whitelist IPs (optional) - Restrict to Need2Watch IP ranges
- Rate limit - Protect your endpoint from abuse
Need2Watch webhook IPs:
# Cloudflare Workers IP ranges (webhooks originate from these)
# See: https://www.cloudflare.com/ips/
Example Integrations
Slack Notification
async function sendSlackNotification(change) {
const message = {
text: `Price Alert!`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*${change.source.title}*\nPrice changed from $${change.before.price} to $${change.after.price}`
}
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "View Product" },
url: change.source.url
}
]
}
]
};
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});
}
Email Notification
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
async function sendEmailNotification(change) {
const client = new SESClient({ region: "us-east-1" });
const command = new SendEmailCommand({
Source: "alerts@your-app.com",
Destination: { ToAddresses: ["user@example.com"] },
Message: {
Subject: { Data: `Price Alert: ${change.source.title}` },
Body: {
Html: {
Data: `
<h2>Price Changed!</h2>
<p><strong>${change.source.title}</strong></p>
<p>Price changed from $${change.before.price} to $${change.after.price}</p>
<p><a href="${change.source.url}">View Product</a></p>
`
}
}
}
});
await client.send(command);
}
Database Update
async function updateDatabase(change) {
await db.priceHistory.create({
monitorId: change.monitor_id,
url: change.source.url,
previousPrice: change.before.price,
currentPrice: change.after.price,
timestamp: change.detected_at
});
// Trigger additional logic if price dropped significantly
const priceDrop = change.before.price - change.after.price;
const dropPercentage = (priceDrop / change.before.price) * 100;
if (dropPercentage > 10) {
await sendPriceDropAlert(change, dropPercentage);
}
}
Next Steps
- API Reference - Full webhook endpoint docs
- Error Reference - Webhook error codes
- Recipes - Webhook integration examples