Agent skill
Freshdesk Webhooks Skill
Receive and authenticate Freshdesk webhooks sent by the "Trigger Webhook" action in Freshdesk automation rules (Ticket Creation and Ticket Updates). Use when building a Freshdesk webhook receiver, securing it with a shared secret custom header (Freshdesk has no HMAC signature), writing the Advanced JSON payload with placeholders like {{ticket.id}} and {{Triggered event}}, handling ticket_created / ticket_updated deliveries, or debugging Freshdesk webhook retries (every 30 minutes, 48 calls) and the 1000 calls/hour limit.
Install this skill
npx skills add hookdeck/webhook-skills --skill freshdesk-webhooks
Freshdesk doesn't have webhook subscriptions. A webhook is an action inside an automation rule: Admin → Workflows → Automations → Ticket Creation (or Ticket Updates) → New Rule → Trigger Webhook. When the rule's conditions match, Freshdesk calls the URL you entered with the method, headers and body you configured.
This differs from most providers in three ways:
- There is no signature. Freshdesk doesn't sign the body. There's no HMAC, signing secret or timestamp header, so don't go looking for an
X-Freshdesk-Signature. It doesn't exist. - You write the payload. With Advanced content you write the JSON body yourself using placeholders such as
{{ticket.id}}. There are no fixed event type strings, so you have to put an event name in the body yourself. - You choose the request. The method (GET/POST/PUT/PATCH/DELETE), encoding (JSON, XML or XML-Encoded) and headers are all set in the rule.
When to Use This Skill
- How do I receive Freshdesk webhooks?
- How do I verify Freshdesk webhook signatures? (You can't. Send a shared secret in a custom header instead.)
- How do I set up "Trigger Webhook" in a Freshdesk automation rule?
- What payload does a Freshdesk webhook send? (Whatever you write in the Content section.)
- How do I tell a ticket-created webhook from a ticket-updated one?
- Why does my Freshdesk webhook keep arriving every 30 minutes? (Retries after a non-2xx response.)
Verification (core): a shared secret custom header
In the Trigger Webhook action, turn on Add custom headers and add one line in Freshdesk's X-Sample-CustomHeader1: VALUE format:
X-Freshdesk-Webhook-Secret: 3f9c0e...a long random hex value...
The header name is your choice. Freshdesk treats header names as case-insensitive and sends the value exactly as entered. Check the header in constant time before you parse the body:
const crypto = require('crypto');
// NOT a Freshdesk signature. This is a secret YOU put in the rule's custom headers.
function verifyFreshdeskSecret(headerValue) {
const expected = process.env.FRESHDESK_WEBHOOK_SECRET;
if (!expected) throw new Error('FRESHDESK_WEBHOOK_SECRET is not set'); // fail closed
if (typeof headerValue !== 'string') return false;
const a = Buffer.from(headerValue);
const b = Buffer.from(expected);
// Length check first: timingSafeEqual throws on a length mismatch.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express lowercases header names: req.get('X-Freshdesk-Webhook-Secret') works too.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Return 401 for a missing or wrong secret and 500 when the secret isn't configured, so the endpoint never accepts everything by accident. Use a value without spaces, such as openssl rand -hex 32. Freshdesk's docs say the trigger won't run if a header has spaces in it.
"Requires authentication" is a separate toggle in the same action ("provide your API key"). Freshdesk doesn't document what it puts on the wire, so use the custom header described above. See references/verification.md.
The Payload: you define it
Choose Encoding: JSON and Content: Advanced, then write a body with a fixed event field for each rule. Here's the body for the Ticket Creation rule:
{
"event": "ticket_created",
"triggered_event": "{{Triggered event}}",
"ticket": {
"id": "{{ticket.id}}",
"subject": "{{ticket.subject}}",
"status": "{{ticket.status}}",
"priority": "{{ticket.priority}}",
"requester_email": "{{ticket.requester.email}}",
"url": "{{ticket.url}}"
}
}
The Ticket Updates rule uses the same body with "event": "ticket_.
eventis this skill's convention, not a Freshdesk field. Freshdesk defines no event-type strings for webhooks.{{Triggered event}}is "available only in webhooks and returns the name of the event that triggered the rule". Freshdesk doesn't list its possible values, so log it but don't branch on it.- Placeholder values are substituted as text. Treat them as strings and re-fetch the ticket from the Freshdesk API (
GET /api/v2/tickets/{id}) before you act on anything important. - Simple Content sends a list of ticket properties you pick, in a format Freshdesk doesn't document. Use Advanced so the body is predictable.
Events (this skill's event values)
event | Rule tab | Fires when |
|---|---|---|
ticket_created | Ticket Creation | A new ticket matches the rule's conditions |
ticket_updated | Ticket Updates | An update to a ticket matches the rule's conditions (e.g. "Status is updated", "Customer replies to ticket") |
If an event value isn't one you handle, log it and return 200. If the body has no event field, the rule is misconfigured, so return 400.
Delivery, Retries and Idempotency
- Success is any 2xx response. 3xx responses are followed as redirects. Anything else counts as a failure and "will automatically be retried once every 30 minutes, totaling 48 calls". Account admins get a failure email.
- Rate limit: 1000 webhook calls per hour. Calls over the limit are queued until the next hour.
- There's no delivery ID header, so dedupe on values you put in the body (e.g.
event+ticket.id+ a status or timestamp placeholder), or make your handler an upsert keyed onticket.id. - Webhooks run asynchronously to the automation, and Freshdesk ignores your response body.
Environment Variables
# REQUIRED. The value you put in the rule's custom header. Generate with: openssl rand -hex 32
FRESHDESK_WEBHOOK_SECRET=
# OPTIONAL. The custom header name (case-insensitive). Default: X-Freshdesk-Webhook-Secret
FRESHDESK_WEBHOOK_HEADER=X-Freshdesk-Webhook-Secret
Local Development
npx hookdeck-cli listen 3000 freshdesk --path /webhooks/freshdesk
No account required. The CLI creates a guest account on first run and gives you a public HTTPS URL to paste into the Trigger Webhook action, plus a web UI for inspecting requests.
In production, Hookdeck's Freshdesk source type checks the header for you. You enter the header name and its exact value, and requests without them are rejected. See references/setup.md.
Reference Materials
- references/overview.md - How Trigger Webhook works, rule types, payload templates, placeholders, retries and limits
- references/setup.md - Step-by-step rule setup, custom header, testing, Hookdeck source setup
- references/verification.md - Why there is no signature, the shared-secret header pattern, gotchas