# INGENIOUS.BUILD Webhooks

INGENIOUS.BUILD (construction project management) pushes approved financial records to your
accounting middleware through the Custom Accounting Integration. You configure one webhook URL per
object type (Budgets, Budget Changes, Contracts, Contract Changes, Invoices). A webhook fires only
when a user clicks **Sync to Accounting** on an approved record.

Requests are **not signed**. You authenticate them with a secret header that you add to each
webhook URL in the integration settings, and your endpoint compares that header with the value
it has stored.

Setup note (same as Hookdeck's INGENIOUS.BUILD source): "Add an auth header, such as Authorization: Bearer <token>, to each webhook URL on the Custom Accounting Integration."

## When to Use This Skill

* How do I receive INGENIOUS.BUILD webhooks?
* How do I authenticate INGENIOUS.BUILD Custom Accounting Integration requests?
* Does INGENIOUS.BUILD sign webhook payloads? (No: use the auth header you configure)
* How do I handle `invoice_exported_to_accounting` or `contract_exported_to_accounting`?
* Why does my INGENIOUS.BUILD webhook endpoint return 401?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyIngeniousBuildRequest(
  headers,
  name = process.env.INGENIOUS_BUILD_WEBHOOK_HEADER || 'Authorization',
  secret = process.env.INGENIOUS_BUILD_WEBHOOK_SECRET
) {
  if (!secret) return false;                        // never accept when unconfigured
  const received = headers[name.toLowerCase()];     // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret); // WHOLE value, no 'Bearer ' parsing
}

```

* The header name is whatever you typed in the integration (`Authorization` by default here). Look
  it up case-insensitively and compare the **whole** value to `INGENIOUS_BUILD_WEBHOOK_SECRET`,
  including any `Bearer ` prefix you typed. Do not strip or parse the prefix.
* Return `401` for a missing header or a mismatch, `400` for a body that isn't a JSON object,
  `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse it only
  after the header check.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (there is no signature and no timestamp). Use HTTPS only and rotate the credential
  if it leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ingenious-build-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ingenious-build-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ingenious-build-webhooks/examples/fastapi/).

## Event Types

Every request is a `POST` with a JSON body `{ name, subject, data }`. Dispatch on `name`.

| `name` | `subject` | Sent When |
| --- | --- | --- |
| `budget_exported_to_accounting` | `budget` | Sync to Accounting is clicked on a Budget |
| `budget_change_exported_to_accounting` | `budget_change` | ... on a Budget Change |
| `contract_exported_to_accounting` | `contract` | ... on a Contract |
| `contract_change_exported_to_accounting` | `contract_change` | ... on a Contract Change |
| `invoice_exported_to_accounting` | `invoice` | ... on an Invoice |

## Environment Variables

```bash
INGENIOUS_BUILD_WEBHOOK_HEADER=Authorization       # header name you added to each webhook URL
INGENIOUS_BUILD_WEBHOOK_SECRET="Bearer your-token" # exact, whole header value

```

## Local Development

INGENIOUS.BUILD can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 ingenious-build --path /webhooks/ingenious-build

```

Use the printed URL as a webhook URL in the Custom Accounting Integration.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ingenious-build-webhooks/references/overview.md) - What the webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ingenious-build-webhooks/references/setup.md) - Enable the integration, add URLs and the auth header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ingenious-build-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ingenious-build-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests