Skip to main content

Connect HubSpot & Salesforce end to end

Orbit’s CRM integrations are a bidirectional loop, not a one-way import:
  • Inbound — a contact changes in HubSpot or Salesforce, Orbit’s contact book picks up the change (we poll + receive webhooks), and campaigns that reference the lifecycle stage fire in real time.
  • Outbound — a contact changes in Orbit (lifecycle stage, phone, email, your own custom attributes), Orbit pushes the update back to HubSpot, and Orbit events can be delivered into HubSpot as timeline events.
This guide is the numbered checklist operators run when they wire up either provider. The endpoint reference it links back to is /api-reference/integrations — this page is the “do this, then this” walkthrough for the same surface.
HubSpot is the primary example below because it exercises the full surface (OAuth connect, inbound webhook, outbound writeback, timeline events). Salesforce is covered in the Provider differences section — same shape, a few different property names.

What you’ll set up

  1. A Nango OAuth connection from Orbit to your HubSpot portal (or Salesforce org).
  2. Inbound sync — Orbit polls HubSpot/Salesforce for contact + company changes, and receives webhook events for real-time updates.
  3. Outbound writeback — when a contact changes in Orbit, a queued push updates the corresponding HubSpot contact.
  4. Event delivery — Orbit events can flow into HubSpot as timeline/webhook events if you’ve configured event subscriptions.
  5. Signature-verified webhook endpoints — inbound HubSpot events are verified with your app client secret; Salesforce uses an HMAC key you provision.

Step 1 — Connect via OAuth (admin only)

Initiate the connect flow from the Integrations API. Owner or admin role required — this is the only step that touches provider credentials, so Orbit restricts it to admins.
Use "salesforce" for Salesforce. The response returns a JSON envelope with an auth_url.
Redirect the browser to auth_url. After OAuth consent, the portal is linked and the connection is live — check GET /api/v1/integrations for the provider’s row.
Manual fallback — if you can’t run OAuth from this environment, use the Nango dashboard’s “Connect” flow directly for the same integration_id. The connection is the same object either way.

Step 2 — Understand what syncs inbound

Once connected, Orbit runs two inbound paths: Poller — a scheduled sync (typically every few minutes) pulls contact, company, and deal changes via Nango. It reads the set of syncs registered under your integration template and upserts them into your Orbit contact book. Webhook receiver — real-time events from HubSpot/Salesforce hit POST /api/v1/integrations/webhooks/{provider} and are written as soon as they arrive. For HubSpot these are the Orbit contacts fields that get populated (mapped from the HubSpot property set): email, phone, firstname, lastname, lifecyclestage, plus the custom hubspot: namespace attributes if you’ve created them.
The subscription set Orbit installs on first connect covers contact creation, field changes on the core properties, and deal/ticket events. Operator-side re-registration via the HubSpot UI is rarely needed — the service handles the canonical set.
For Salesforce the sync covers Contact, Account, and Opportunity objects, with the same upsert semantics. Once you’ve configured outbound events below, Contact field changes flow in real time.

Step 3 — Outbound: push Orbit changes back to the CRM

When a contact changes in Orbit, a queued job propagates the diff upstream:
  • The contact.updated event in Orbit enqueues a hubspot-contact-push job (BullMQ).
  • The worker re-reads the contact row fresh at run time (so retries see the latest state), maps the fields into a HubSpot PATCH body, and pushes it via the Nango proxy.
  • Idempotency: if the pushed payload hash matches the last-pushed stamp, the push short-circuits without a network call.
  • Fail-open: any failure (HubSpot 4xx/5xx, Nango outage, missing connection) is logged + audited but never blocks the primary operation — the contact change in Orbit always completes. Three BullMQ retries with exponential backoff absorb transient blips.
What gets pushed — the canonical identifier link is attributes.hubspot_id on the Orbit contact (set during the inbound link). If that attribute is missing, the push is skipped as not_linked — it means Orbit has no upstream counterpart to update. Field-mapped properties are email, phone, firstname, lastname, lifecyclestage, and your hubspot:-prefixed custom attributes.
After updating a contact’s lifecycle stage in Orbit, watch your audit log for crm.contact.pushed_to_hubspot. That’s the success marker.

Step 4 — Wire the webhook correctly (signature verification)

Both providers push webhooks to an endpoint on your domain. Orbit verifies the sender before doing anything with the payload — the check fails closed when a secret is set. HubSpot — verification uses X-HubSpot-Signature-v3:
Orbit reads clientSecret from its environment; you supply the same value when you add the webhook URL to your HubSpot App in the developer dashboard. The endpoint is POST /api/v1/integrations/webhooks/hubspot and expects 2xx within 5s (HubSpot retries up to 10 times over 24 hours). Salesforce — verification uses X-Devotel-Sf-Signature:
You supply hmacSecret in your Salesforce org’s Apex trigger or Flow HTTP action. The endpoint is POST /api/v1/integrations/webhooks/salesforce.
Orbit’s webhooks are dispatched in the order they arrive; the endpoint expects the Body to be JSON (for Salesforce, JSON-translated from SOAP by your trigger or Flow).

Step 5 — Failure modes and monitoring

CRM integrations are fail-open by design — a HubSpot outage or a revoked OAuth token never blocks a contact write in Orbit, it only means the push to the CRM will land when the connection is restored. What to monitor:
  • Queue depth — if the BullMQ hubspot-contact-push queue backs up, pushes are accumulating but not dispatching. Check Redis connectivity and the webhook-worker for stuck jobs.
  • Connection status — poll GET /api/v1/integrations/{id}/status. A disconnected status or a stale last_sync timestamp means the OAuth connection needs re-auth.
  • Audit log eventscrm.contact.push_failed_hubspot is the push-failure record; crm.contact.pushed_to_hubspot is the success record. Both surface in your org audit timeline.
  • Sentry — any transport error from the push path is captured with service: hubspot-contact-push + reason tag.
If the inbound webhook isn’t firing, check that the webhook URL is still configured in your HubSpot App’s developer dashboard and that DEVOTEL_HUBSPOT_CLIENT_SECRET matches your portal’s client secret. A mismatched secret returns 401 from the endpoint — correct it and the next delivery will verify.

Step 6 — Troubleshooting common issues


Provider differences

For a mostly-automated path, put your effort into HubSpot. For Salesforce, budget one manual step (pasting the Apex trigger or configuring the Flow) after OAuth connect before outbound events flow.