The loop
Create an endpoint and it prints a permanent ingest URL. Everything sent there is captured whole — headers, body, the exact bytes — before anything else happens. Stream those events to a port on your machine, and replay any one of them as often as you like while you fix the handler. Forwarding runs from your own machine, so it isn't a billable delivery.
What's different about HubSpot
A HubSpot delivery is not an event. HubSpot batches up to 100 events into a single POST, and the five-second timeout applies to the whole batch rather than to each event inside it. Nothing in that batch is ordered either: HubSpot does not guarantee notifications arrive in the order they occurred, does not guarantee only one notification per event, and states outright that eventId is not guaranteed to be unique. You order by each notification's occurredAt millisecond timestamp and deduplicate yourself. Locally, the first symptom is a handler written for one notification being handed a batch of them.
Where the URL goes
Which kind of app you built decides where the target URL lives. In a legacy private app created through the UI: Development, then Legacy apps, your app, the Webhooks tab, Edit webhooks, Target URL, Create subscription, Commit changes. Legacy private apps built with projects do not currently support webhook subscriptions, and there is no API for managing private-app subscriptions. On the current developer platform there is no UI step — targetUrl goes in a -hsmeta.json file inside src/app/webhooks/.
HubSpot ships a test button for this. On the Webhooks tab, hover a subscription, click View details, then click Test: HubSpot sends the sample event payload listed in the panel to your configured target URL and shows your endpoint's response at the bottom. Legacy public apps have the equivalent Test this subscription flow. It is a canned sample, so read it as a shape check, not a data check.
What HubSpot sends
Subscription types read as object then action: the object lowercase, snake_case when it is two words, and the action in lowerCamelCase — creation, deletion, propertyChange, associationChange, restore, merge, privacyDeletion, newMessage. A generic form using object.creation style types also exists, but it is a public beta scoped to public apps and carries the object type in the payload's objectTypeId field.
- contact.creation
- contact.propertyChange
- contact.deletion
- company.propertyChange
- deal.creation
- conversation.newMessage
Worth knowing first
Five seconds covers the whole batch
The clock is per POST, not per event. If your service takes longer than five seconds to send back a response to a batch of notifications, HubSpot treats it as a failure — as it does a connection failure, or any 4xx or 5xx status. A 2xx acknowledges receipt. Design the handler around that budget, because a hundred events can share it.
Retries run for a day
Failed notifications are retried up to 10 times, spread over the next 24 hours with varying delays and per-notification randomization so concurrent failures do not all come back at once. No per-attempt schedule is published. Every payload carries attemptNumber, starting at 0, so your handler can tell a first delivery from a retry.
Settings are cached for up to five minutes
Webhook settings can be cached for up to five minutes, so a change to the target URL, the concurrency limit or the subscriptions themselves may take that long to take effect. Concurrency is 10 in-flight requests per account that installed your app, and it is an adjustable setting rather than a fixed ceiling.
Signatures are v3, with their own five minutes
Requests are signed with X-HubSpot-Signature-v3, lowercase v, alongside X-HubSpot-Request-Timestamp. Validation is a base64 HMAC-SHA256 over request method, request URI, request body and timestamp, keyed on the app's client secret and compared in constant time, rejecting anything with a timestamp older than five minutes. Two unrelated five-minute numbers, worth keeping straight.
The logs tab shows, it does not resend
Past deliveries live under the app's Logs tab, then Webhooks: a filterable history of successful and unsuccessful requests you can drill into. No redelivery or resend action is documented there. Once the 24-hour retry window closes on a delivery you missed, the log entry is the record you are left with.
The journal API is a different surface
HubSpot's newer webhooks journal is polled rather than pushed — subscriptions are written to a journal your app reads, and there is no targetUrl anywhere in it. The path /docs/api/webhooks now serves that journal documentation, so it is easy to land there while looking for push-delivery behaviour and read the wrong thing.
Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.