Can HubSpot Send Webhooks?
Yes, in two different ways, and picking the right one saves a lot of trouble:
- Workflow webhooks. A Send a webhook action inside a HubSpot workflow. You choose the trigger (contact created, deal stage changed, form submitted), the data to send and the URL. Configured in the HubSpot UI; requires Data Hub Professional or Enterprise.
- App webhook subscriptions. A HubSpot app (for example a private app) subscribes to events like
contact.creationordeal.propertyChange, and HubSpot posts batches of events to your URL. Configured in the app’s settings; aimed at developers.
| Workflow webhook | App subscription | |
|---|---|---|
| Best for | Business rules: “when a deal hits Closed Won, send it to accounting” | Broad sync: every contact change, everywhere |
| Filtering | Full workflow logic (criteria, branches, delays) | By event type and property |
| Payload | The record’s properties, or a body you design | Small event objects (ID + what changed) |
| Plan | Data Hub Professional / Enterprise | Developer app (private or public) |
In This Post
Option 1: Workflow Webhooks
Step 1: Create the workflow and trigger
- Go to Automation > Workflows and create a workflow based on the object you care about (contact, company, deal, ticket…)
- Set the enrollment trigger. Common ones:
- Record created: new contacts or deals to another system
- Property value changed: lifecycle stage, deal stage, owner
- Form submission: route leads in real time
- Decide on re-enrollment. Without it, a record goes through the workflow once, so a contact whose lifecycle stage changes twice only sends one webhook
Step 2: Add the Send a webhook action
- Add an action and choose Send a webhook (in the Data ops section)
- Method: POST to send data (GET is for retrieving it)
- Webhook URL: the endpoint we provide
- Authentication type (see below)
- Request body: either Include all [object] properties, or Customize request body
(HubSpot: webhooks in workflows)
Step 3: Design the request body
“Include all properties” sends everything HubSpot knows about the record, including (for contacts) form submissions and list memberships. It works, but it’s large and changes shape whenever someone adds a property.
For integrations we use Customize request body: you add a Key and pick the HubSpot property for its value, or use Add static value for a fixed key/value. The receiving side then gets exactly this, every time:
{
"event": "deal.closed_won",
"deal_id": "18452317901",
"deal_name": "Northside HVAC – service agreement",
"amount": "12000",
"close_date": "2026-10-01",
"pipeline": "default",
"hubspot_owner_id": "73520117"
}
Here event is a static value, and the rest map to hs_object_id, dealname, amount, closedate, pipeline and hubspot_owner_id. Always include the record ID (hs_object_id). It’s how the integration finds the record again, and the only value that never changes. The static event value tells the receiving side which workflow sent it when several share one URL.
Authentication
The Authentication type setting has two options:
- Include request signature in header: you enter your HubSpot app’s ID, and HubSpot adds an
X-HubSpot-Signatureheader (signature version v2: a SHA-256 hash of your app secret + method + URL + body). The receiver recomputes it to prove the request came from your portal. - API key: you set a key name and where to send it (query parameter or request header). The simplest option when the endpoint checks a shared secret.
Retries and rate limiting
- If a webhook fails, HubSpot retries for up to three days, starting one minute after the failure, with gaps of up to 8 hours.
- A 4xx response stops retries, except 429 (rate limited): HubSpot retries and honors
Retry-After. - The action has an optional rate limit (executions per second, minute or hour). It’s off by default, and worth turning on if the receiving system is fragile.
So return 5xx or 429 for temporary problems (you’ll get a retry) and 4xx only for requests you never want resent.
Testing a workflow webhook
Open the action and expand Test action. With a custom body you enter test values; with “include all properties” you pick a real record. HubSpot sends the request and shows the response and request, without changing any records. Point the URL at a request inspector (webhook.site or similar) first if you want to see the raw request before your endpoint exists.
Option 2: App Webhook Subscriptions
For developers syncing everything (all contact changes, all deal changes) an app subscription is more efficient than dozens of workflows. Private apps manage their subscriptions in the app’s settings (the API for managing subscriptions is for public apps), and the app’s Test button sends a sample event to your URL. (Webhooks guide, private app webhooks)
HubSpot sends an array of small event objects:
[
{
"objectId": 1246965,
"propertyName": "lifecyclestage",
"propertyValue": "subscriber",
"changeSource": "ACADEMY",
"eventId": 3816279340,
"subscriptionId": 25,
"portalId": 33,
"appId": 1160452,
"occurredAt": 1462216307945,
"eventType": "contact.propertyChange",
"attemptNumber": 0
}
]
(HubSpot’s docs show this field as eventType in examples and subscriptionType in the field reference. Handle both.)
How delivery works:
- Batches of up to 100 events, in no guaranteed order. Use
occurredAtto sequence them. - Respond within 5 seconds. Slower responses, connection failures and any 4xx or 5xx are retried, up to 10 times over 24 hours.
- Up to 10 requests in flight at once per account or private app.
eventIdisn’t guaranteed unique, so de-duplicate on a combination likeobjectId+propertyName+occurredAt.- The payload says what changed. Fetch the full record through the API when you need more.
Validating app webhooks (signature v3)
App webhooks send X-HubSpot-Signature-v3 and X-HubSpot-Request-Timestamp. To validate (request validation):
- Reject the request if the timestamp is more than 5 minutes old
- Concatenate method + full URL + raw body + timestamp (UTF-8; decode the URL-encoded characters HubSpot lists)
- HMAC SHA-256 that string with your app’s client secret, then base64-encode it
- Compare to the header in constant time
import base64, hashlib, hmac, time
def valid_hubspot_v3(method, url, body: bytes, timestamp: str, signature: str, client_secret: str) -> bool:
if abs(time.time() * 1000 - int(timestamp)) > 5 * 60 * 1000:
return False
message = method.encode() + url.encode() + body + timestamp.encode()
digest = hmac.new(client_secret.encode(), message, hashlib.sha256).digest()
return hmac.compare_digest(base64.b64encode(digest).decode(), signature)
Testing Checklist
- Use Test action (workflows) or the app’s Test button (subscriptions) before going live.
- Trigger the real event with a test record and confirm the payload has the ID and the fields you mapped.
- Check re-enrollment: change the trigger property twice and confirm you get two webhooks if you expect two.
- Return a 500 once from your endpoint and confirm the retry arrives.
- For app subscriptions, make a bulk change (import or bulk edit) and confirm your endpoint handles batches and bursts.
Troubleshooting
“Send a webhook” Isn’t in the Action List
Workflow webhooks need Data Hub Professional or Enterprise. Without it, use an app subscription or call your endpoint from another tool.
Workflow Not Firing, or Only Firing Once
Check the enrollment trigger and re-enrollment. Without re-enrollment, each record only goes through once. Test with a broad trigger, then narrow it.
Requests Rejected as Unauthorized
Signature checks fail when the URL you hash doesn’t exactly match the one HubSpot called (scheme, host, path and query), or when the body was parsed and re-serialized before hashing. Hash the raw body.
Webhook Fires on Every Edit
A “property value changed” trigger can enroll on changes you don’t care about. Add an if/then branch that checks the specific value before the webhook action.
Events Arrive Out of Order or Twice
Expected for app subscriptions: there’s no ordering guarantee, and retries can repeat events. Sequence on occurredAt and de-duplicate on the receiving side.
Endpoint Overwhelmed During Imports
Turn on the workflow action’s rate limit, return 429 when you need HubSpot to back off, and process events from a queue rather than inline.
Next Steps
- Create the properties your payload needs: HubSpot Custom Properties
- Set up the private app for API calls back into HubSpot: HubSpot Private App Setup
- Which credentials to use: HubSpot API Credentials
- General webhook debugging: Common Webhook Issues
- Rather we handle it? HubSpot Implementation Services