TMS CRM Integration with Salesforce and HubSpot: The Technical Side

Featured image with a peach pastel background and a soft blob pattern, showing the post title "TMS CRM Integration with Salesforce and HubSpot"

TMS CRM Integration with Salesforce and HubSpot: The Technical Side

A TMS CRM integration works best as a thin, event-driven sync. The training management system (TMS) owns sessions, enrollments, attendance and invoices. The CRM owns companies, contacts and deals. The two connect through stable external IDs, push events, and a scheduled reconciliation job that catches whatever the events miss.

Most guides stop at “connect your LMS to your CRM to save admin time”. This one covers what an engineer or product lead has to decide: which objects to sync, which way, how to match people without duplicates, and how API limits shape the design. Limits and behaviors are as of November 2026, from each vendor’s developer documentation.

Key takeaways

  • Pick one system of record per object. Sync the TMS-owned objects (enrollments, attendance, invoices) to the CRM, and the CRM-owned objects (companies, deals) to the TMS.
  • Match records on external IDs and upsert, never on email alone. Salesforce rejects an ambiguous upsert with HTTP 300, and HubSpot lets you upsert on a unique ID property.
  • Use webhooks or change events for speed, and a scheduled reconciliation poll for safety. Neither vendor promises that every event arrives, and HubSpot also does not guarantee order or single delivery.
  • Batch writes and plan for limits: 200 records per Salesforce collections call and 100 per HubSpot batch call, with a shared daily request budget on both platforms.

What should a TMS CRM integration sync, and which system owns each object?

Sync the objects that cross the sales-to-delivery boundary: clients, contacts, deals, courses, sessions, enrollments and invoice status. Give each object exactly one system of record, so edits flow in one direction and conflicts cannot occur. The CRM owns the commercial relationship. The TMS owns anything that happens on a training calendar.

This starting map for a training provider assumes one owner and one match key per row. Adjust it to your own data model.

Object System of record Direction Trigger Match key
Client company CRM CRM to TMS Account created or deal won CRM account ID stored in the TMS
Contact or booker CRM CRM to TMS Contact created or updated CRM contact ID
Delegate (learner) TMS TMS to CRM Enrollment confirmed TMS person ID as external ID, email as fallback
Course TMS TMS to CRM Course published or retired TMS course ID
Session or cohort TMS TMS to CRM Created, rescheduled or cancelled TMS session ID
Enrollment TMS TMS to CRM Status change: confirmed, cancelled, attended TMS enrollment ID
Deal or opportunity CRM CRM to TMS Stage changes to closed won CRM deal ID
Invoice status TMS or accounting system TMS to CRM Issued, paid, overdue Invoice ID

Custom objects are the cleanest home: one record per session or enrollment, linked to a contact. HubSpot’s knowledge base says custom objects require an Enterprise subscription. On lower plans, flatten the data: put the latest enrollment summary on the contact and the booked course on the deal as a line item.

Should the sync be one-way or two-way?

Start with one-way flows per object, as in the table, rather than a general two-way sync. A true bidirectional sync of the same field needs conflict rules, such as last write wins, and invites loops where each side’s update triggers the other. Per-object ownership gives you the benefit of two-way data without those problems.

Two-way is justified only for a few shared fields, such as a phone number or job title. Decide the winner by field, and ignore change events your own integration caused. Without that, one edit can bounce between systems until a rate limit stops it.

How do you match people and companies across both systems without duplicates?

Store each system’s ID in the other and match on those, never on email alone. Create an external ID field in the CRM that holds the TMS person ID, and save the CRM contact ID in the TMS. Then every write is an upsert keyed on an ID that is unique by design.

Salesforce upserts through a PATCH to the object’s external ID field. Per the Salesforce REST API upsert documentation, no match creates a record (HTTP 201), one match updates it (200 in current API versions), and multiple matches fail with HTTP 300 and change nothing. That 300 is a feature: it surfaces duplicates instead of silently picking one.

HubSpot’s batch upsert for contacts takes an idProperty that is either email or a custom unique identifier property. HubSpot’s properties guide allows up to ten unique ID properties per object, which you create by setting hasUniqueValue to true. Use one for the TMS person ID so a changed email address does not create a second contact.

Who is the contact: the booker or the delegate?

Training providers often have two people per enrollment: the booker who pays and the delegate who attends. Model them separately. The booker is a CRM contact tied to the deal. The delegate is a TMS person who gets a CRM contact only if you market to learners. Decide this before you build.

Read more: What Is an API First LMS? Why It Matters for Growing Training Providers

Pro tip: Add a merge strategy before launch. People change employers and email addresses. Plan for a “merge” event on each side: when the CRM merges two contacts, the TMS should repoint its person record and keep both external IDs. A sync with no merge path slowly fills with duplicates.

Should a TMS CRM integration use webhooks, polling, or both?

Use both. Push events (webhooks or change events) give you updates within seconds, but neither Salesforce nor HubSpot guarantees perfect delivery. A scheduled poll that compares records changed since the last run closes the gaps. Treat events as a fast path and the poll as the source of truth.

HubSpot’s webhooks guide sets the rules your receiver must follow. It must answer within five seconds. Failed notifications are retried up to 10 times over 24 hours. HubSpot does not guarantee order or one notification per event, so duplicates are possible and the occurredAt timestamp is the only reliable clock. Webhook calls do not count against your API rate limits.

On the Salesforce side, Change Data Capture publishes events when records are created, updated, deleted or undeleted. The CDC allocations page says events are stored for 72 hours and can be replayed using the Replay ID, so a subscriber that was offline can resume where it stopped. Store the last Replay ID you processed.

Rank objects by what a late or missed update costs. A confirmed enrollment that never reaches the CRM loses a sales follow-up. A missed invoice-paid status can trigger a wrong dunning email.

Approach Strength Weakness Use it for
Webhooks or change events only Updates arrive in seconds Missed, duplicate or out-of-order events Nothing critical on its own
Polling only Simple, self-healing Delay equals poll interval and every poll spends API calls Low-volume, low-urgency objects such as course lists
Events plus reconciliation poll Fast and eventually correct More moving parts Enrollments, invoices, deal-won triggers

How do you design a TMS CRM integration around API limits?

Design for batching and for a shared budget. Both platforms count every call against an account-wide allowance that other tools also use. Write in batches, cache what you read, and keep a retry queue that backs off when told to slow down. Check the current numbers before sizing anything.

Limit (as of November 2026) Salesforce HubSpot
Daily request budget 100,000 plus licenses times a per-license allowance, plus add-ons, for the whole org (Enterprise Edition) 625,000 (Professional) or 1,000,000 (Enterprise) per account for private apps
Short-term limit 25 concurrent requests running 20 seconds or longer in production orgs 190 per 10 seconds per private app on Professional and Enterprise; 110 per 10 seconds per installing account for public OAuth apps
Batch write size 200 records per sObject Collections request 100 records per batch request
Push events CDC defaults on Enterprise: 5 entities and 25,000 events per 24 hours, shared by all clients Webhooks do not count toward API limits; up to 1,000 subscriptions per app
When exceeded CDC subscription is disconnected with an error; concurrency overruns return REQUEST_LIMIT_EXCEEDED HTTP 429 with a policyName of DAILY or SECONDLY

These figures come from Salesforce’s API request limits and the CDC allocations page above, and HubSpot’s API usage guidelines. They vary by edition and tier, and vendors change them, so confirm yours.

What does that mean in a worked example?

Assume a provider syncs three objects per enrollment: the delegate, the enrollment record and the deal update. Written one record at a time, that is three calls. At 400 enrollments a day, that is 1,200 calls, which is small against Salesforce’s own example of 115,000 requests a day for an Enterprise org with 15 licenses.

The risk is a spike, not the average (the 400 a day and three objects are illustrative assumptions, so substitute your own). Importing 20,000 historical enrollments one call at a time costs 60,000 calls, over half of that same org’s daily budget, which other integrations share. With batches, the arithmetic changes: 20,000 records at 200 per Salesforce collections request is 100 calls per object, or 300 for three objects. On HubSpot at 100 per batch, it is 200 calls per object, or 600, before association calls.

Can a bulk edit in the CRM overwhelm the integration?

Yes. A mass update of 20,000 Salesforce records can produce events on the order of 20,000 (Salesforce may merge changes to records of one object within a second, so treat that as a ceiling). Against the default Enterprise allocation of 25,000 events a day, one admin action could use most of it. Select only the objects you need for change capture and alert on delivery usage.

How should the integration handle failures and retries?

Assume every call can fail, repeat or arrive late, and make each operation safe to run twice. Key every write on an external ID so a replay updates instead of duplicating. Queue failures, retry with backoff, and move records that keep failing to a dead-letter queue that a person reviews.

Partial success is the case teams forget. Per Salesforce’s sObject Collections upsert documentation, a well-formed request returns HTTP 200 even if some items fail, with a success flag and errors on each result. Check every item, not just the status code, unless you set allOrNone to true.

Pro tip: Reconcile with an overlap window. Run the reconciliation poll on a watermark such as “records modified since the last successful run”, but subtract a safety margin of several minutes. HubSpot notes that new or updated records can take a few moments to appear in search results, and the overlap costs only a few repeated upserts, which are harmless when writes are idempotent.

Log source ID, target ID, event ID, outcome and timestamp for every synced record. Alert on rising 429 responses (HubSpot expects errors to stay under 5% of daily requests for marketplace apps), a disconnected CDC subscription, and a growing retry queue.

Should you build the integration, buy a connector, or use your TMS vendor’s?

Buy or reuse a connector when your needs match its field mappings, and build when your objects, such as sessions or multi-client bookings, do not fit a generic template. The deciding costs are ownership and change: someone must maintain the integration when either vendor changes an API or a limit.

Ask any vendor concrete questions. Which CRM objects does the connector write? Does it upsert on external IDs? Does it use webhooks, polling or both, and what happens on a 429? SimpliTrain’s integrations page states live CRM connections to Salesforce and GenSuit for current clients, with custom CRM integrations through its API for other platforms. The same page puts a fully public API layer on its near-term roadmap, so confirm the current API scope during evaluation.

If you are weighing a custom platform partly for this kind of control, Mundrisoft’s piece on custom enterprise LMS platforms covers why some organizations build for integration and compliance.

What should you do next?

Start with the sync map. List your objects, name one owner per object, and choose a match key for each. Then write down your expected daily record volume and your largest one-off import, and compare both with the limits for your Salesforce edition or HubSpot tier.

Next, prototype one flow end to end: a confirmed enrollment becomes a CRM upsert, with a retry queue and a reconciliation job. Prove that a replayed event creates no duplicate before adding the next object.

If you are evaluating a TMS for this, book a SimpliTrain demo and bring this checklist of questions about objects, IDs, events and limits.

Frequently asked questions

What is a TMS CRM integration?

It is a connection that moves data between a training management system and a CRM. Typical flows send client and deal data from the CRM to the TMS, and send enrollments, attendance and invoice status from the TMS back to the CRM, so sales and delivery teams work from the same facts.

Can one TMS integrate with both Salesforce and HubSpot?

Yes, if the TMS has an API or a connector for each. The sync map, external IDs and failure handling stay the same. What changes is the details: Salesforce uses external ID fields and change events, while HubSpot uses unique ID properties and app webhooks, and each has its own limits.

Do I need real-time sync between my TMS and CRM?

Rarely for everything. Real-time matters for triggers such as a deal being won or an enrollment being confirmed. Course lists, session details and reports can sync on a schedule. A mix of events and a reconciliation poll gives speed where it matters and correctness everywhere else.

What happens when I hit the Salesforce or HubSpot API limit?

HubSpot returns HTTP 429, with a policy name that shows whether the daily or the 10-second limit was hit. On Salesforce, exceeding the CDC delivery allocation returns an error and disconnects the subscription. Your integration should pause, back off, queue the work, and resume, rather than retrying in a tight loop.

How do I stop duplicate contacts when syncing training data?

Match on external IDs instead of email. Store the TMS person ID in a Salesforce external ID field or a HubSpot unique ID property, and use upsert for every write. Handle merges explicitly, and decide upfront whether bookers and delegates are separate records.