[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’sdealstage, 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
Ahubspot:deal-update request naming any property other than dealstage or amount is
refused before any network call, not silently dropped:
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
(appointmentscheduled → qualifiedtobuy → presentationscheduled →
decisionmakerboughtin → contractsent → closedwon), 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.
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):
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:
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
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:
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.readcrm.objects.deals.write
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) hitMockHubSpotServer, not the real network. They run on everynpm test, no gate required. This covers the full authorize → verify → execute → confirm chain, every policy rule branch, replay with zero second calls,businessTransactionHashtamper 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-wiredPOST /executeroute this page’s examples above are captured from, againstMockHubSpotServer. A policy denial here is proven to make zero HubSpot calls two ways: the mock server’s own state is confirmed unchanged, and afetchspy 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 behindALLOW_LIVE_HUBSPOT=1plus a realTEST_HUBSPOT_PRIVATE_APP_TOKEN(must start withpat-, checked before any network call), does not run in a defaultnpm 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. Seedocs/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/amountupdate only. - Delete or archive a deal. No implementation exists, deny-by-default this milestone
touches only
dealstage/amounton 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-updatecall 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
pipelineproperty.isHubSpotStageTransitionAllowedaccepts astageOrderoverride, 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.