---
title: "Test HubSpot Webhooks Locally | WebhookRelay"
meta:
  "og:description": "Test HubSpot webhooks locally and receive them on localhost without deploying. Inspect the batched payload, forward to your handler, and verify the X-HubSpot-Signature-v3 signature."
  "og:title": "Test HubSpot Webhooks Locally"
  description: "Test HubSpot webhooks locally and receive them on localhost without deploying. Inspect the batched payload, forward to your handler, and verify the X-HubSpot-Signature-v3 signature."
---

![Stripes](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/images/stripes.svg)

# **Test HubSpot Webhooks Locally**

Test HubSpot webhooks locally and receive them on localhost without deploying. Inspect the batched payload, forward to your handler, and verify the X-HubSpot-Signature-v3 signature.

![Test HubSpot Webhooks Locally (Receive HubSpot Webhooks on localhost)](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/images/blog/heroes/localhost.jpg)

You are building a HubSpot integration — syncing CRM contacts, reacting to a deal stage change, kicking off an automation — and you need to watch your handler react to a real `contact.creation` or `deal.propertyChange` event. The problem is immediate: HubSpot will only POST to a **public URL**, and your handler is running on `localhost:3000`. HubSpot has no way to reach it.

The usual workarounds are painful. Deploying to a staging server for every change is slow. Copying a sample payload from the docs into curl gives you a _guess_ at the real request — not the real headers, the real signature, or HubSpot's batched array format. What you want is to **test HubSpot webhooks locally** — real events hitting your local handler, with a URL that doesn't change every time you restart.

This guide shows how to do exactly that.

## [Why receiving HubSpot webhooks locally is tricky](#why-receiving-hubspot-webhooks-locally-is-tricky)

A webhook is just an HTTP request that HubSpot sends to a URL when something happens in your CRM. HubSpot sits on the public internet; your dev machine usually does not. It's behind a router, a corporate firewall, or both, with no public IP and no inbound ports open.

So you need something in the middle: a public endpoint HubSpot can hit, that relays each request down to your laptop without you opening a single firewall port. That's what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure HubSpot once and never touch it again.

## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin)

Before you write any handler code, find out what HubSpot actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL.

1. Copy the Webhook Bin URL.
2. In HubSpot, open your app's **Webhooks** settings (see Step 2 for exactly where) and set this URL as the **Target URL**.
3. Subscribe to an event such as `contact.creation`, then create a contact in your portal.

Inspect the captured request and you'll notice two HubSpot-specific things right away:

- **The body is a JSON array.** HubSpot **batches** events, so even a single event arrives as an array of event objects. Your handler must loop over the array.
- **The signature and timestamp headers.** Newer apps send `X-HubSpot-Signature-v3` together with `X-HubSpot-Request-Timestamp`. You'll verify these in Step 3.

Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/blog/what-is-webhook/).

## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent)

Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `hubspot`. The bucket gives you a stable public input endpoint.

Start forwarding to your local server:

```
relay forward --bucket hubspot http://localhost:3000/webhook
```

The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:3000/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/docs/webhooks/internal/localhost/).

Now update the HubSpot **Target URL** to your Webhook Relay endpoint (or just set it there from the start), trigger an event, and watch it arrive on `localhost`.

## [HubSpot-specific configuration and quirks](#hubspot-specific-configuration-and-quirks)

Webhooks live inside an **app**, not in the general portal settings:

- **Where to configure them:** create or open a **private app** (or a developer/public app) in HubSpot, then go to **App settings → Webhooks**. Set the **Target URL** to your Webhook Relay endpoint and add **subscriptions** for the events you care about — `contact.creation`, `contact.propertyChange`, `deal.creation`, `deal.propertyChange`, and so on across CRM objects.
- **You need the right scopes.** Webhook subscriptions require the relevant CRM scopes (for example, contacts) to be granted to the app.
- **Events are batched.** As noted above, the body is always a JSON **array**. Each element carries fields like `subscriptionType`, `objectId`, `propertyName`, `propertyValue`, `occurredAt`, and `eventId`. Iterate; don't assume one object.
- **Concurrency and retries.** HubSpot may deliver batches concurrently and retries failed deliveries, so make your handler **idempotent** — dedupe on `eventId` — and return a `2xx` quickly.

## [Step 3: Verify the HubSpot webhook signature](#step-3-verify-the-hubspot-webhook-signature)

Authenticate every request before trusting it. Newer HubSpot apps use the **`X-HubSpot-Signature-v3`** header, which is the most secure option and the only one with replay protection. Here's how v3 works:

1. **Check the timestamp.** Read `X-HubSpot-Request-Timestamp`. HubSpot timestamps are in **milliseconds**. Reject the request if it's more than **5 minutes** old — this is what stops replay attacks.
2. **Build the signed string.** Concatenate, in order: the HTTP **method** + the full request **URI** + the raw request **body** + the **timestamp**. Decode any URL-encoded characters in the URI (but leave the `?` that begins the query string).
3. **Compute the HMAC.** Take an **HMAC-SHA256** of that string using your app's **client secret** as the key.
4. **Base64-encode and compare.** Base64-encode the HMAC result and compare it to the `X-HubSpot-Signature-v3` header using a **constant-time** comparison.

A subtle but critical detail: the signed string includes the **full URL HubSpot actually called** — which, in this setup, is your Webhook Relay endpoint, not `localhost`. Compute and capture the v3 signature against the public URL HubSpot saw, exactly as it arrived in the forwarded request.

Older apps may still send the v1 (`X-HubSpot-Signature` as a SHA-256 over client-secret + body) or v2 schemes; v3 is the current, recommended version. To sanity-check an HMAC-SHA256 implementation quickly, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls — reading the body _after_ a JSON parser has consumed it, timing-safe comparison — read [Verify a webhook signature](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/blog/verify-webhook-signature/).

## [Replay and iterate](#replay-and-iterate)

This is where local development gets fast:

- **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured HubSpot batch against your handler without touching HubSpot at all.
- **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No object changes in the CRM, no deploys, just to test a code path.

Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the HubSpot Target URL never needs to change.

## [Get started](#get-started)

1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/webhook-bin/) — no signup needed.
2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket hubspot http://localhost:3000/webhook`.
3. Point your HubSpot private app's Target URL at the stable endpoint, trigger an event, and watch it hit `localhost`.

You'll be testing real HubSpot events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once.