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 200 status code within 5 seconds
  • Accept application/json content 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:

AttemptWait
11 minute
25 minutes
330 minutes
42 hours
512 hours
624 hours

After 6 failures, the event is marked as failed. After 50 consecutive failures across any events, the subscription is paused.

Best Practices

  1. Respond fast, process later — Return 200 immediately and handle the event in a background job queue (Bull, Celery, etc.).
  2. Deduplicate with event_id — Retries may deliver the same event twice. Use the event_id to skip duplicates.
  3. Always verify signatures — Never trust a payload without checking X-HempData-Signature.
  4. Use specific filters — Only subscribe to the states and substances relevant to your business to reduce noise.
  5. Monitor delivery health — Check the dashboard regularly for failed deliveries.
  6. 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,
    ]
  );
}