Skip to main content

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 TypeDescription
change.detectedA monitor detected a change on a monitored page

More event types coming soon:

  • monitor.created - Monitor created
  • monitor.paused - Monitor paused
  • monitor.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"

  1. Ensure you're using the raw request body (not parsed JSON)
  2. Verify the secret matches exactly (no extra spaces)
  3. Check you're using HMAC-SHA256, not plain SHA256

"Webhook not receiving events"

  1. Verify webhook is active: GET /webhooks
  2. Check monitor is active and has detected changes
  3. Verify your endpoint is publicly accessible
  4. Check delivery logs for error details

"Connection timeout"

  1. Ensure your endpoint responds within 30 seconds
  2. Move processing to background job queue
  3. 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

  1. Always verify signatures - Prevents spoofed requests
  2. Use HTTPS - Protects payload in transit
  3. Rotate secrets - Periodically regenerate webhook secrets
  4. Whitelist IPs (optional) - Restrict to Need2Watch IP ranges
  5. 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