Skip to main content
[AVAILABLE], hermetic test suite plus a live run against a real HubSpot developer/test account. packages/connector-hubspot/, docs/CLAIMS.md §3.10. This is the second connector in this codebase (after Razorpay) that talks to a real external system, everything else registered by default (vendor-payment) or offered as a reference (SAP, Salesforce, Workday, Oracle) is a mock, see The gateway.

What this connector does

Updates one property, or two, on one HubSpot object type: a Deal’s dealstage, optionally alongside amount, in a single PATCH /crm/v3/objects/deals/{dealId} call. Nothing else. It does not touch Contacts or Companies, does not delete or archive deals, has no webhook or event-driven trigger, and does not perform multi-object writes, see [FUTURE] below. It does not change anything about the Execution Gateway, how policies are evaluated (see Policies and the decision), Replay, Receipt Generation, Verification, or the REST API, those work exactly as documented elsewhere on this site. @parmana/connector-hubspot is a standalone package, not a subdirectory of @parmana/connector-sdk like the four reference mocks and Razorpay, it depends on @parmana/connector-sdk’s Connector contract the same way they do.

Deny-by-default at the property level

A hubspot:deal-update request naming any property other than dealstage or amount is refused before any network call, not silently dropped:
Silently dropping an unsupported property could mask a caller’s real intent behind an update that quietly did less than requested, refusing outright is the safer failure mode.

The policy model

policies/hubspot-deal-update/1.0.0/policy.json rejects a proposed dealstage transition unless it moves strictly forward through a fixed default stage order (appointmentscheduledqualifiedtobuypresentationscheduleddecisionmakerboughtincontractsentclosedwon), with one explicit exception: moving to closedlost is allowed from any non-terminal stage. Any move backward, any move out of a terminal stage (closedwon or closedlost), and any move to or from a stage id the order doesn’t recognize, are all rejected. Separately, an amount change is rejected if its absolute delta from the deal’s current amount exceeds a configured threshold (10,000, in the deal’s own currency units) unless the caller declares preAuthorizedForAmountChange: true.
preAuthorizedForAmountChange is a caller-declared signal this milestone does not independently verify. There is no separately signed approval artifact behind it yet, a caller asserting true is taken on faith, the same way an unbound signal on any other policy is. Absent, it defaults to false, the safe default, see [FUTURE] below.
boundSignals is present from this policy’s first version, not added after a live demonstration of a signal/intent mismatch the way razorpay-refund/1.0.0 was (see Razorpay and Policies and the decision for that history):
A caller’s declared proposedDealStage/proposedAmount signals must exactly equal intent.parameters.dealstage/intent.parameters.amount, checked by SignalIntentBinder before policy evaluation ever runs, so the facts the policy approved describe the same update that actually executes, not just a payload that happens to look correct in isolation.

Try it: an authorized update and a denied one

An authorized request, dealstage moving forward one step, and a denied one, an attempted backward move, sent to the real, production-wired POST /execute route in a hermetic test process (packages/api/tests/integration/hubspot-deal-update.integration.test.ts), captured from an actual local run, 2026-07-31:
The denied request, same deal, an attempted backward move (qualifiedtobuy back to appointmentscheduled), rejected before the connector is ever dispatched, zero HubSpot API calls, confirmed by a fetch spy in the underlying test, not just by inspecting the response:

Try it without touching HubSpot at all

No environment variables, no network calls. This spins up an in-process MockHubSpotServer with a fake test-mode token and runs the real HubSpotDealUpdateService against it, loading the real policy above from disk. Real output from a run against this exact code:
Hash values are random per run, everything else in the output is stable. Covers dealstage/ amount update only, nothing else demonstrated here.

Setup

Create a Private App in the target HubSpot account (Settings → Integrations → Private Apps), scoped to exactly two CRM scopes:
  • crm.objects.deals.read
  • crm.objects.deals.write
No broader scope is needed, and none is checked for by this connector, HubSpot itself will reject a request if the token’s scopes are insufficient. The Private App generates one token, that’s the credential this connector reads, there is no separate “key ID” the way Razorpay’s Basic-auth credential has two parts.

Configuration

What’s proven where, precisely

This is the part worth reading in full before deciding how much to trust this connector.
  • packages/connector-hubspot/tests/unit/ (42 tests) hit MockHubSpotServer, not the real network. They run on every npm test, no gate required. This covers the full authorize → verify → execute → confirm chain, every policy rule branch, replay with zero second calls, businessTransactionHash tamper rejection, and the placeholder-credential guard against a real-shaped endpoint.
  • packages/api/tests/integration/hubspot-deal-update.integration.test.ts (3 tests) drives the same real, production-wired POST /execute route this page’s examples above are captured from, against MockHubSpotServer. A policy denial here is proven to make zero HubSpot calls two ways: the mock server’s own state is confirmed unchanged, and a fetch spy confirms literally nothing reached its base URL.
  • packages/api/tests/integration/hubspot-live.integration.test.ts (3 tests) is the only test in this codebase that calls the real HubSpot API for this connector. Gated behind ALLOW_LIVE_HUBSPOT=1 plus a real TEST_HUBSPOT_PRIVATE_APP_TOKEN (must start with pat-, checked before any network call), does not run in a default npm test. Run to completion against a real HubSpot developer/test account: a reachability check against a non-existent deal id, a policy denial proven to make zero real HubSpot calls, and a non-destructive amount change against a real test deal, read live, nudged by a small within-threshold delta, verified via an independent live read, then reverted to its original value in the same run, safe to repeat, unlike an irreversible action. See docs/CLAIMS.md §3.10 for the full trace, including one test-fixture bug this live run caught and how it was fixed.

[FUTURE]

  • Contacts and Companies. No implementation exists. This connector covers Deal dealstage/amount update only.
  • Delete or archive a deal. No implementation exists, deny-by-default this milestone touches only dealstage/amount on an existing deal.
  • A webhook or event-driven trigger. No implementation exists. This connector is request-response only, there is no asynchronous confirmation loop the way Razorpay’s refund lifecycle has one, see Verify Razorpay webhooks for what that took.
  • Multi-object transactions. No implementation exists, each hubspot:deal-update call is a single Deal write.
  • Per-pipeline stage-transition rules. The forward-only stage order above is one global order, not keyed by a deal’s own pipeline property. isHubSpotStageTransitionAllowed accepts a stageOrder override, nothing wires it to a pipeline yet.
  • Independent verification of preAuthorizedForAmountChange. It’s an unverified caller-declared signal today, see the warning above.

Next

Connector Development Guide

The pattern this connector follows, and how to build the next one.

Policies and the decision

What boundSignals closes, and why it’s here from this policy’s first version.