HubSpot webhooks are unforgiving to develop against. They only fire from a real HubSpot app in your developer account, they demand a publicly reachable HTTPS Target URL, and they arrive batched as an array of event objects — not the single JSON body most tutorials assume. On top of that, HubSpot signs every request with X-HubSpot-Signature-v3, an HMAC that hashes the request URI, so the moment you rewrite the URL to hit your machine, verification breaks.
The usual workaround is to deploy a half-finished handler to staging, trigger a contact creation, read logs, tweak, redeploy, and repeat. That loop is slow and it hides the actual payload from your debugger. This guide wires HubSpot straight to a handler running on localhost:3000 — with signatures intact and a live payload inspector — using Relayers.
What you'll build
Relayers gives you a stable public ingest URL, applies routing rules, and forwards the untouched request down a WebSocket tunnel to your app. Because the original body and headers are preserved, your HubSpot signature check runs locally exactly as it would in production.
Prerequisites
- A HubSpot developer account and an app created in it (webhooks are configured per app, not per portal). Have the app's client secret handy — it's the key HubSpot uses to sign requests.
- The
relayersCLI installed, or the desktop app. Grab either at relayers.app — the free tier covers this workflow. - A local service listening on
localhost:3000(swap the port to match yours).
1Create an endpoint in Relayers
In the desktop app, open Endpoints → Create, name it hubspot, and copy the public ingest URL. It looks like:
https://api.relayers.app/v1/webhooks/wep_8f3a1c92b7e04d51
That wep_... public ID is your Target URL for HubSpot. Use Send test now to fire a sample request so you can confirm the endpoint is live before touching HubSpot.
2Configure webhooks on your HubSpot app
In your HubSpot developer account, open your app and go to Webhooks:
- Set the Target URL to your Relayers ingest URL from step 1.
- Under Subscriptions, add the event types you care about — for example
contact.creation,contact.propertyChange, ordeal.creation. - Save, and make sure the subscriptions are toggled active.
Two things to internalize about HubSpot's delivery model:
- Events arrive batched. HubSpot groups events and POSTs them as a JSON array, even when there's a single event. Every payload looks like this:
[
{
"subscriptionType": "contact.creation",
"objectId": 123,
"portalId": 987654,
"eventId": 100,
"occurredAt": 1752364800000
}
]
- Requests are signed. HubSpot adds
X-HubSpot-Signature-v3plusX-HubSpot-Request-Timestamp. The v3 signature is an HMAC-SHA256 over the concatenation of the HTTP method, the full request URI, the raw request body, and the timestamp, keyed with your app client secret. (OlderX-HubSpot-Signatureversions exist; prefer v3.)
3Forward events to localhost
Log in and start the tunnel:
relayers login
relayers listen --forward localhost:3000
Now create a test contact in a portal that has your app installed. Within seconds the event array reaches Relayers and is forwarded to localhost:3000. Open the Events view and the payload inspector to see the exact body and headers — including the two X-HubSpot-* headers — that hit your machine.
4Route and filter with JQ
You probably don't want every subscription hitting the same handler. Relayers Rules evaluate a JQ filter against the event body; first match wins, and you can reorder rules by dragging.
Since the body is an array, index into it. To match only contact creations:
.[0].subscriptionType == "contact.creation"
Set the rule's destination to your tunnel (localhost:3000). Add more rules — say, deal.creation routing to localhost:3001 — and Relayers dispatches each event to the right place.
Batches usually contain one subscription type, but HubSpot can mix types in a single POST. If that matters, filter with
any(.[]; .subscriptionType == "contact.creation")and let your handler iterate.
5Transform the payload
Your handler may want a flatter shape than HubSpot's array. Relayers Transformations support jq, jsonata, javascript, and go_template, each with a test playground so you can iterate on real data.
A minimal jq transform that unwraps the first event:
{
type: .[0].subscriptionType,
objectId: .[0].objectId,
portalId: .[0].portalId
}
If you'd rather preserve the batch, map across the whole array instead:
[ .[] | { type: .subscriptionType, objectId: .objectId } ]
Test against a captured payload in the playground, then attach the transformation to your rule.
A note on signatures
The X-HubSpot-Signature-v3 HMAC covers the request URI and body along with the timestamp. Relayers forwards the original body and headers untouched, so your local verification can recompute the hash and match.
One caveat: because the v3 signature includes the URI, verify against the URI HubSpot actually signed — your public Relayers ingest URL (https://api.relayers.app/v1/webhooks/wep_...), not localhost. Read the raw body before parsing (HMAC is byte-sensitive) and reject requests where X-HubSpot-Request-Timestamp is older than five minutes to block replays.
Going to production
When your handler is deployed, you don't need to touch HubSpot again. In Relayers, point the rule's destination at your public production URL instead of the tunnel, or keep both with an ordered rule set. Because your app is already verifying v3 signatures locally, the same code ships unchanged.
Use Export to save your endpoint, rules, and transformations as a JSON bundle, then Import it into another workspace to reproduce the exact setup for a teammate or a staging project.
Troubleshooting
- My filter never matches. The body is an array —
.subscriptionTypeisnull. Use.[0].subscriptionTypeor iterate with.[]. - Events look truncated or grouped oddly. HubSpot batches events and expects a fast response; aim to return
2xxwell within the ~5 second window and offload heavy work to a queue. Slow responses cause retries and, eventually, throttling. - Signature verification fails. Confirm you're on
X-HubSpot-Signature-v3(not an older version), that you're hashing the raw body bytes, and that the URI you sign matches the public ingest URL. Check the timestamp header hasn't drifted past your tolerance. - Nothing arrives at all. Re-check that the subscription is active and the app is installed in the test portal, then fire Send test from Relayers to isolate whether the problem is HubSpot-side or local.
Wrap-up
With a Relayers endpoint in front of your HubSpot app, you get a stable Target URL, JQ routing that understands HubSpot's batched array, a transform playground, and a payload inspector — all forwarding to localhost with signatures preserved. No more redeploy-to-debug loop.
Create your first endpoint at relayers.app and download the app to start forwarding HubSpot webhooks to your machine in minutes.