Setting Up Webhook Alerts for Regulation Changes
This guide walks you through registering a webhook endpoint, verifying signatures, and handling regulation change events. Webhooks are available on the Enterprise tier.
Why Use Webhooks?
Polling the API for changes works, but webhooks give you:
- Real-time alerts — Know within minutes when a state changes its rules
- Reduced API calls — No need to poll repeatedly
- Actionable summaries — Every event includes a plain-English explanation of what changed
- Filtered delivery — Only receive events for the states and substances you care about
Step 1: Set Up Your Endpoint
Create an HTTPS endpoint on your server that accepts POST requests. It must:
- Use HTTPS (HTTP endpoints are rejected)
- Return a
200status code within 5 seconds - Accept
application/jsoncontent type
Express.js Example
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.HEMPDATA_WEBHOOK_SECRET;
app.post('/webhooks/hempdata', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verify signature
const signature = req.headers['x-hempdata-signature'];
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body, 'utf8')
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'))) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// 2. Parse and acknowledge immediately
const event = JSON.parse(req.body);
res.status(200).send('OK');
// 3. Process asynchronously
processRegulationChange(event).catch(console.error);
});
async function processRegulationChange(event) {
console.log(`[${event.event_type}] ${event.jurisdiction.state}: ${event.plain_english_summary}`);
switch (event.event_type) {
case 'legal_status_changed':
// Critical — a product may now be illegal in a state
await notifyComplianceTeam(event);
if (event.after.legal_status === 'prohibited') {
await pauseShipmentsToState(event.jurisdiction.state, event.substance, event.product_type);
}
break;
case 'details_updated':
// Requirements changed (age, labeling, limits)
await logComplianceUpdate(event);
break;
case 'new_regulation':
// New regulation added for a substance/state combo
await reviewNewRegulation(event);
break;
case 'bill_linked':
// Heads-up: pending legislation that may change things
await flagPendingLegislation(event);
break;
}
}
app.listen(3000);
Flask (Python) Example
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["HEMPDATA_WEBHOOK_SECRET"]
@app.route("/webhooks/hempdata", methods=["POST"])
def handle_webhook():
# 1. Verify signature
signature = request.headers.get("X-HempData-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
request.data,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, "Invalid signature")
# 2. Acknowledge immediately
event = json.loads(request.data)
# 3. Process (in production, queue this for async processing)
handle_event(event)
return "OK", 200
def handle_event(event):
event_type = event["event_type"]
state = event["jurisdiction"]["state"]
summary = event["plain_english_summary"]
print(f"[{event_type}] {state}: {summary}")
if event_type == "legal_status_changed" and event["after"]["legal_status"] == "prohibited":
# Trigger emergency compliance review
print(f"ALERT: {event['substance']} {event['product_type']} now prohibited in {state}")
if __name__ == "__main__":
app.run(port=3000, ssl_context="adhoc")
Step 2: Register the Webhook
curl -X POST "https://api.hempdataapi.com/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://yourapp.com/webhooks/hempdata",
"filters": {
"states": ["CO", "CA", "TX", "FL", "NY", "WA", "OR"],
"substances": ["delta-9-thc", "delta-8-thc", "cbd"],
"event_types": ["legal_status_changed", "details_updated", "new_regulation"]
},
"description": "Production compliance alerts for key sales states"
}'
Save the signing_secret from the response. It is only shown once.
{
"success": true,
"data": {
"id": "whk_a1b2c3d4e5f6",
"signing_secret": "whsec_live_k8J2mN4pQ7rT1vX3zA5cE8gI0lO2qU4wY6bD9fH",
"status": "active"
}
}
Step 3: Test Your Endpoint
You can verify your endpoint is working by checking the webhook list for delivery stats:
curl -X GET "https://api.hempdataapi.com/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY"
Look at total_deliveries and failed_deliveries to confirm events are being received.
Retry Behavior
If your endpoint fails, HempData retries on this schedule:
| Attempt | Wait |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
| 6 | 24 hours |
After 6 failures, the event is marked as failed. After 50 consecutive failures across any events, the subscription is paused.
Best Practices
- Respond fast, process later — Return
200immediately and handle the event in a background job queue (Bull, Celery, etc.). - Deduplicate with
event_id— Retries may deliver the same event twice. Use theevent_idto skip duplicates. - Always verify signatures — Never trust a payload without checking
X-HempData-Signature. - Use specific filters — Only subscribe to the states and substances relevant to your business to reduce noise.
- Monitor delivery health — Check the dashboard regularly for failed deliveries.
- Alert on
legal_status_changed— This is the most critical event. Wire it to high-priority alerts (Slack, PagerDuty, email).
Common Patterns
Slack Notification on Status Change
async function notifySlack(event) {
const color = event.after.legal_status === 'prohibited' ? '#dc2626' : '#eab308';
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
color,
title: `Regulation Change: ${event.jurisdiction.state_name}`,
text: event.plain_english_summary,
fields: [
{ title: 'Substance', value: event.substance, short: true },
{ title: 'Product Type', value: event.product_type, short: true },
{ title: 'Before', value: event.before.legal_status, short: true },
{ title: 'After', value: event.after.legal_status, short: true },
],
ts: Math.floor(new Date(event.timestamp).getTime() / 1000),
}],
}),
});
}
Database Audit Log
async function logToDatabase(event) {
await db.query(
`INSERT INTO regulation_changes (event_id, event_type, state, substance, product_type, before_status, after_status, summary, detected_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
event.event_id,
event.event_type,
event.jurisdiction.state,
event.substance,
event.product_type,
event.before?.legal_status,
event.after?.legal_status,
event.plain_english_summary,
event.timestamp,
]
);
}