Connect HubSpot — every sales touch on the same timeline as the app journey
Connect your HubSpot and every sales touch — deals and their stage changes, contacts as they appear — lands on the same customer timeline as the journey Crossdeck already captures in your app, joined to the person by their contact email. In reverse, a Prism by Crossdeck card renders inside HubSpot on every contact and deal, showing the revenue, product usage and errors HubSpot can't see. And if you send email — from your own code, through HubSpot's transactional API or any tool — one helper joins each click to the person. Crossdeck only ever reads your HubSpot. Nothing is written back.
TL;DR
- Connect read-only in one click. Dashboard → Developers → Integrations → HubSpot → Connect. OAuth 2.0, read-only scopes (
crm.objects.contacts.read,crm.objects.deals.read). You approve in HubSpot's own UI; no API key to paste. - Sales touches join the app journey. Deals and contacts land on the same customer timeline as sign-ups, feature usage, revenue and errors — joined by contact email, not stitched by hand.
- Events you'll see:
deal_stage_changed(on every deal modification, carrying amount, pipeline and stage) andlead_created(once, when a contact first appears). - The Prism card runs inside HubSpot. On every contact and deal, your reps see the product-side truth — last active, plan, revenue, recent errors, journey — without leaving the CRM.
- Join your email clicks to identity with one line,
campaignLink()(below) — the same helper across every email tool. - Read-only, always. Crossdeck never writes to HubSpot. Tokens live in Google Cloud Secret Manager. Disconnect anytime from the dashboard.
What you get
HubSpot is where your sales and marketing team's version of the customer lives — the deals, the pipeline stages, the leads. Crossdeck already holds the product version: the anonymous first visit, the sign-up, the feature usage, the subscription, the errors they hit. On their own, each is half the picture. Connecting HubSpot merges the two halves onto one timeline, keyed to one person.
Concretely, once connected:
- Deals and their stage changes appear on the customer's Journey — you can see the marketing-attributed visit that started weeks before the deal opened, and the product usage that ran alongside the sales cycle.
- Contacts resolve to the Crossdeck customer they already are — not a second, duplicate record. One human, whether they came in through your app or the sales team added them to HubSpot.
- The Prism by Crossdeck card renders inside HubSpot, so your reps read the product-side truth without leaving the CRM. This ships with the integration, the same as every Crossdeck integration.
Crossdeck reads your HubSpot; it does not write to it. No deals are created, no properties overwritten, no contacts mutated. The connection holds only the two read scopes above — it has no permission to change anything in your portal.
How the loop works
The read side is automatic once you connect. Crossdeck polls your HubSpot on an hourly schedule (the source of truth), reading the deals and contacts already in your portal and joining each to the person's journey by contact email. It also pulls a person's HubSpot record onto their timeline on the spot the moment they click a tagged campaign link, so the sales context is there without waiting for the next poll.
In reverse, the Prism card is served to HubSpot's UI extension on demand — HubSpot signs the request, Crossdeck verifies it, and returns the joined portfolio for the contact or deal being viewed (below).
Connect HubSpot (OAuth, read-only)
→ Crossdeck reads your deals + contacts (hourly, and on a campaign click)
→ each joins the person's app journey by contact email
→ the Prism card renders inside HubSpot with what HubSpot can't see
→ (optional) stamp your email links so a click joins to the person
Step 1 — Connect HubSpot
In the Crossdeck dashboard, open Developers → Integrations → HubSpot and click Connect. Crossdeck redirects you to HubSpot's OAuth 2.0 authorize screen, where you approve the connection in HubSpot's own UI under whichever portal you're signed into. There is no API key to generate and paste.
Crossdeck requests only two read scopes — crm.objects.contacts.read and crm.objects.deals.read. Disconnect anytime from the same page; Crossdeck stops polling and discards the tokens.
What flows into the journey
Once connected, these events land on the person's timeline, each joined to them by contact email:
lead_created— fires once, when a contact first appears in your HubSpot. Carries the contact's lifecycle stage, lead status and original source.deal_stage_changed— fires on every deal modification (a stage move is the one you care about), carrying the deal's amount, pipeline and stage.
Because they share the timeline with everything else Crossdeck holds, you can finally see the whole arc — the content someone read before they became a lead, the product usage that ran alongside the sales cycle, and which journeys produce deals that actually close.
Identity model — joined by email
HubSpot records join the person by their contact email. A deal carries no email of its own, so Crossdeck resolves the deal's primary associated contact and matches on that address. If a contact's email matches a Crossdeck customer, the two are the same person; if no one carries that address yet, the record is held and stitches to the person on a later identify.
Crossdeck's core identity graph never treats an email as a proof of identity — that is a deliberate security stance (see How identity works). Integrations are different: an email match between a HubSpot contact and a known customer is good enough to join a sales touch to a person. The two systems are separate by design, so an integration match never weakens the graph. To make the match airtight rather than email-soft, use the campaign-link stamp (below), which binds a specific click to a specific person deterministically.
The Prism by Crossdeck card
The integration ships a Prism by Crossdeck UI-extension card that renders inside HubSpot on contacts and deals. When a rep opens a record, HubSpot signs the request (X-HubSpot-Signature-v3) and calls Crossdeck; Crossdeck verifies the signature — so the request provably came from your portal — and returns the joined portfolio for that person: revenue, plan, last active, recent errors and their recent journey. Your reps read the product-side truth without leaving the CRM, and nothing about it is written back to HubSpot.
Join your email clicks to identity
When you send email — from your own backend, through HubSpot's transactional API, or any tool — you know the recipient's address. Stamp your email links with campaignLink() and the click binds to that person on landing, so an email touch joins their journey and you can follow what they did next in your app. This is the same one helper across every email tool (Resend, HubSpot, your own SMTP) — it is one line, written into your email-sending code.
// your backend, wherever you build a link that goes into an email
import { CrossdeckServer } from "@cross-deck/node"
const crossdeck = new CrossdeckServer({ secretKey: process.env.CROSSDECK_SECRET_KEY })
// Stamp the link with the recipient's identity ticket (server-side), then send
// the returned URL however you already send email.
const { url } = await crossdeck.campaignLink({
email: contact.email, // resolved to the person, once, here
url: "https://app.yoursite.com/welcome", // where they should land
})
// → put `url` in the email body
campaignLink() mints an identity bind for the address you name, so it must run on your server with a secret key (cd_sk_ or a workspace key). A publishable key is refused — a browser must never mint a bind for an arbitrary address. The email travels in the request body, never in a URL. Prefer raw HTTP? It's POST /v1/campaign/email-link with { email, url }; the response is { url }. The returned URL carries only an opaque cd_ref ticket (no PII), single-use, valid 30 days.
It degrades honestly: with the Crossdeck Web SDK on your landing page the click deterministically binds the session to the person and the full post-click journey joins their identity; without it, the touch still lands by identity — only the deep session stitch softens. Never implied, always stated.
Read-only & security
- Read-only. Crossdeck holds two read scopes only and has no write path into your HubSpot. It cannot create, edit or delete deals, contacts or properties.
- Tokens in Secret Manager. The OAuth access and refresh tokens HubSpot issues are stored in Google Cloud Secret Manager — never in Firestore, never in the browser, never in a bundle. The connector reads them server-side to poll the API.
- The card is signature-verified. Every Prism-card request from HubSpot is verified against HubSpot's
X-HubSpot-Signature-v3, so Crossdeck answers only requests that provably came from your portal. - Disconnect anytime. Remove the connection from Developers → Integrations → HubSpot. Crossdeck stops polling and discards the tokens. Data already on the timeline stays; no new HubSpot events ingest until you reconnect.
Troubleshooting
Connected, but sales touches aren't appearing on the timeline
The connector polls hourly, so allow a little time after connecting for the first pull (or click a tagged campaign link to pull a person's record on the spot). If touches still don't appear, the contacts aren't resolving to a customer — check that the HubSpot contact's email matches an email Crossdeck knows for that person. A contact whose email Crossdeck has never seen is held until a later identify connects it.
Deals show up under the wrong person, or as new people
Almost always the emails differ. HubSpot records match on contact email, so a contact whose HubSpot email is a work address while Crossdeck knows their personal one resolves softly or not at all. Stamp your email links (above) to make the join deterministic per click, and make sure your app identifies people by the same email HubSpot holds.
The Prism card is blank inside HubSpot
The card renders for a contact or deal that resolves to a Crossdeck customer. If the record's email isn't one Crossdeck knows, there's no person to show yet — the card states that rather than inventing data.
What's next
- Identify users — how a person comes to carry an email, and how the same human collapses across anonymous device, email and rail ids.
- How identity works — the identity graph, and why email is an integration join but never a bank-grade anchor.
- Connect Resend — the same email-click stamping, on the email-sending rail.
- Prism — the card that renders inside HubSpot, and reading the joined portfolio from your own AI.
- Node SDK — the server-side client that carries
campaignLink()andidentify().
Read-only HubSpot connector — OAuth 2.0 connect (contacts + deals read scopes), hourly poll + on-click enrichment, lead_created / deal_stage_changed onto the person's timeline joined by contact email, the signature-verified Prism card, and the optional campaignLink() email-click stamp (July 27, 2026). Related: Connect Resend, How identity works.
Using an AI assistant? Read this page as clean markdown — index.md — or the whole docs index at /docs/llms.txt.