Gareth Wilson Gareth Wilson

Guide to Freshsales Webhooks: Features and Best Practices

Published


Freshsales webhooks let your CRM workflows call out to other systems. When a deal reaches a stage, a contact matches a set of conditions or an account is updated, a workflow can send an HTTP request to your service with the record's details, so you can generate an invoice, provision an account, notify a team or sync the record somewhere else.

This guide covers how a Freshsales webhook is built inside a workflow, how to authenticate it, what reaches your endpoint, how Freshsales handles timeouts and failures, and the practices that keep a Freshsales receiver reliable.

What are Freshsales webhooks?

Freshsales has no webhook subscriptions. A webhook is an action, Trigger webhook, inside a workflow. When the workflow's trigger and conditions match a record, the action sends a request to a callback URL, built from settings you choose:

  • the request type: POST, GET, PUT, PATCH or DELETE
  • the encoding: JSON, XML or X-FORM-ENCODED
  • the content: Simple (fields you select, in a predefined format) or Advanced (a body you write, with placeholders)
  • optional custom headers and an optional Authentication setting

Because you write the request, Freshsales adds no event type of its own. Which workflow fired, and about which record, is whatever you put in the body or URL.

Freshsales does not sign the request. The dependable way to authenticate it is a secret custom header that your receiver checks.

Freshsales webhook features

FeatureDetails
ConfigurationA workflow's Trigger webhook action
Request typePOST, GET, PUT, PATCH or DELETE, chosen per action
EncodingJSON, XML or X-FORM-ENCODED (for POST, PUT and PATCH)
ContentSimple (selected fields) or Advanced (your template with placeholders). GET and DELETE send none
PlaceholdersContacts workflows: contact and primary account. Accounts workflows: account. Deals workflows: deal and related account
Custom headersOne Name: VALUE pair per line
Authentication settingOptional username and password, or API key. How it is sent is not documented
Signature and timestampNone
Event typeNone. Defined by your content
Response window6 seconds, or a Read Timeout error
FailuresAn email naming the failed callback URL, retries for an hour, then for the next 2 days before the request is terminated
Source IPsNot published

What Freshsales sends

There is no fixed payload. With Simple content, Freshsales sends the fields you selected from the record in its own predefined format. With Advanced content, you write the body and use placeholders such as {{deal.id}}, {{contact.id}} and {{sales_account.id}}. A JSON Advanced body for a Deals workflow might look like this:

{
  "event": "deal_won",
  "deal": {
    "id": "{{deal.id}}",
    "name": "{{deal.name}}",
    "amount": "{{deal.amount}}"
  }
}

The event field there is not something Freshsales provides. It is a constant you add so your receiver knows which workflow sent the request. Do this in every workflow that points at the same endpoint.

Freshsales' own docs recommend Advanced content, because the Simple format may not match what the receiving endpoint expects.

Inspect Freshsales webhooks as they arrive. Point a Trigger webhook action at a Hookdeck Inspect URL to see the exact method, headers and body your workflow sends, with no account or setup required.

Setting up Freshsales webhooks

Generate a long random secret first, for example with openssl rand -hex 32. Use a value without spaces, commas or colons.

  1. Create or open a workflow and set its trigger and conditions.
  2. Add the Trigger webhook action.
  3. Choose the request type. POST is the usual choice for notifications.
  4. Enter your HTTPS callback URL.
  5. Leave Authentication empty and click Add custom headers. Enter one line such as X-Webhook-Secret: <your secret>. A header name containing spaces stops the trigger from executing.
  6. For POST, PUT or PATCH, choose JSON encoding and Advanced content, and write your body with placeholders.
  7. Test the webhook, then test the whole workflow with a real record.

Testing the action on its own sends sample data: an ID placeholder becomes 12345, for example. That is enough to check authentication and routing, but a handler that looks the record up will fail on it, so test the workflow too.

To route through Hookdeck, create a Hookdeck source with the Freshsales type and use its URL as the callback URL. The source asks for a Header Name and a Header Value: enter the name and the whole value of your custom header. The source pre-selects POST but leaves the method open, because Freshsales lets you choose. If your action uses PUT, PATCH, GET or DELETE, add that method to the source's allowed HTTP methods as well.

Securing Freshsales webhooks

Read the header you chose and compare its whole value with your stored secret in constant time. Because the request type and encoding are yours to choose, accept every method you use, read the body raw, and treat an empty body as valid.

const crypto = require('crypto');
const express = require('express');

const HEADER_NAME = (process.env.FRESHSALES_WEBHOOK_HEADER || 'x-webhook-secret').toLowerCase();
const SECRET = process.env.FRESHSALES_WEBHOOK_SECRET;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

// Accept every request type a workflow might use.
app.all('/webhooks/freshsales', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME);
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  const raw = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : '';
  if (raw && req.is('json')) {
    let payload;
    try {
      payload = JSON.parse(raw);
    } catch {
      return res.sendStatus(400);
    }
    // Route on the constant you put in your Advanced content, e.g. payload.event.
  }
  // GET and DELETE arrive with no body: use the URL or query string you configured.

  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

HEADER_NAME = os.environ.get("FRESHSALES_WEBHOOK_HEADER", "X-Webhook-Secret")
SECRET = os.environ.get("FRESHSALES_WEBHOOK_SECRET", "")


def verify_freshsales_request(headers) -> bool:
    if not SECRET:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

A secret header authenticates the sender, nothing more. It does not cover the body, so a modified request still passes, and with no timestamp a captured request can be replayed. Use HTTPS, a long random value, and rotate it in every workflow and in your environment together if it might have leaked.

Make Freshsales webhooks production-ready. Hookdeck Event Gateway checks the custom header you configure, answers well within Freshsales' 6-second window, and queues every workflow call durably for your service.

Freshsales webhook limitations and pain points

The Authentication setting is a black box

The Problem: You fill in Authentication with a username and password or an API key, and your receiver cannot find the credential, or finds it in a form you did not expect.

Why It Happens: Freshsales offers the setting but does not document how either credential is sent: which header, which scheme, or whether the API key travels in a header at all.

Workarounds:

  • Leave Authentication empty and use a custom header, which is sent exactly as you type it.
  • If you must use Authentication, send a test to a Hookdeck Inspect URL first and see what arrives.

How Hookdeck Can Help: Hookdeck's Freshsales source checks a custom header by name and exact value. Hookdeck answers Freshsales with a 200 either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service.

No event type on the wire

The Problem: Several workflows point at one URL and your receiver cannot tell them apart.

Why It Happens: Freshsales sends only what you configure. Simple content has no field naming the workflow, and nothing in the headers does either.

Workarounds:

  • Use Advanced content and include a constant such as "event": "deal_won" in every body.
  • Or give each workflow its own path or query parameter.

How Hookdeck Can Help: Hookdeck filters can route on any body field, header or path, so one source can send each workflow's calls to the right destination.

Six seconds to answer

The Problem: A handler that does real work, such as creating an invoice or provisioning an account, gets a Read Timeout and a failure email, even though the work may have completed.

Why It Happens: Freshsales waits 6 seconds for a response and reports a timeout after that. It then retries, so the same work can run again.

Workarounds:

  • Authenticate, store the request, respond, and do the work afterwards.
  • Make the work idempotent, keyed on the record ID and the workflow.

How Hookdeck Can Help: Hookdeck responds to Freshsales as soon as it has the request, then delivers it to your service with a delivery timeout and retries you control.

Method and encoding are yours to get wrong

The Problem: A route that only accepts POST returns 405 to a workflow set to PUT, and a JSON parser fails on an X-FORM-ENCODED body.

Why It Happens: The request type and encoding are per action, and nothing keeps them consistent across workflows.

Workarounds:

  • Standardise on POST with JSON for notifications.
  • If you need other methods, accept them on your route, and allow them on the Hookdeck source.

How Hookdeck Can Help: Hookdeck shows each request's method, content type and body, so a misconfigured workflow is obvious, and you can replay its requests once it is fixed.

Best practices

Write Advanced content with identifiers

Include the record ID placeholder and an event constant in every body. Your receiver then knows what happened and to which record without having to infer it.

Deduplicate deliveries

Retries after timeouts can repeat a request whose work already succeeded. Key on the record ID plus your event constant, and skip repeats. See our guide to webhook idempotency.

Re-read the record when it matters

Placeholders are filled in when the workflow runs. If your handler acts on money or access, fetch the record from the Freshsales API before acting rather than trusting the body.

Acknowledge fast and process asynchronously

The 6-second window is short. Verify, persist, return 200, and process afterwards. See why to process webhooks asynchronously.

Rotate the secret everywhere at once

The secret lives in every workflow that calls your endpoint. Keep a list of them, and during a rotation accept both old and new values until every workflow is updated.

Conclusion

Freshsales webhooks are workflow actions that send the request you design, with no signature and no event type of their own. Authenticate them with a secret custom header rather than the undocumented Authentication setting, put an event constant and record ID in an Advanced JSON body, accept the methods you configure, and respond inside 6 seconds.

Hookdeck Event Gateway checks the header, answers Freshsales immediately, and queues every workflow call for your service with retries and replay, so a slow handler stops producing timeouts and duplicate work.

Get started with Hookdeck for free and handle Freshsales 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.