Agent skill

TheirStack Webhooks Skill

Receive and verify TheirStack webhooks. Use when setting up TheirStack webhook handlers, debugging X-TheirStack-Signature-256 signature verification, or handling job and technographic events like job.new, company.new, job.closed, or tech.new.

Install this skill

npx skills add hookdeck/webhook-skills --skill theirstack-webhooks


When to Use This Skill

  • How do I receive TheirStack webhooks?
  • How do I verify the X-TheirStack-Signature-256 header?
  • How do I handle job.new, company.new, job.closed, or tech.new events?
  • Why is my TheirStack webhook signature verification failing?
  • Why does TheirStack send no signature header?

Verification (core)

TheirStack signs the raw request body with HMAC-SHA256 keyed on the webhook's signing secret (minimum 16 characters, set or generated on the webhook) and sends X-TheirStack-Signature-256: sha256=<hex>. There is no SDK, so verify manually: require and strip the sha256= prefix, compute the HMAC over the raw body (before JSON parsing), and compare in constant time.

The header is only sent when a signing secret is configured. Without one, deliveries are unsigned and fail verification, so always set a secret.

Node (check for exactly 64 hex chars first: Buffer.from(x, 'hex') silently truncates invalid input):

const crypto = require('crypto');

function verifyTheirStackWebhook(rawBody, signatureHeader, secret) {
  if (!secret || typeof signatureHeader !== 'string') return false;
  if (!signatureHeader.startsWith('sha256=')) return false;
  const received = signatureHeader.slice('sha256='.length);
  if (!/^[0-9a-f]{64}$/i.test(received)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(received, 'hex'), expected);
}

Python (compare bytes so a non-ASCII header gives 401, not a 500 TypeError):

import hmac, hashlib

def verify_theirstack_webhook(raw_body: bytes, signature_header: str | None, secret: str | None) -> bool:
    if not secret or not signature_header or not signature_header.startswith("sha256="):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    received = signature_header[len("sha256="):].lower().encode()
    return hmac.compare_digest(received, expected.encode())

Express: read the body with express.raw({ type: '*/*' }) so any content type verifies.

For complete handlers with route wiring, event dispatch, and tests, see:

Setup with Hookdeck

Set a signing secret on the TheirStack webhook (Signing Secret field, or secret via the Webhooks API). Without one, deliveries are unsigned and fail verification. Use the same secret in your receiver or in the Hookdeck source's webhook secret. See references/setup.md.

Common Event Types

The event name is in the body, not a header. Every delivery is one record in the envelope { "id": ..., "type": "<event>", "payload": {...} }.

Event (type)Triggered WhenPayload
job.newA new job matching the saved search is discoveredFull Job record
company.newA company matches the saved search for the first time (once per webhook)Full Company record
job.closedA matching job is closed or removed by the source{ id, closed_at }
tech.newA company adopts a new technology (access on request)Technology change record

For the full event reference, see TheirStack webhook docs.

Important Headers

HeaderDescription
X-TheirStack-Signature-256sha256=<hex> HMAC-SHA256 of the raw body. Only sent when a signing secret is set

Environment Variables

THEIRSTACK_WEBHOOK_SECRET=your_signing_secret_here   # Signing secret on the TheirStack webhook (min 16 chars)

Delivery Behavior

  • Return any 2xx to acknowledge. Failed deliveries are retried every hour for 48 hours.
  • Be ready for at least 2 concurrent requests and for duplicates; deduplicate on the record id.

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 theirstack --path /webhooks/theirstack

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →