# Freshsales Webhooks

Freshsales (Freshworks CRM) has no webhook subscriptions. A webhook is an **action** inside a
workflow: when the workflow's trigger and conditions match, the **Trigger webhook** action calls the
URL you entered with the request type, headers and content you configured.

This differs from most providers in three ways:

1. **Webhooks are not signed.** There is no HMAC, signing secret or timestamp header. You
  authenticate the sender with a secret custom header you add to the action.
2. **You choose the request.** The request type (POST, PUT, PATCH, GET or DELETE) and encoding
  (JSON, XML or X-FORM-ENCODED) are set in the workflow, so the endpoint must not be POST-only,
  and GET and DELETE have no body.
3. **There is no event type.** The body is Simple (selected fields) or Advanced (content you write
  with placeholders). Which workflow fired is whatever you put in the body.

## When to Use This Skill

* How do I receive Freshsales webhooks?
* How do I verify Freshsales webhook signatures? (You can't: use a secret custom header)
* How do I set up "Trigger webhook" in a Freshsales workflow?
* What payload does a Freshsales webhook send? (Whatever Simple or Advanced content you chose)
* Why does my Freshsales webhook endpoint return 401 or never fire?

## Verification (core)

On the Trigger webhook action choose **Add custom headers** and add one line in Freshsales'
`X-Sample-CustomHeader1: VALUE` format, for example `X-Webhook-Secret: 3f9c0e...` (a long random hex
value, no spaces). The header name is yours to choose; the **whole** header value is the secret.

```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 verifyFreshsalesRequest(headers, secret = process.env.FRESHSALES_WEBHOOK_SECRET,
    headerName = process.env.FRESHSALES_WEBHOOK_HEADER || 'X-Webhook-Secret') {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);
}

```

* Compare the **entire** value with the exact string you typed. Don't strip or parse a `Bearer `
  prefix: if you typed one, it's part of the secret.
* Look the header up case-insensitively. Missing header or mismatch: return `401`.
* Python: `hmac.compare_digest` on **bytes** (encode both sides; `str` raises on non-ASCII).
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS, keep the secret out of logs, and rotate it
  (change it in the workflow and your env var) if it leaks.
* Freshsales also has an optional **Authentication** field (username and password, or API key), but
  how it's sent on the wire isn't documented. Use a custom header instead.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it. Accept
  POST, PUT, PATCH, GET and DELETE, and treat an empty body as valid.

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

## Payloads

Freshsales defines no event type strings. The examples assume an illustrative Advanced body, with
an endpoint-style JSON document and placeholders (Deals workflows offer Deal and related account
fields, Contacts workflows offer Contact and primary Account fields, Accounts workflows offer
Account fields):

```json
{ "deal": { "id": "{{deal.id}}", "name": "{{deal.name}}", "amount": "{{deal.amount}}" } }

```

```json
{ "contact": { "id": "{{contact.id}}", "email": "{{contact.email}}" } }

```

| Payload shape | Meaning in the examples |
| --- | --- |
| top-level `deal` | A deal workflow fired (illustrative) |
| top-level `contact` | A contact workflow fired (illustrative) |

This `deal` / `contact` shape is a convention of the examples, not something Freshsales sends. If
you need to tell "created" from "updated", add a literal field to each workflow's body.

## Environment Variables

```bash
FRESHSALES_WEBHOOK_HEADER=X-Webhook-Secret       # name of the custom header (default shown)
FRESHSALES_WEBHOOK_SECRET=your-long-random-hex   # exact, whole header value

```

## Delivery Behavior

* Freshsales expects a response within **6 seconds** (otherwise a read timeout).
* On failure it emails you the failed Callback URL, retries for an hour, then for the next 2 days,
  then drops the request. Make handlers idempotent.
* The **Test** button sends sample data (for example `12345` for ID placeholders).

## Local Development

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

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

```

Use the printed URL as the Callback URL, and add the secret custom header to the action.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/freshsales-webhooks/references/overview.md) - What Freshsales webhooks are, request types and payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/freshsales-webhooks/references/setup.md) - Create the workflow, Trigger webhook action and secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/freshsales-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/freshsales-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests