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

# CRM integration model: sync directions, object mapping, and verification

> The architecture behind Orbit's HubSpot and Salesforce integrations — inbound polling plus webhooks, outbound writeback with retry, object and property mapping, per-provider signature verification, and where each failure mode lands.

# CRM integration model

Orbit's CRM integrations are a **full-duplex sync**, not a one-way import. Changes flow inbound from the CRM into your Orbit contact book, and changes flow outbound from Orbit back to the CRM. This page documents that model as a contract — the two transport paths, the object and field mapping, the signature-verification rules by provider, and precisely which guarantees hold when something fails. For the step-by-step setup checklist, see [Connect HubSpot & Salesforce](/guides/hubspot-salesforce-integration); for the endpoint contract, see the [Integrations API reference](/api-reference/integrations).

## 1. Inbound from the CRM vs. outbound to the CRM

**Inbound (CRM → Orbit)** runs on two complementary transports:

* **Poller** — a scheduled sync (cadence: minutes) pulls Contact, Company, and Deal changes for HubSpot, or Contact, Account, and Opportunity changes for Salesforce, through the integration layer. It upserts matching rows into your Orbit contact book. The poller is the catch-up mechanism: it converges state even when a webhook was missed or the subscription was never reached.
* **Webhook receiver** — provider events arrive in real time at `POST /api/v1/integrations/webhooks/{provider}` and are written as soon as they arrive. The webhook path is the freshness path; the poller bounds how stale it can get.

**Outbound (Orbit → CRM)** is a queued writeback, asynchronous by design:

* A contact change in Orbit (lifecycle stage, phone, email, mapped custom attributes) emits a `contact.updated` event that enqueues a push job.
* The worker re-reads the contact row at run time, so a retry always pushes the latest state rather than a stale snapshot.
* Before the network call, the payload is hashed against the last-pushed stamp; a matching hash short-circuits the push as a no-op.
* The writeback is **fail-open**: a CRM failure never blocks the primary Orbit contact write. The link between an Orbit contact and its CRM counterpart lives in `attributes.hubspot_id` (or `attributes.salesforce_id`); without it, the push is skipped as `not_linked`.

Today the outbound writeback is wired for **HubSpot only**; Salesforce is inbound-only (see [Provider differences](#5-provider-differences)).

## 2. Object mapping

The canonical mapping out of the box:

| CRM object             | Orbit entity                   | Canonical link field       |
| ---------------------- | ------------------------------ | -------------------------- |
| HubSpot Contact        | Contact                        | `attributes.hubspot_id`    |
| HubSpot Company        | Account-level attributes       | —                          |
| HubSpot Deal           | Deal context attributes        | —                          |
| Salesforce Contact     | Contact                        | `attributes.salesforce_id` |
| Salesforce Account     | Account-level attributes       | —                          |
| Salesforce Opportunity | Opportunity context attributes | —                          |

**Field mapping** — for contacts, the core property set is email, phone, firstname, lastname, and lifecycle stage. In Orbit, the lifecycle stage is a first-class contact attribute, so campaigns and flows can branch on it in real time once inbound sync lands.

**Custom fields and the round trip** — a custom property travels in both directions when it is mapped with the `hubspot:` namespace prefix on the Orbit contact's attributes. Anything outside the mapped set syncs inbound but does not survive the outbound round trip: the writeback only pushes the core property set plus `hubspot:`-prefixed attributes. If you need a custom property to round-trip, route it through that namespace — unmapped fields are inbound-read-only.

<Warning>
  If you drop the link attribute (`attributes.hubspot_id`) from a contact, outbound writeback stops for that contact — it is skipped as `not_linked` rather than guessed at. Re-link via the contact detail page to resume.
</Warning>

## 3. Signature verification by provider

Every inbound event is verified before Orbit touches the payload, and the check **fails closed** — when a secret is configured, unverified deliveries are rejected.

**HubSpot** — the app client secret, symmetric HMAC:

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

Orbit validates the `X-HubSpot-Signature-v3` header against the client secret from its environment. You supply the same value when you register the webhook URL in your HubSpot app's developer dashboard.

**Salesforce** — a tenant-provisioned HMAC key:

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

Orbit validates the `X-Devotel-Sf-Signature` header against the key you embed in your Salesforce Apex trigger or Flow HTTP action. Because the key is provisioned per tenant rather than issued by the provider, you can rotate it without provider-side coordination.

Both formulas run over the **raw request body** — a proxy that re-serializes JSON between the provider and Orbit breaks verification. Treat signature failures as a closed gate, not a filter: a `401` from either endpoint means the secret on your side does not match.

## 4. Failure semantics by transport

The failure model differs per direction and per transport — knowing which guarantee holds is how you size your monitoring.

**Inbound poll gaps.** The poller is the convergence backstop: a missed webhook event is recovered on the next scheduled run, so the worst case is staleness bounded by the poll cadence (minutes), not loss. A manual `POST /api/v1/integrations/{id}/sync` forces catch-up outside the schedule.

**Inbound webhook failures.** A failed signature check returns `401`, and HubSpot retries its delivery (up to 10 attempts over 24 hours). An unconfigured secret returns `503` until the receiver is enabled. An endpoint that is simply unreachable leaves recovery to the poller.

**Outbound retry and dead-letter.** The writeback queue retries failures (CRM `4xx`/`5xx`, integration-layer outage, missing connection) with backoff. A push that exhausts its retries lands in the dead-letter queue, and the outcome is recorded in your org audit timeline — `crm.contact.pushed_to_hubspot` on success, `crm.contact.push_failed_hubspot` on failure. The audit record, not the queue alone, is the tenant-owned control that tells you whether a given contact change made it upstream.

**Outbound queue backpressure.** Accumulating queue depth means pushes are enqueueing but not dispatching — check the worker and Redis before assuming the CRM side is broken.

## 5. Provider differences

The two integrations share the model above with provider-specific vocabularies:

|                            | HubSpot                                  | Salesforce                                             |
| -------------------------- | ---------------------------------------- | ------------------------------------------------------ |
| **Inbound sync covers**    | Contacts, Companies, Deals               | Contacts, Accounts, Opportunities                      |
| **Canonical link field**   | `attributes.hubspot_id`                  | `attributes.salesforce_id`                             |
| **Signature contract**     | `X-HubSpot-Signature-v3` (client secret) | `X-Devotel-Sf-Signature` (tenant-provisioned HMAC key) |
| **Custom property prefix** | `hubspot:` (round-trips)                 | All attributes through integration-layer mapping       |
| **Outbound writeback**     | Queued push, wired today                 | Not wired — inbound-only                               |

**Property-name divergence.** HubSpot speaks in flat property names (`lifecyclestage`, `firstname`); Salesforce speaks in object-scoped field names (`Contact.Email`, `Account.Name`). Orbit normalizes both into the same contact attributes, so your campaigns and flows branch on one vocabulary regardless of the source CRM. The divergence only matters at the edges — when you inspect raw CRM payloads or debug a mapping — not in downstream Orbit logic.

## Where this fits

* [Connect HubSpot & Salesforce (guide)](/guides/hubspot-salesforce-integration) — the numbered setup walkthrough for this model
* [Integrations API reference](/api-reference/integrations) — the connect / sync / status / disconnect endpoint contract
* [Webhook delivery semantics](/concepts/webhook-delivery-semantics) — the at-least-once and dead-letter guarantees for Orbit's own outbound webhooks
* [On-call alerting guide](/guides/oncall-alerting) — queue-backpressure and error-signal monitoring for the sync queues
