← All guides
HubSpotJuly 12, 20266 min read

How to Test HubSpot Webhooks on Localhost

Receive a hubspot webhook localhost-side, filter contact.creation events with JQ, transform the batched array, and keep X-HubSpot-Signature-v3 valid.

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

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:

  1. Set the Target URL to your Relayers ingest URL from step 1.
  2. Under Subscriptions, add the event types you care about — for example contact.creation, contact.propertyChange, or deal.creation.
  3. Save, and make sure the subscriptions are toggled active.

Two things to internalize about HubSpot's delivery model:

[
  {
    "subscriptionType": "contact.creation",
    "objectId": 123,
    "portalId": 987654,
    "eventId": 100,
    "occurredAt": 1752364800000
  }
]

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

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.