# TIMIFY Webhooks

TIMIFY (appointment scheduling) sends an HTTP `POST` with a JSON body to your Server URL when
something happens in a webhook subscription's scope: a booking is created, a customer is deleted,
a resource is updated. Webhooks are **not signed**. The only built-in protection is the optional
custom header: enable **Include header name** when you create the subscription and TIMIFY adds
that header, with the secret value you entered, to every request. Your endpoint authenticates the
request by comparing the header with the value you stored.

> Sync-type webhooks, such as Before Confirmations, expect a synchronous answer and aren't supported.

## When to Use This Skill

* How do I receive TIMIFY webhooks?
* How do I authenticate TIMIFY webhook requests? (No signature: use the custom header)
* How do I use "Include header name" on a TIMIFY webhook subscription?
* How do I handle `Event` created, updated or deleted webhooks?
* Why does my TIMIFY 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 verifyTimifyRequest(headers, secret = process.env.TIMIFY_WEBHOOK_SECRET,
                             name = process.env.TIMIFY_WEBHOOK_HEADER || 'Authorization') {
  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 subscription, so read it from
  `TIMIFY_WEBHOOK_HEADER` (default `Authorization`) and look it up case-insensitively.
* Compare the **whole** header value with `TIMIFY_WEBHOOK_SECRET`. If you entered `Bearer abc123`,
  the secret is `Bearer abc123`; 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: '*/*' })`) and authenticate before parsing it.
* **Know the limits.** A static shared credential authenticates the sender, not the body, and gives
  no replay protection (no signature, no timestamp). Use HTTPS only, and rotate the value in the
  subscription and in `TIMIFY_WEBHOOK_SECRET` if it leaks.

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

## Webhooks and Types

TIMIFY's webhook overview lists these webhooks (the scope table) and their types. The overview
page does not document the payload field names, so the examples dispatch defensively on an
illustrative `webhook` and `type` and acknowledge anything they don't recognise.

| Webhook | Types |
| --- | --- |
| Customers | `Created` `Updated` `Deleted` |
| Event | `Created` `Updated` `Deleted` |
| Events | `Deleted` |
| Services | `Created` `Updated` `Deleted` |
| Resource | `Created` `Updated` `Deleted` |
| Company | `Created` `Updated` `Deleted` |
| Manager | `Created` `Updated` `Deleted` |
| Enterprise | `Updated` |
| Customer Fields | `Created` `Updated` `Deleted` |
| Shift | `Created` `Updated` `Deleted` |
| Notification | `Created` |
| Service Groups | `Created` `Updated` `Deleted` |
| Before Confirmations, Before Reminders | `Sync` (not supported) |

The Developer Platform application needs the scopes each webhook requires (see
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/timify-webhooks/references/overview.md)).

## Environment Variables

```bash
TIMIFY_WEBHOOK_HEADER=Authorization       # the header name you entered in the subscription
TIMIFY_WEBHOOK_SECRET=your-secret-value   # the exact, whole header value

```

## Local Development

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

```bash
npx hookdeck-cli listen 3000 timify --path /webhooks/timify

```

Use the printed URL as the Server URL of the webhook subscription.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/timify-webhooks/references/overview.md) - What TIMIFY webhooks are, webhooks, types and scopes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/timify-webhooks/references/setup.md) - Create a subscription and enable Include header name
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/timify-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/timify-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests