Digital Empire › Webhooks docs
Register a URL (Zapier "Catch Hook", n8n webhook node, Make custom webhook, or your own HTTP endpoint) to receive signed JSON callbacks whenever a scan finishes, an HTS code matches a new notice, an order ships, or a subscription event fires. HMAC-SHA256 signed. SSRF-hardened egress (private IPs and metadata endpoints are refused at registration time).
/api/customer/webhooks with your email, token, target product, and webhook_url.secret. Save it now -- it is shown once and used to verify every subsequent delivery.Every delivery uses the same envelope: { id, event, product, occurred_at, data }. Pass event_types: [] (or omit) to receive all events for the chosen product; pass an explicit list (e.g. ["pixelproof.scan.complete"]) to filter.
pixelproof.scan.completeproduct: pixelproofFires when a Chrome-extension scan finishes and its snapshot is written.
{
"id": "evt_c7f4...",
"event": "pixelproof.scan.complete",
"product": "pixelproof",
"occurred_at": "2026-08-26T12:34:56.000Z",
"data": {
"store_domain": "acme-cosmetics.myshopify.com",
"scan_type": "pixel",
"error_count": 2,
"warn_count": 1
}
}tariffwatch.hts.matchedproduct: tariffwatchFires when a new BIS Federal Register notice matches one of the customer's watched HTS codes.
{
"id": "evt_9a12...",
"event": "tariffwatch.hts.matched",
"product": "tariffwatch",
"occurred_at": "2026-08-26T12:34:56.000Z",
"data": {
"hts_code": "7208.10.00",
"notice_id": "2026-15961",
"notice_title": "Section 232 derivative products expansion"
}
}entryproof.order.shippedproduct: entryproofFires when an EntryProof filing / GCC packet is packaged and dispatched to the customer.
{
"id": "evt_4b8e...",
"event": "entryproof.order.shipped",
"product": "entryproof",
"occurred_at": "2026-08-26T12:34:56.000Z",
"data": {
"order_id": "ord_ep_1a2b3c",
"packet_type": "gcc"
}
}founding_trio.claimedproduct: allFires when a Founding Trio slot is claimed (Stripe checkout completed).
{
"id": "evt_2d3e...",
"event": "founding_trio.claimed",
"product": "all",
"occurred_at": "2026-08-26T12:34:56.000Z",
"data": {
"cohort_slot": 3,
"cohort_cap": 5,
"customer_email": "buyer@example.com",
"primary_product": "pixelproof"
}
}Every request includes four headers you MUST verify before trusting the payload:
X-Digital-Empire-Event-Id -- stable UUID for idempotency.X-Digital-Empire-Event-Type -- e.g. pixelproof.scan.complete.X-Digital-Empire-Timestamp -- Unix seconds; reject if drift > 5 min.X-Digital-Empire-Signature -- sha256=<hex> HMAC of {timestamp}.{raw_body}.import crypto from 'node:crypto';
function verify(req) {
const sig = req.headers['x-digital-empire-signature']; // "sha256=<hex>"
const ts = req.headers['x-digital-empire-timestamp'];
const raw = req.rawBody; // exact JSON body bytes we POSTed
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.DIGITAL_EMPIRE_WEBHOOK_SECRET)
.update(`${ts}.${raw}`)
.digest('hex');
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('stale');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) throw new Error('bad sig');
}import hmac, hashlib, os, time
def verify(headers, raw_body: bytes):
sig = headers["X-Digital-Empire-Signature"]
ts = headers["X-Digital-Empire-Timestamp"]
if abs(time.time() - int(ts)) > 300:
raise ValueError("stale")
expected = "sha256=" + hmac.new(
os.environ["DIGITAL_EMPIRE_WEBHOOK_SECRET"].encode(),
f"{ts}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(sig, expected):
raise ValueError("bad sig")import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strconv"
"time"
)
func verify(headers map[string]string, rawBody []byte) error {
sig := headers["X-Digital-Empire-Signature"]
ts := headers["X-Digital-Empire-Timestamp"]
tsInt, _ := strconv.ParseInt(ts, 10, 64)
if time.Now().Unix() - tsInt > 300 || tsInt - time.Now().Unix() > 300 {
return fmt.Errorf("stale")
}
mac := hmac.New(sha256.New, []byte(os.Getenv("DIGITAL_EMPIRE_WEBHOOK_SECRET")))
mac.Write([]byte(ts + "." + string(rawBody)))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
return fmt.Errorf("bad sig")
}
return nil
}require 'openssl'
def verify(headers, raw_body)
sig = headers['X-Digital-Empire-Signature'] # "sha256=<hex>"
ts = headers['X-Digital-Empire-Timestamp'].to_i
raise 'stale' if (Time.now.to_i - ts).abs > 300
expected = 'sha256=' + OpenSSL::HMAC.hexdigest(
'SHA256',
ENV.fetch('DIGITAL_EMPIRE_WEBHOOK_SECRET'),
"#{ts}.#{raw_body}"
)
# Timing-safe equality via OpenSSL.
raise 'bad sig' unless OpenSSL.fixed_length_secure_compare(sig, expected)
end<?php
function verify(array $headers, string $rawBody): void {
$sig = $headers['X-Digital-Empire-Signature'] ?? ''; // "sha256=<hex>"
$ts = (int) ($headers['X-Digital-Empire-Timestamp'] ?? 0);
if (abs(time() - $ts) > 300) {
throw new RuntimeException('stale');
}
$expected = 'sha256=' . hash_hmac(
'sha256',
$ts . '.' . $rawBody,
getenv('DIGITAL_EMPIRE_WEBHOOK_SECRET') ?: ''
);
if (!hash_equals($expected, $sig)) {
throw new RuntimeException('bad sig');
}
}failure_count. After 5 consecutive failures the webhook is auto-disabled and stops firing until you re-enable it via the PATCH endpoint (which also resets the counter).http:// and https://. Only ports 80 / 443. HTTPS strongly recommended in production.crypto.randomBytes. It is returned once in the POST response, never re-listed on GET.