Guide to Help Scout Webhooks: Features and Best Practices
Help Scout webhooks tell your systems what is happening in your shared inbox as it happens: a conversation arrives, is assigned or closed, a customer replies, a satisfaction rating comes in, or a Beacon chat starts. If your support team works in Help Scout, webhooks are how that activity reaches your CRM, analytics, alerting or internal tools without polling the Inbox API.
This guide covers how Help Scout webhooks are configured and signed, the events and their payloads, how Help Scout treats your responses, and the best practices for running a Help Scout receiver in production.
What are Help Scout webhooks?
A Help Scout webhook is a callback URL, a list of events and a secret key, configured in Help Scout's Webhooks app or through the Inbox API. When one of the selected events happens, Help Scout sends a JSON POST to the URL.
The body is the affected object in the format of Help Scout's API, for example the v2 Conversation object for conversation events. The body doesn't say which event it is. That comes in the X-HelpScout-Event header, so your handler has to read the header to know how to treat the body.
Every request is signed. The X-HelpScout-Signature header holds a base64 HMAC-SHA1 of the raw body, keyed with the secret key you chose when creating the webhook. There is no timestamp in the scheme.
Help Scout webhook features
| Feature | Details |
|---|---|
| Configuration | Webhooks app under Settings > Workspace > Apps, or the Inbox API (POST /v2/webhooks) |
| Subscription model | Choose events per webhook. Conversation events can be limited to specific mailboxes through the API |
| Event header | X-HelpScout-Event |
| Signature header | X-HelpScout-Signature |
| Scheme | HMAC-SHA1 of the raw body, base64-encoded |
| Secret | Chosen by you. The API accepts 40 characters or fewer |
| Replay protection | None in the scheme: no timestamp or nonce |
| Payload format | The API object for the resource. Delete events send only { "id": ... } |
| Payload versions | V2 (deprecated, the default) or V3 through the API's payloadVersion |
| Success | Any 2xx. The response body is discarded |
410 Gone | Deactivates or deletes the webhook |
| Retries | Any other status is retried up to 10 times with increasing intervals, then discarded |
| Auto-deactivation | After ten or more events are discarded |
Help Scout's webhook docs don't state a response timeout, a delivery order or source IP addresses, so the signature is how you authenticate a request.
Common events
Help Scout's webhooks page lists 29 events. The ones most integrations start with:
| Area | Events | Body |
|---|---|---|
| Conversations | convo.created, convo.assigned, convo.status, convo.moved, convo.merged, convo.tags, convo.custom-fields | v2 Conversation object |
| Threads | convo.customer.reply.created, convo.agent.reply.created, convo.note.created | v2 Conversation object |
| AI Answers | convo.ai-answers.created | v2 Conversation object |
| Customers | customer.created, customer.updated | v2 Customer object |
| Ratings | satisfaction.ratings | v2 Rating object |
| Beacon chat | beacon.chat.created, beacon.chat.customer.replied, beacon.chat.agent.replied, beacon.chat.assigned, beacon.chat.ended | Chat object |
| Tags | tag.created, tag.updated | v2 Tag object |
| Surveys | message.survey.response.received | Survey Response object |
| Organizations | organization.created, organization.updated | Organization object |
| Users | user.status.changed | User Status object |
| Deletes | convo.deleted, customer.deleted, tag.deleted, organization.deleted | { "id": ... } only |
Two behaviours catch people out. customer.created can't be filtered by inbox and fires for every customer created anywhere in the account. And convo.agent.reply.created only covers threads written by a user, so auto-replies don't trigger it.
The Inbox API's own list of accepted event names doesn't match the webhooks page exactly. It includes convo.ai-answers-email.reply.created and convo.ai-answers-email.resolution, for example. Check the API reference if you create webhooks programmatically.
Here is Help Scout's example body for message.survey.response.received, with X-HelpScout-Event: message.survey.response.received:
{
"message": {
"name": "Customer satisfaction survey"
},
"response": {
"name": "Happy",
"value": "\ud83d\ude00",
"comment": "Just awesome!",
"timestamp": "2022-09-05T12:00:00Z"
},
"_embedded": {},
"_links": {
"message:dashboard": {
"href": "https://secure.helpscout.net/messages/8de613e9-0154-426e-9fe7-fd0017947959"
}
}
}
The emoji in value arrives as a JSON escape sequence rather than raw UTF-8. Help Scout escapes non-ASCII characters before signing, which is one reason to verify the bytes you received rather than a re-encoded copy.
Setting up Help Scout webhooks
In the Webhooks app
- In Help Scout, click the gear icon (Settings), go to Workspace > Apps, and select the Webhooks app.
- Create a webhook and enter your HTTPS callback URL.
- Enter a secret key. Help Scout doesn't generate one: you choose it, so use a long random string. Store the same value server-side, for example as
HELPSCOUT_WEBHOOK_SECRET. - Select the events you want and save.
Through the Inbox API
curl -X POST "https://api.helpscout.net/v2/webhooks" \
-H "Authorization: Bearer YOUR_OAUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/help-scout",
"events": ["convo.created", "convo.assigned", "convo.status"],
"secret": "a-random-string-of-40-characters-or-less",
"payloadVersion": "V3",
"label": "Support sync"
}'
The API adds a few options the webhooks page doesn't mention:
mailboxIdslimits conversation events to the listed mailboxes. Without it, conversations in every mailbox trigger the webhook.payloadVersionchoosesV2(deprecated, and the default when omitted) orV3, which preserves thesystem_usertype in conversation payloads.notification: truesends only the URI of the changed resource instead of the full body.
If you use Hookdeck, create a source with the Help Scout source type, enter the same secret key, and use the Hookdeck source URL as the callback URL in Help Scout.
Securing Help Scout webhooks
To verify a request, compute an HMAC-SHA1 of the raw request body with your secret key, base64-encode it, and compare it in constant time with X-HelpScout-Signature. Do this before parsing the JSON.
const crypto = require('crypto');
const express = require('express');
function verifyHelpScoutSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const expected = crypto.createHmac('sha1', secret).update(rawBody).digest();
const received = Buffer.from(signatureHeader.trim(), 'base64');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/help-scout', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyHelpScoutSignature(req.body, req.get('x-helpscout-signature'), process.env.HELPSCOUT_WEBHOOK_SECRET)) {
// Never 410 here: Help Scout deactivates the webhook on a 410.
return res.sendStatus(401);
}
const eventName = req.get('x-helpscout-event');
const payload = JSON.parse(req.body.toString('utf8'));
// Route on eventName, then process async.
res.sendStatus(200);
});
The same check in Python:
import base64
import binascii
import hashlib
import hmac
import os
def verify_help_scout_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
try:
received = base64.b64decode(signature_header.strip(), validate=True)
except (binascii.Error, ValueError):
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).digest()
return hmac.compare_digest(received, expected)
# In FastAPI: raw_body = await request.body()
# verify_help_scout_signature(raw_body, request.headers.get("x-helpscout-signature"),
# os.environ["HELPSCOUT_WEBHOOK_SECRET"])
Help Scout's page includes a community-contributed Node.js example. Be careful copying it: it calls next() before the body has finished streaming, so req.isHelpScout is still false when the next handler runs, and it compares signatures with === rather than in constant time.
To check your verifier, use Help Scout's own test vector. With the secret your secret key and this exact body:
{"ticket":{"id":"1","number":"2"},"customer":{"id":"1","fname":"Jackie","lname":"Chan","email":"jackie.chan@somewhere.com","emails":["jackie.chan@somewhere.com"]}}
the X-HelpScout-Signature header should be:
I1KlvGppYqvFTJgJ9jezdQMDiyI=
Make Help Scout webhooks production-ready. Hookdeck Event Gateway verifies the
X-HelpScout-Signatureheader on every delivery, keeps Help Scout's 10-retry budget from running out while your service is down, and gives you a searchable log of every conversation event.
Help Scout webhook limitations and pain points
A 410 deletes your webhook
The Problem: A webhook disappears from Help Scout and events stop arriving.
Why It Happens: Help Scout treats a 410 Gone response as an instruction to deactivate or delete the webhook. A framework, proxy or platform that returns 410 for a removed route, or a handler that uses it for "not interested", is enough to switch it off.
Workarounds:
- Return
401or400for a bad signature, and200for events you don't handle. - Check what your hosting platform returns for retired routes before you move the endpoint.
How Hookdeck Can Help: Help Scout only ever talks to Hookdeck, which answers with a 2xx once the event is stored. Whatever your own service returns, including a 410, it can't delete the Help Scout webhook.
Ten discarded events and the webhook is deactivated
The Problem: After an outage or a bad deploy, the webhook is deactivated and new events are lost.
Why It Happens: Each failed event is retried up to 10 times with increasing intervals, then discarded. Once ten or more events have been discarded, Help Scout deactivates the webhook. On a busy inbox, a short outage can discard ten events quickly. Help Scout's docs don't describe a way to replay discarded events.
Workarounds:
- Keep the handler small: verify, persist, return
200, and process asynchronously. - Monitor the webhook's status in the Webhooks app and re-enable it after an outage.
- After an outage, reconcile through the Inbox API, for example by listing conversations modified since the outage began.
How Hookdeck Can Help: Hookdeck accepts Help Scout's deliveries while your service is down, so Help Scout sees no failures and never discards or deactivates. Events wait in Hookdeck, and you replay them when your service is healthy.
No replay protection in the signature
The Problem: A captured request can be resent later and still pass verification.
Why It Happens: The signature covers only the body. There is no timestamp or nonce header, so nothing in the scheme tells you when a request was signed.
Workarounds:
- Make processing idempotent, for example by keying on the object's
idtogether with itsupdatedAtor the thread ID, so a repeated delivery changes nothing. - Serve the endpoint over HTTPS only, and use a long random secret.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on fields you choose, and the event log shows every request Hookdeck received.
The event name is only in a header
The Problem: Stored payloads can't be told apart later, and some no-code tools can't route Help Scout events at all.
Why It Happens: The body is the resource, not an envelope. A Conversation object looks the same whether it came from convo.created, convo.assigned or convo.status. Only X-HelpScout-Event says which.
Workarounds:
- Store the
X-HelpScout-Eventvalue alongside the body whenever you persist a delivery. - Use one webhook per group of related events if your tooling can't read request headers.
How Hookdeck Can Help: Hookdeck filters and routes on headers as well as bodies, so you can send convo.* and customer.* events to different destinations without touching your code.
Signatures break on re-encoded bodies
The Problem: Verification fails for some requests, often those containing names, emoji or non-English text.
Why It Happens: Help Scout escapes and transliterates non-ASCII characters before signing, and signs the exact bytes it sends. Parsing the JSON and serialising it again produces different bytes, most visibly when non-ASCII characters come back as raw UTF-8.
Workarounds:
- Read the raw body (
express.raw(),await request.body()) and verify before parsing. - Don't let body-parsing middleware run on the webhook route first.
How Hookdeck Can Help: Hookdeck's Help Scout source type verifies against the original bytes, so your service receives requests that have already been checked.
Best practices
Route on the header, store the header
Read X-HelpScout-Event before you look at the body, log and acknowledge names you don't recognise, and keep the header with any payload you store.
Choose a strong secret
Help Scout uses whatever secret you give it. Generate a long random string within the API's 40-character limit rather than a memorable phrase, and store it in a secrets manager.
Treat deletes differently
convo.deleted, customer.deleted, tag.deleted and organization.deleted send only an ID. Don't run them through the same parser as the full objects.
Expect customer.created from every inbox
Because it can't be filtered by inbox, filter on your side if you only care about some mailboxes, and expect volume from every channel in the account.
Acknowledge fast and process asynchronously
Help Scout doesn't document a response timeout, so don't depend on a generous one. Verify, persist, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Make handlers idempotent
Retries mean the same event can arrive more than once. Upsert by resource ID rather than inserting, and see our guide to webhook idempotency.
Conclusion
Help Scout's webhooks send the affected API object, put the event name in X-HelpScout-Event, and sign the raw body with HMAC-SHA1 and a secret you choose. Verify the exact bytes you receive, route on the header, never answer with a 410, and keep failures rare, because ten discarded events deactivate the webhook.
Hookdeck Event Gateway sits in front of your receiver, verifies every Help Scout signature, and holds events while your service is unavailable, so an outage doesn't cost you the webhook or the conversations sent during it.
Get started with Hookdeck for free and handle Help Scout webhooks reliably in minutes.