# 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):

```javascript
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`):

```python
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:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/theirstack-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/theirstack-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/theirstack-webhooks/examples/fastapi/)

## 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/theirstack-webhooks/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 When | Payload |
| --- | --- | --- |
| `job.new` | A new job matching the saved search is discovered | Full Job record |
| `company.new` | A company matches the saved search for the first time (once per webhook) | Full Company record |
| `job.closed` | A matching job is closed or removed by the source | `{ id, closed_at }` |
| `tech.new` | A company adopts a new technology (access on request) | Technology change record |

> **For the full event reference**, see [TheirStack webhook docs](https://theirstack.com/en/docs/webhooks).

## Important Headers

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

## Environment Variables

```bash
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

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/theirstack-webhooks/references/overview.md) - TheirStack webhook concepts and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/theirstack-webhooks/references/setup.md) - Webhook and signing secret configuration
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/theirstack-webhooks/references/verification.md) - Signature verification details