> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect HubSpot & Salesforce end to end

> Run the full CRM loop in one guide: connect HubSpot or Salesforce via OAuth, watch inbound contact sync + outbound event delivery, wire the webhook signatures correctly, and debug the connection when it stalls.

# 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`](/api-reference/integrations) — this page is the "do this, then this" walkthrough for the same surface.

<Note>
  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](#provider-differences) section — same shape, a few different property names.
</Note>

## 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.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/integrations/connect \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "integration_id": "hubspot" }'
```

Use `"salesforce"` for Salesforce. The response returns a JSON envelope with an `auth_url`.

<Check>
  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.
</Check>

**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.

<Info>
  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.
</Info>

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.

<Check>
  After updating a contact's lifecycle stage in Orbit, watch your audit log for `crm.contact.pushed_to_hubspot`. That's the success marker.
</Check>

***

## 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`:

```
signature = base64(HMAC-SHA256(clientSecret, method + uri + body + timestamp))
```

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`:

```
signature = base64(HMAC-SHA256(hmacSecret, unixMsTimestamp + "." + rawBody))
```

You supply `hmacSecret` in your Salesforce org's Apex trigger or Flow HTTP action. The endpoint is `POST /api/v1/integrations/webhooks/salesforce`.

<Tip>
  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).
</Tip>

***

## 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 events** — `crm.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

| Symptom                                  | Cause                                      | Fix                                                                                                                                                                                 |
| ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` on outbound push                   | Revoked or expired OAuth token             | Re-run the connect flow; Nango auto-refreshes tokens but revocation requires a new OAuth consent.                                                                                   |
| `404` on push                            | `attributes.hubspot_id` missing or stale   | The contact wasn't linked during inbound sync — delete the stale id or re-link via the contact detail page.                                                                         |
| `429` rate-limited                       | HubSpot API quota exceeded                 | Orbit retries automatically. Reduce push frequency or upgrade your HubSpot tier if it persists.                                                                                     |
| `skipped_not_connected` on every push    | Integration server not configured          | `DEVOTEL_NANGO_SECRET_KEY` must be set on the API pod; the push no-ops until Nango is reachable.                                                                                    |
| Webhooks returning `503`                 | `DEVOTEL_HUBSPOT_CLIENT_SECRET` unset      | Set the client secret in your API environment — the receiver is disabled until it's present.                                                                                        |
| Inbound sync showing old values          | Poller scheduled gap vs real-time          | Poll cadence is \~minutes; rely on the webhook for real-time. Trigger a manual `POST /integrations/{id}/sync` to catch up.                                                          |
| `sub_status: partial` on status endpoint | Subscription registration partially failed | Some HubSpot subscriptions couldn't be created. Check `nango_connection_state.metadata.hubspot_subscriptions_summary` for the failure set and re-sync — registration is idempotent. |

***

## Provider differences

|                               | HubSpot                                      | Salesforce                                                    |
| ----------------------------- | -------------------------------------------- | ------------------------------------------------------------- |
| **OAuth integration\_id**     | `hubspot`                                    | `salesforce`                                                  |
| **Webhook signature header**  | `X-HubSpot-Signature-v3` (app client secret) | `X-Devotel-Sf-Signature` (HMAC key you provision)             |
| **Webhook receiver endpoint** | `POST /api/v1/integrations/webhooks/hubspot` | `POST /api/v1/integrations/webhooks/salesforce`               |
| **Inbound sync covers**       | Contacts, Companies, Deals                   | Contacts, Accounts, Opportunities                             |
| **Canonical link field**      | `attributes.hubspot_id`                      | `attributes.salesforce_id`                                    |
| **Custom attribute prefix**   | `hubspot:` (for portal-specific properties)  | N/A — all attributes sync through Nango mapping               |
| **Outbound writeback**        | `hubspot-contact-push` queued push           | Not wired — outbound push is HubSpot-only today               |
| **Setup hint**                | Webhooks + poll + push all automatic         | Poll-based; webhooks need your Apex/Flow setup to emit events |

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.

***

## Related reading

* [Integrations API reference](/api-reference/integrations) — endpoint map for the connect/sync/status/ disconnect surface.
* [On-call alerting guide](/guides/oncall-alerting) — the same queue-backpressure + Sentry pattern applies to the CRM sync queues.
* [Contact external-sync contract](/api-reference/contacts) — how Orbit decides whether an inbound event is a new contact or an update to an existing one.
