Gareth Wilson Gareth Wilson

Guide to TutorCruncher Webhooks: Features and Best Practices

Published


TutorCruncher is business management software for tutoring agencies, covering tutors, clients, lessons, billing and payroll. Its webhooks push every action on the account to another system, which agencies use with Zapier or their own apps to sync a CRM, alert staff to new enquiries or start payroll when lessons are completed.

This guide covers how TutorCruncher integrations send webhooks, the action list and payload, verification with the private API key, and the retry and auto-disable rules that should shape your responses.

What are TutorCruncher webhooks?

A TutorCruncher webhook belongs to an integration on your account. You add the integration's URL, and from then on TutorCruncher posts a webhook every time an action involves a user or a Job: a tutor is created, a client submits an enquiry, a lesson is marked complete, an invoice is raised. There's no per-action subscription, so your handler receives everything and decides what to ignore.

Each request carries an events array, and each element describes one action.

TutorCruncher webhook features

FeatureDetails
Signaturehex HMAC-SHA256
Signature headerwebhook-signature
BodyJSON
Event routingBody field events[].action

From TutorCruncher's help article and the webhooks section of its API documentation:

  • Each integration has webhook logs showing when each webhook fired, whether it succeeded, and the request and response data.
  • Timeouts, no response and 5xx responses are retried up to 4 times, after 1, 10, 100 and 1,000 seconds.
  • Redirects and 4xx responses other than 429 aren't retried.
  • An endpoint whose failure rate stays above 20% over a sustained period is disabled automatically.

Events and payloads

Route on action in each element of events. TutorCruncher lists over 200 action types. Some common ones:

| Event routing | Body field events[].action |

Event Payload Structure

  • action: the upper-snake event type. verb: a readable description.
  • actor: who performed the action (url is an empty string for administrators).
  • subject: the item the action was performed on. Its fields depend on subject.model.
  • extra_msg: how the action happened, e.g. "Created via the API." (generally empty for UI actions).
  • Always loop over every element of events.

Actions TutorCruncher triggers by itself have extra_msg set to "Processed automatically via TutorCruncher". Each event also has a timestamp and a branch ID. The help article's example additionally shows a top-level _request_time in Unix seconds and a much fuller subject for a tutor, including user, skills and extra_attrs.

The CREATED_A_CONTRACTOR example from the API documentation:

{
  "events": [
    {
      "action": "CREATED_A_CONTRACTOR",
      "verb": "Created a Tutor",
      "timestamp": "2025-03-05T15:26:20.558919Z",
      "branch": 590,
      "actor": {
        "name": "Jane Smith",
        "id": 1621,
        "user_id": 1611,
        "url": ""
      },
      "extra_msg": "Created via the API.",
      "subject": {
        "model": "Tutor",
        "url": "https://app.tutorcruncher.com/api/contractors/1623/",
        "id": 1623,
        "status": "approved",
        "last_name": "Gallagher",
        "email": "liam@example.com"
      }
    }
  ]
}

"Contractor" is TutorCruncher's API name for a tutor, and "service" and "appointment" are its names for a job and a lesson.

Setting up TutorCruncher webhooks

  1. In TutorCruncher, open System > Settings > Integrations.
  2. Click Add Integration.
  3. Enter your webhook URL, for example https://example.com/webhooks/tutorcruncher, and save.
  4. Copy the integration's private API key and store it as TUTORCRUNCHER_PRIVATE_KEY. It's the signing key, as there's no separate webhook secret.

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

To route through Hookdeck, create a TutorCruncher source with the private API key and use its URL as the integration's webhook URL. For local development:

npx hookdeck-cli listen 3000 tutorcruncher --path /webhooks/tutorcruncher

HTTP method

Neither TutorCruncher page names the HTTP method or Content-Type. Hookdeck's TutorCruncher source pre-selects POST without managing the method, so allow other methods on the source if you see them. The receiver below uses app.all, verifies whatever method arrives, and acknowledges a verified empty body with 200.

Securing TutorCruncher webhooks

Webhook-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the integration's private API key. TutorCruncher's API docs show a Python check with hmac.compare_digest. The help article's sample uses assert with ==, which isn't constant-time and raises an error instead of returning 401.

Despite the header's name, this isn't the Standard Webhooks scheme: there are no webhook-id or webhook-timestamp headers, and nothing time-based is signed.

Node.js verification

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['webhook-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler loops over every event, builds a key from the action, time and subject, and queues the ones it handles. Unknown actions still get a 200.

const express = require('express');

const app = express();
const HANDLED = new Set(['CLIENT_ENQUIRY', 'MARKED_AN_APPOINTMENT_AS_COMPLETE', 'RAISED_INVOICE']);

app.all('/webhooks/tutorcruncher', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.TUTORCRUNCHER_PRIVATE_KEY;
  if (!secret) return res.sendStatus(500);
  const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
  if (!verifySignature(body, req.headers, secret)) return res.sendStatus(401);
  if (!body.length) return res.sendStatus(200);

  let payload;
  try {
    payload = JSON.parse(body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!payload || typeof payload !== 'object' || !Array.isArray(payload.events)) {
    return res.sendStatus(400);
  }

  const jobs = payload.events
    .filter((e) => e && typeof e === 'object' && HANDLED.has(e.action))
    .map((e) => {
      const subject = e.subject && typeof e.subject === 'object' ? e.subject : {};
      return {
        key: [e.action, e.timestamp, subject.model, subject.id].map(String).join('|'),
        action: e.action,
        model: subject.model,
        subjectId: subject.id,
        branch: e.branch,
      };
    });

  try {
    for (const job of jobs) {
      if (await alreadyHandled(job.key)) continue;
      await queue.add('tutorcruncher-action', job);
      await markHandled(job.key);
    }
  } catch {
    return res.sendStatus(503);
  }
  res.sendStatus(200);
});

alreadyHandled and markHandled stand in for whatever dedupe store you use, and queue for your job queue. Fetch full records through the TutorCruncher API from subject.url in the worker when you need more than the webhook carries.

The TutorCruncher source type in Hookdeck applies this same verification. Hookdeck answers TutorCruncher with 200 and marks a mismatched request as failed verification. No timestamp is signed, and Hookdeck imposes no time window of its own.

Make TutorCruncher webhooks production-ready. Hookdeck Event Gateway answers TutorCruncher straight away, so your endpoint's failures never count towards the 20% rate that gets an integration disabled.

TutorCruncher webhook limitations and pain points

A high failure rate disables the endpoint

The Problem: An outage, or a handler that returns errors for some actions, can get the integration's endpoint disabled.

Why It Happens: TutorCruncher disables an endpoint whose failure rate stays above 20% over a sustained period.

Workarounds:

  • Return 200 for actions you ignore, never a 4xx.
  • Watch the integration's webhook logs for rising failures.

How Hookdeck Can Help: Hookdeck accepts every request, so TutorCruncher sees successes while Hookdeck retries your service.

4xx responses aren't retried

The Problem: A validation bug that returns 400 or 404 loses the event for good.

Why It Happens: TutorCruncher doesn't retry redirects or 4xx responses other than 429.

Workarounds:

  • Return 503 for temporary problems on your side, so TutorCruncher retries.
  • Avoid redirects on the webhook URL, including HTTP to HTTPS.

How Hookdeck Can Help: Hookdeck stores each request even when your service rejects it, so you can fix the bug and replay.

Every action, no filter

The Problem: Your endpoint receives all of the account's activity, most of which you may not need.

Why It Happens: Integrations have no per-action subscription.

Workarounds:

  • Keep a set of handled actions and acknowledge the rest.
  • Respond quickly, since volume tracks everything happening on the account.

How Hookdeck Can Help: Hookdeck filters can pass only the actions each service needs, such as RAISED_INVOICE to accounting.

One example for hundreds of actions

The Problem: You can't tell from the docs what subject holds for most actions.

Why It Happens: TutorCruncher publishes a single API example and one fuller example in the help article, and subject changes shape with subject.model.

Workarounds:

  • Use the webhook logs to see real request bodies for the actions you handle.
  • Read subject.url and fetch the full object through the API.

How Hookdeck Can Help: Hookdeck keeps every request body for you to inspect, search and replay.

Best practices

Loop over events

A request can hold more than one event. Process each element and don't assume events[0] is the only one.

Build a key per event

There's no event ID. Combine action, timestamp, subject.model and subject.id, and store it before acting. The idempotency guide has more on this.

Respond fast

The timeout isn't documented, so queue first and do the work afterwards. The asynchronous processing guide walks through queue design.

Protect the private API key

The key that signs webhooks also authenticates API calls, so keep it out of logs and client-side code.

Conclusion

TutorCruncher sends every action on an account to an integration's URL, as an events array signed with a hex HMAC-SHA256 keyed by the private API key. Loop over the events, answer 200 for actions you ignore, and keep your failure rate low, because errors on your side can get the endpoint disabled.

Hookdeck Event Gateway verifies TutorCruncher webhooks, filters actions and retries delivery to your services.

Get started with Hookdeck for free and handle TutorCruncher webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.