# TutorCruncher Webhooks

TutorCruncher sends a webhook for every action on the account (a tutor created, a client
enquiry, an invoice raised, ...). Each request body is `{"events": [...]}` and each element
carries its own `action`.

## When to Use This Skill

* How do I receive TutorCruncher webhooks?
* How do I verify the TutorCruncher `Webhook-Signature` header?
* Why does my TutorCruncher signature check fail, or return 401?
* How do I handle `CREATED_A_CONTRACTOR`, `CREATED_AN_APPOINTMENT` or `CLIENT_ENQUIRY` actions?

## Verification (core)

`Webhook-Signature` is the **bare lowercase hex** HMAC-SHA256 of the **raw request body**,
keyed with your integration's **private API key** (there is no separate webhook secret). No
prefix, no id and no timestamp. The header name matches Standard Webhooks, but this is **not**
Standard Webhooks. Compare the whole header value as one digest.

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

function verifyTutorCruncherSignature(rawBody, signature, secret) {
  // rawBody: Buffer of the exact bytes received (empty Buffer for no body)
  if (!secret || typeof signature !== 'string' || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signature.toLowerCase(), 'utf8');    // hex is case-insensitive
  const wanted = Buffer.from(expected, 'utf8');
  return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}

```

* The key is the secret's UTF-8 bytes as-is (not hex- or base64-decoded).
* Missing or empty header, malformed value or mismatch: `401`.
* Nothing time-based is signed, so there is no replay window to check.
* Read the body raw (`express.raw({ type: '*/*' })`) and parse JSON only after the check.
* Python: `hmac.compare_digest` on **bytes**, never `str`.

**HTTP method:** TutorCruncher's docs don't name the method, so accept `GET`, `POST`, `PUT`,
`PATCH` and `DELETE`, verify the signature first for each, and treat a missing body as zero
bytes (a verified empty body gets `200`). Hookdeck's TutorCruncher source pre-selects POST
without managing the method, so allow any other method the sender uses on the source too.

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

## Hookdeck Setup

Hookdeck's TutorCruncher source type verifies the signature (HMAC, SHA-256, header
`webhook-signature`, hex). Hookdeck shows this note for the source:

"Paste your TutorCruncher private API key. TutorCruncher signs webhooks with it."

## Common Event Types

The event name is the `action` of each element in `events`. Loop over every element.

| `action` | Fired when |
| --- | --- |
| `CREATED_A_CONTRACTOR` | A Tutor is created |
| `CHANGED_CONTRACTOR_STATUS` | A Tutor's status is updated |
| `CREATED_A_CLIENT` | A Client is created |
| `CLIENT_ENQUIRY` | A Client makes an Enquiry |
| `CREATED_A_SERVICE` | A Job is created |
| `CREATED_AN_APPOINTMENT` | A Lesson is created |
| `MARKED_AN_APPOINTMENT_AS_COMPLETE` | A Lesson is marked as complete |
| `RAISED_INVOICE` | An Invoice is sent (once per Invoice) |

> **For the full list**, see the Action Types section of the [TutorCruncher API reference](https://app.tutorcruncher.com/api/docs/).

## Environment Variables

```bash
TUTORCRUNCHER_WEBHOOK_SECRET=your_private_api_key   # the integration's private API key

```

## Delivery and Retries

Timeouts, no response and `5xx` are retried up to 4 times with exponential backoff (after 1 s,
10 s, 100 s and 1000 s). Redirects and `4xx` other than `429` are not retried. An endpoint whose
failure rate stays above 20% is disabled. Each integration keeps webhook logs with request and
response data.

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tutorcruncher-webhooks/references/overview.md) - Webhook concepts, payload shape, common actions
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tutorcruncher-webhooks/references/setup.md) - Add an integration, get the private API key
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tutorcruncher-webhooks/references/verification.md) - Signature details and debugging