Nurture TechnologiesNurture Tech
Integrations24 min read·July 28, 2026

Salesforce Integration GuideAccounts, Leads, Opportunities, Sync, and Enterprise Automation

SalesforceReactNext.jsVueAngularSvelteAstroNode.jsNestJSFastAPILaravelDjangoGoSpring BootASP.NET CoreRust

A complete Salesforce CRM integration guide covering accounts, leads, opportunities, Platform Events, Change Data Capture, OAuth 2.0, and enterprise automation across every major frontend and backend stack.

Salesforce is the enterprise CRM that underpins the revenue operations of most large organizations. When your product needs to integrate with an enterprise customer's Salesforce instance, the stakes are high: the data is critical, the volumes are large, and the organizational dependency is deep.

This guide covers the full Salesforce integration surface authentication with Connected Apps and JWT, REST and Bulk APIs, Platform Events, Change Data Capture, and enterprise automation workflows across every major frontend and backend stack.

Overview

Salesforce is a cloud-based CRM platform that has been the dominant system of record for enterprise revenue teams for over two decades. Its data model is built around a set of standard objects Accounts, Contacts, Leads, Opportunities, Cases, Campaigns, and Activities that represent the full arc of a B2B customer relationship from initial interest through closed deal, ongoing support, and renewal.

Standard Salesforce objects relevant to most integrations:

  • Accounts: companies or organizations that your business has a relationship with; the anchor record for B2B data
  • Contacts: individual people associated with accounts; linked to activities, opportunities, and cases
  • Leads: unqualified prospects that have not yet been associated with an account; converted to Contact + Account + Opportunity when qualified
  • Opportunities: potential revenue-generating deals at various pipeline stages, each with an amount, close date, and probability
  • Cases: customer support and service requests linked to accounts and contacts
  • Campaigns: marketing initiatives with associated campaign members tracking response and attribution
  • Custom Objects: organization-specific data models defined in Salesforce with the same API access as standard objects
  • Flows and Apex: Salesforce's native automation layer that can trigger actions, update records, and call external systems

Salesforce dominates enterprise CRM for several structural reasons. Its data model is flexible enough to represent almost any B2B business process through custom objects and relationships. Its automation platform (Flows, Process Builder, Apex triggers) allows business users and developers to build workflows without external code. Its AppExchange ecosystem connects Salesforce to virtually every enterprise system category. And its reporting and forecasting tools give leadership teams a single, authoritative view of pipeline, revenue, and customer health.

For software teams, this dominance means that any product serving enterprise customers will eventually receive a Salesforce integration requirement. Understanding the integration surface thoroughly before that requirement arrives saves weeks of architectural rework.

Business Benefits

Single Source of Truth for Customer Data

In most enterprises, customer data lives in four to eight systems: the product, the CRM, the ERP, the support desk, the marketing automation platform, and billing. A Salesforce integration collapses this into a unified record that all teams work from. When a sales rep opens an account, they see product usage data. When customer success opens a contact, they see deal history and support cases side by side. Decisions get faster because the data is in one place.

Sales Pipeline Visibility and Forecasting

Salesforce Opportunities are the basis for revenue forecasting. When your product automatically creates or updates opportunities based on real customer behavior a trial activation, a usage threshold, an upgrade event the pipeline becomes accurate in real time rather than dependent on salespeople manually updating deal stages. Leaders can forecast with confidence because the data reflects actual product signals, not optimistic estimates.

Automated Lead Routing and Qualification

A product-qualified lead a user who has activated a feature, hit a usage milestone, or completed a meaningful trial flow can be automatically created as a Salesforce Lead or Opportunity with full context attached: company size, usage data, trial start date, and activation events. The lead routes to the right owner based on territory rules, and the rep receives a notification before the prospect has raised their hand. This compresses the time-to-first-contact from days to minutes.

Customer Lifecycle Tracking

An integrated Salesforce instance tracks the complete customer journey: from first website interaction (Campaign membership) through lead qualification (Lead conversion), deal closing (Opportunity stage), onboarding, and expansion (additional Opportunities). When churn risk signals appear in your product, that context is immediately visible in Salesforce so customer success can act. When a customer expands their usage, a renewal opportunity is created automatically.

Enterprise Reporting and Compliance

Enterprise customers often require Salesforce as the system of record for audit, compliance, and board reporting. A reliable integration ensures that the data in Salesforce reflects reality not a two-week-old export. Revenue reports are accurate, compliance audits can be run against live data, and leadership teams make investment decisions with confidence that the numbers represent the current state of the business.

Architecture Overview

Enterprise Salesforce integrations require a layered architecture. The frontend never touches Salesforce directly. A backend service layer owns all Salesforce API credentials and all outbound calls. Inbound events from Salesforce are received via Platform Events or Change Data Capture, verified, queued, and processed asynchronously.

salesforce-architecture.mmd
flowchart TD
  FE["Frontend\nReact / Next.js / Angular"] -- "User actions\n(lead submit, deal view)" --> API["Backend API Layer"]
  API -- "REST / Bulk API\n(CRUD, SOQL queries)" --> SF["Salesforce APIs\nv59.0 REST + Bulk v2"]
  SF -- "CRM Platform\nAccounts / Leads / Opportunities" --> SFDB[("Salesforce Org\n+ Custom Objects")]
  SFDB -- "Platform Events\n(CDC, custom events)" --> PE["Platform Event Bus"]
  PE -- "Streamed via CometD\n(jsforce / Bayeux)" --> SUB["Event Subscriber\n(your backend)"]
  SUB -- "Enqueue event" --> Q["Message Queue\nBullMQ / SQS / RabbitMQ"]
  Q -- "Process event" --> WORKER["Background Worker"]
  WORKER -- "Sync state" --> DB[("Your Database")]
  API -- "Cache SOQL lookups" --> CACHE["Redis Cache"]
  API -- "Large imports" --> BULK["Bulk API v2 Worker"]

Integration Patterns

  • Real-time sync: product event fires a background job that immediately calls the Salesforce REST API to create or update a record; suitable for high-value events (lead creation, deal stage changes, support case creation)
  • Scheduled sync: a cron job runs every N minutes and queries Salesforce via SOQL for records changed since the last run using SystemModstamp filtering; suitable for lower-priority data consistency checks and reconciliation
  • Event-driven sync: Salesforce publishes a Platform Event or Change Data Capture event; your subscriber receives it via CometD streaming, queues it, and processes it asynchronously; suitable for Salesforce-initiated changes that your product needs to react to
  • Bulk sync: Salesforce Bulk API v2 handles millions of records in a single job; use for initial data migrations, nightly reconciliation of large object types, and large-scale backfills

Architectural rule: your backend owns the Salesforce OAuth tokens and all API calls. Your frontend never communicates with Salesforce directly. Platform Events are received by a dedicated subscriber process running server-side, not by client code. All event processing happens in a queue worker, not synchronously in the event handler.

Frontend Integration

The frontend's role in a Salesforce integration is capturing user input and displaying CRM data retrieved through your backend. All Salesforce API calls including authentication, querying, and record creation are made exclusively from the backend. The frontend communicates only with your own API layer.

React

React handles Salesforce-connected interfaces through custom hooks that abstract backend API calls. Lead capture forms, opportunity dashboards, and account detail views all fetch data from your API, which fetches from Salesforce.

hooks/useSalesforceLead.ts
import { useState } from "react";

interface LeadPayload {
  firstName: string;
  lastName: string;
  email: string;
  company: string;
  title?: string;
  phone?: string;
}

export function useSalesforceLead() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function submitLead(data: LeadPayload) {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch("/api/crm/leads", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error(await res.text());
      return await res.json();
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : "Submission failed");
      return null;
    } finally {
      setLoading(false);
    }
  }

  return { submitLead, loading, error };
}

Do not expose Salesforce record IDs directly to the frontend. Return your internal record IDs and resolve them to Salesforce IDs server-side. This prevents clients from constructing Salesforce API calls and keeps your integration layer replaceable without frontend changes.

Next.js

Next.js server actions run entirely on the server, making them ideal for Salesforce lead creation from marketing pages and landing forms. The form data never reaches the client-side JavaScript bundle, and the Salesforce credentials never leave the server.

app/actions/salesforce.ts
"use server";

import { getSalesforceClient } from "@/lib/salesforce";
import { redirect } from "next/navigation";

export async function submitEnterpriseLeadForm(formData: FormData) {
  const sf = await getSalesforceClient();

  const result = await sf.sobject("Lead").upsert(
    {
      FirstName: formData.get("firstName") as string,
      LastName: formData.get("lastName") as string,
      Email: formData.get("email") as string,
      Company: formData.get("company") as string,
      Title: formData.get("title") as string,
      LeadSource: "Website",
    },
    "Email"
  );

  if (!result.success) {
    throw new Error("Lead creation failed");
  }

  redirect("/thank-you");
}

Vue.js

Vue 3 composables abstract Salesforce-related API calls and expose reactive state for use across components. Separate composables for leads, opportunities, and account lookups keep concern boundaries clean and make testing straightforward.

composables/useSalesforce.ts
import { ref } from "vue";

export function useSalesforceLead() {
  const loading = ref(false);
  const error = ref<string | null>(null);
  const leadId = ref<string | null>(null);

  async function createLead(payload: {
    firstName: string;
    lastName: string;
    email: string;
    company: string;
  }) {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch("/api/crm/leads", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (!res.ok) throw new Error("Lead creation failed");
      const data = await res.json();
      leadId.value = data.id;
    } catch (e: unknown) {
      error.value = e instanceof Error ? e.message : "Error";
    } finally {
      loading.value = false;
    }
  }

  return { createLead, loading, error, leadId };
}

Angular

Angular enterprise applications benefit from a centralized SalesforceService registered at the root level. This single service handles all CRM interactions and is injected into whichever components and feature modules need access. Angular's HttpClient with interceptors manages authentication headers, retry logic, and error normalization consistently across all Salesforce API calls.

services/salesforce.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { retry, catchError, throwError } from "rxjs/operators";

interface Lead { firstName: string; lastName: string; email: string; company: string; }

@Injectable({ providedIn: "root" })
export class SalesforceService {
  constructor(private http: HttpClient) {}

  createLead(lead: Lead): Observable<{ id: string }> {
    return this.http
      .post<{ id: string }>("/api/crm/leads", lead)
      .pipe(retry(2), catchError((err) => throwError(() => err)));
  }

  getOpportunities(accountId: string): Observable<unknown[]> {
    return this.http
      .get<unknown[]>(`/api/crm/accounts/${accountId}/opportunities`)
      .pipe(retry(2), catchError((err) => throwError(() => err)));
  }

  searchAccounts(query: string): Observable<unknown[]> {
    return this.http
      .get<unknown[]>(`/api/crm/accounts?q=${encodeURIComponent(query)}`)
      .pipe(catchError((err) => throwError(() => err)));
  }
}

Svelte

SvelteKit form actions handle Salesforce lead creation server-side with progressive enhancement. The form submits to a named action, which runs entirely on the server and calls your Salesforce service layer before redirecting. No Salesforce logic or credentials reach the browser.

+page.server.ts
import type { Actions } from "./$types";
import { createSalesforceLead } from "$lib/salesforce";

export const actions: Actions = {
  submitLead: async ({ request }) => {
    const data = await request.formData();
    await createSalesforceLead({
      firstName: String(data.get("firstName")),
      lastName: String(data.get("lastName")),
      email: String(data.get("email")),
      company: String(data.get("company")),
    });
    return { success: true };
  },
};

Solid.js

Solid.js createResource handles asynchronous Salesforce data fetching with fine-grained reactivity. Use a signal as the query key so that changing the selected account ID automatically triggers a refetch of that account's opportunities without re-rendering unrelated UI.

Qwik

Qwik's routeAction$ and routeLoader$ run on the server, making them natural integration points for Salesforce calls. A routeLoader$ pre-fetches account data before the page renders. A routeAction$ handles lead form submissions server-side. Neither adds any JavaScript bundle cost to the client for the CRM interaction itself.

Astro

Astro API endpoints handle Salesforce form submissions at the edge, create leads in Salesforce, and redirect to confirmation pages. Static marketing pages with Astro-powered lead forms are fully decoupled from the React or Vue components that may render the rest of the site, with Salesforce calls running entirely at the Astro server layer.

pages/api/leads.ts
import type { APIRoute } from "astro";
import { createSalesforceLead } from "../../lib/salesforce";

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  const result = await createSalesforceLead(body);
  return new Response(JSON.stringify({ id: result.id }), {
    status: 201,
    headers: { "Content-Type": "application/json" },
  });
};

Backend Integration

Salesforce provides official SDKs for JavaScript and Java. For all other languages, the Salesforce REST API is well-documented and straightforward to call with any HTTP client. All integrations should use the latest available API version (v59.0 at time of writing) and a dedicated service class that centralizes all Salesforce logic.

Node.js

$npm install jsforce

jsforce is the most complete Salesforce client for Node.js. It supports OAuth flows, SOQL queries, CRUD on all Salesforce objects, Bulk API v2, and Platform Event streaming via CometD. Instantiate a single connection and reuse it across all requests, refreshing the access token when it expires.

lib/salesforce.ts
import jsforce from "jsforce";

let connection: jsforce.Connection | null = null;

export async function getSalesforceClient(): Promise<jsforce.Connection> {
  if (connection) return connection;

  connection = new jsforce.Connection({
    instanceUrl: process.env.SF_INSTANCE_URL,
    accessToken: process.env.SF_ACCESS_TOKEN,
  });

  return connection;
}

export async function upsertLead(email: string, fields: Record<string, string>) {
  const sf = await getSalesforceClient();
  const result = await sf.sobject("Lead").upsert(
    { Email: email, ...fields },
    "Email"
  );
  if (Array.isArray(result)) return result[0];
  return result;
}

export async function queryLeads(soql: string) {
  const sf = await getSalesforceClient();
  const result = await sf.query(soql);
  return result.records;
}

export async function createOpportunity(fields: {
  Name: string;
  AccountId: string;
  StageName: string;
  CloseDate: string;
  Amount: number;
}) {
  const sf = await getSalesforceClient();
  return sf.sobject("Opportunity").create(fields);
}

NestJS

Wrap the jsforce client in an injectable NestJS service. Register SalesforceModule as a global module so any feature module can inject SalesforceService without re-importing the module. Use OnModuleInit to establish the connection at startup and log the connected Salesforce org.

salesforce/salesforce.service.ts
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import jsforce, { Connection } from "jsforce";

@Injectable()
export class SalesforceService implements OnModuleInit {
  private conn: Connection;
  private readonly logger = new Logger(SalesforceService.name);

  constructor(private config: ConfigService) {}

  async onModuleInit() {
    this.conn = new jsforce.Connection({
      instanceUrl: this.config.get<string>("SF_INSTANCE_URL"),
      accessToken: this.config.get<string>("SF_ACCESS_TOKEN"),
    });
    this.logger.log("Salesforce connection initialized");
  }

  async createLead(fields: Record<string, string>) {
    this.logger.log(`Creating SF Lead: ${fields["Email"]}`);
    return this.conn.sobject("Lead").create(fields);
  }

  async updateOpportunityStage(id: string, stageName: string) {
    this.logger.log(`Updating Opportunity ${id} to ${stageName}`);
    return this.conn.sobject("Opportunity").update({ Id: id, StageName: stageName });
  }

  async query<T>(soql: string): Promise<T[]> {
    const result = await this.conn.query<T>(soql);
    return result.records;
  }

  async bulkUpsert(objectType: string, records: object[], externalIdField: string) {
    const job = this.conn.bulk.createJob(objectType, "upsert", {
      extIdField: externalIdField,
    });
    const batch = job.createBatch();
    return new Promise((resolve, reject) => {
      batch.execute(records);
      batch.on("response", resolve);
      batch.on("error", reject);
    });
  }
}

Python FastAPI

$pip install httpx
services/salesforce.py
import httpx
import os
from typing import Any

SF_INSTANCE = os.getenv("SF_INSTANCE_URL")
SF_TOKEN = os.getenv("SF_ACCESS_TOKEN")
SF_API = f"{SF_INSTANCE}/services/data/v59.0"

HEADERS = {
    "Authorization": f"Bearer {SF_TOKEN}",
    "Content-Type": "application/json",
}

async def upsert_lead(email: str, fields: dict[str, Any]) -> dict:
    async with httpx.AsyncClient(timeout=15.0) as client:
        # Check for existing lead
        soql = f"SELECT Id FROM Lead WHERE Email = '{email}' LIMIT 1"
        search = await client.get(f"{SF_API}/query", params={"q": soql}, headers=HEADERS)
        search.raise_for_status()
        records = search.json().get("records", [])

        if records:
            lead_id = records[0]["Id"]
            res = await client.patch(
                f"{SF_API}/sobjects/Lead/{lead_id}",
                headers=HEADERS,
                json=fields,
            )
            res.raise_for_status()
            return {"id": lead_id, "created": False}
        else:
            res = await client.post(
                f"{SF_API}/sobjects/Lead",
                headers=HEADERS,
                json={"Email": email, **fields},
            )
            res.raise_for_status()
            return {**res.json(), "created": True}

async def create_opportunity(fields: dict) -> dict:
    async with httpx.AsyncClient(timeout=15.0) as client:
        res = await client.post(
            f"{SF_API}/sobjects/Opportunity",
            headers=HEADERS,
            json=fields,
        )
        res.raise_for_status()
        return res.json()

Laravel

app/Services/SalesforceService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class SalesforceService
{
    private string $instanceUrl;
    private string $apiBase;

    public function __construct()
    {
        $this->instanceUrl = config('services.salesforce.instance_url');
        $this->apiBase = $this->instanceUrl . '/services/data/v59.0';
    }

    private function headers(): array
    {
        return [
            'Authorization' => 'Bearer ' . config('services.salesforce.access_token'),
            'Content-Type' => 'application/json',
        ];
    }

    public function upsertLead(string $email, array $fields): array
    {
        $soql = "SELECT Id FROM Lead WHERE Email = '{$email}' LIMIT 1";
        $search = Http::withHeaders($this->headers())
            ->get("{$this->apiBase}/query", ['q' => $soql])
            ->throw()->json();

        if (!empty($search['records'])) {
            $id = $search['records'][0]['Id'];
            Http::withHeaders($this->headers())
                ->patch("{$this->apiBase}/sobjects/Lead/{$id}", $fields)
                ->throw();
            return ['id' => $id, 'created' => false];
        }

        $result = Http::withHeaders($this->headers())
            ->post("{$this->apiBase}/sobjects/Lead", array_merge(['Email' => $email], $fields))
            ->throw()->json();

        return ['id' => $result['id'], 'created' => true];
    }

    public function createOpportunity(array $fields): array
    {
        return Http::withHeaders($this->headers())
            ->post("{$this->apiBase}/sobjects/Opportunity", $fields)
            ->throw()->json();
    }
}

Django

services/salesforce.py
import requests
import os

SF_API = os.getenv("SF_INSTANCE_URL") + "/services/data/v59.0"
HEADERS = {
    "Authorization": f"Bearer {os.getenv('SF_ACCESS_TOKEN')}",
    "Content-Type": "application/json",
}

def upsert_lead(email: str, fields: dict) -> dict:
    soql = f"SELECT Id FROM Lead WHERE Email = '{email}' LIMIT 1"
    search = requests.get(f"{SF_API}/query", params={"q": soql}, headers=HEADERS)
    search.raise_for_status()
    records = search.json().get("records", [])

    if records:
        lead_id = records[0]["Id"]
        requests.patch(
            f"{SF_API}/sobjects/Lead/{lead_id}",
            headers=HEADERS,
            json=fields,
        ).raise_for_status()
        return {"id": lead_id, "created": False}

    res = requests.post(
        f"{SF_API}/sobjects/Lead",
        headers=HEADERS,
        json={"Email": email, **fields},
    )
    res.raise_for_status()
    return {**res.json(), "created": True}

def run_soql(soql: str) -> list:
    res = requests.get(f"{SF_API}/query", params={"q": soql}, headers=HEADERS)
    res.raise_for_status()
    return res.json().get("records", [])

Go

salesforce/client.go
package salesforce

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "os"
)

var (
    instanceURL = os.Getenv("SF_INSTANCE_URL")
    accessToken = os.Getenv("SF_ACCESS_TOKEN")
    apiBase     = instanceURL + "/services/data/v59.0"
)

func authHeader() string { return "Bearer " + accessToken }

func CreateLead(fields map[string]interface{}) (string, error) {
    body, _ := json.Marshal(fields)
    req, _ := http.NewRequest("POST", apiBase+"/sobjects/Lead", bytes.NewBuffer(body))
    req.Header.Set("Authorization", authHeader())
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    var result struct{ Id string }
    json.NewDecoder(resp.Body).Decode(&result)
    return result.Id, nil
}

func QuerySOQL(soql string) ([]map[string]interface{}, error) {
    u := apiBase + "/query?q=" + url.QueryEscape(soql)
    req, _ := http.NewRequest("GET", u, nil)
    req.Header.Set("Authorization", authHeader())
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    var result struct {
        Records []map[string]interface{} `json:"records"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decode error: %w", err)
    }
    return result.Records, nil
}

Spring Boot

Salesforce provides an official Java SDK (force-rest-api) but most Spring Boot teams use WebClient directly against the Salesforce REST API. Register a SalesforceClient bean with the instance URL base path and Authorization header. Wrap all calls in a service with retry using Spring Retry and a circuit breaker for resilience.

SalesforceService.java
@Service
public class SalesforceService {
    private final WebClient webClient;
    private final String apiBase;

    public SalesforceService(
        @Value("${sf.instance-url}") String instanceUrl,
        @Value("${sf.access-token}") String token
    ) {
        this.apiBase = instanceUrl + "/services/data/v59.0";
        this.webClient = WebClient.builder()
            .baseUrl(apiBase)
            .defaultHeader("Authorization", "Bearer " + token)
            .defaultHeader("Content-Type", "application/json")
            .build();
    }

    public Mono<Map> createLead(Map<String, Object> fields) {
        return webClient.post()
            .uri("/sobjects/Lead")
            .bodyValue(fields)
            .retrieve()
            .onStatus(HttpStatusCode::isError, r ->
                Mono.error(new RuntimeException("SF error: " + r.statusCode())))
            .bodyToMono(Map.class);
    }

    public Mono<Map> querySOQL(String soql) {
        return webClient.get()
            .uri(uriBuilder -> uriBuilder.path("/query").queryParam("q", soql).build())
            .retrieve()
            .bodyToMono(Map.class);
    }
}

ASP.NET Core

Register a typed HttpClient for Salesforce in Program.cs with the base address and Authorization header. Inject SalesforceService into controllers and other services via the DI container. Add Polly retry and circuit breaker policies to handle transient Salesforce API failures gracefully.

Services/SalesforceService.cs
public class SalesforceService
{
    private readonly HttpClient _http;
    private readonly string _apiBase;

    public SalesforceService(HttpClient http, IConfiguration config)
    {
        _http = http;
        _apiBase = config["Salesforce:InstanceUrl"] + "/services/data/v59.0";
    }

    public async Task<string> CreateLeadAsync(Dictionary<string, object> fields)
    {
        var res = await _http.PostAsJsonAsync($"{_apiBase}/sobjects/Lead", fields);
        res.EnsureSuccessStatusCode();
        var body = await res.Content.ReadFromJsonAsync<JsonElement>();
        return body.GetProperty("id").GetString()!;
    }

    public async Task<JsonElement> QueryAsync(string soql)
    {
        var encoded = Uri.EscapeDataString(soql);
        var res = await _http.GetAsync($"{_apiBase}/query?q={encoded}");
        res.EnsureSuccessStatusCode();
        return await res.Content.ReadFromJsonAsync<JsonElement>();
    }

    public async Task UpdateRecordAsync(string objectType, string id, Dictionary<string, object> fields)
    {
        var res = await _http.PatchAsJsonAsync($"{_apiBase}/sobjects/{objectType}/{id}", fields);
        res.EnsureSuccessStatusCode();
    }
}

Rust

salesforce/client.rs
use reqwest::Client;
use serde_json::{json, Value};
use std::collections::HashMap;

pub struct SalesforceClient {
    client: Client,
    api_base: String,
    token: String,
}

impl SalesforceClient {
    pub fn new(instance_url: &str, token: &str) -> Self {
        Self {
            client: Client::new(),
            api_base: format!("{}/services/data/v59.0", instance_url),
            token: token.to_owned(),
        }
    }

    pub async fn create_lead(&self, fields: HashMap<&str, &str>) -> Result<String, reqwest::Error> {
        let payload: Value = fields.iter().map(|(k, v)| (*k, json!(v))).collect();
        let res: Value = self
            .client
            .post(format!("{}/sobjects/Lead", self.api_base))
            .bearer_auth(&self.token)
            .json(&payload)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;
        Ok(res["id"].as_str().unwrap_or("").to_owned())
    }

    pub async fn query(&self, soql: &str) -> Result<Vec<Value>, reqwest::Error> {
        let res: Value = self
            .client
            .get(format!("{}/query", self.api_base))
            .query(&[("q", soql)])
            .bearer_auth(&self.token)
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?;
        Ok(res["records"].as_array().cloned().unwrap_or_default())
    }
}

Authentication and Security

Salesforce authentication is built around Connected Apps OAuth 2.0 application registrations inside a Salesforce org that define permissions, allowed scopes, and the callback URL for authorization flows. Every integration requires a Connected App.

Connected Apps and OAuth Scopes

Create a Connected App in Salesforce Setup. Set the OAuth scopes to the minimum required: api (REST API access), refresh_token (for token refresh), and optionally offline_access. Additional scopes like full give broader access but violate the principle of least privilege. Note the Consumer Key and Consumer Secret these are your client_id and client_secret.

OAuth 2.0 Web Server Flow

Use the Web Server Flow when your integration acts on behalf of a Salesforce user who needs to authorize your application. The user is redirected to Salesforce's authorization endpoint, approves access, and is redirected back to your callback with an authorization code. Your backend exchanges the code for an access token and a refresh token.

auth/salesforce-oauth.ts
import jsforce from "jsforce";

const oauth2 = new jsforce.OAuth2({
  loginUrl: "https://login.salesforce.com",
  clientId: process.env.SF_CLIENT_ID!,
  clientSecret: process.env.SF_CLIENT_SECRET!,
  redirectUri: process.env.SF_REDIRECT_URI!,
});

// Step 1: Build authorization URL
export function getAuthorizationUrl(state: string) {
  return oauth2.getAuthorizationUrl({ scope: "api refresh_token", state });
}

// Step 2: Handle callback and exchange code for tokens
export async function handleOAuthCallback(code: string) {
  const conn = new jsforce.Connection({ oauth2 });
  await conn.authorize(code);
  return {
    accessToken: conn.accessToken,
    refreshToken: conn.refreshToken,
    instanceUrl: conn.instanceUrl,
  };
}

// Step 3: Build an authenticated connection from stored tokens
export async function getConnectionFromTokens(
  accessToken: string,
  refreshToken: string,
  instanceUrl: string
) {
  const conn = new jsforce.Connection({
    oauth2,
    accessToken,
    refreshToken,
    instanceUrl,
  });
  conn.on("refresh", (newAccessToken: string) => {
    // Persist the new access token to your database
    console.log("Salesforce token refreshed");
  });
  return conn;
}

JWT Bearer Token Flow

The JWT Bearer Token flow is the correct choice for server-to-server integrations that run without user interaction. Generate an RSA key pair, upload the public certificate to your Connected App in Salesforce, and sign a JWT assertion with the private key. Exchange the signed JWT for an access token at Salesforce's token endpoint. This flow requires no user authorization step and is suitable for background jobs and API services.

auth/salesforce-jwt.ts
import { createSign } from "crypto";
import { readFileSync } from "fs";

const PRIVATE_KEY = readFileSync(process.env.SF_PRIVATE_KEY_PATH!);
const CLAIM_URL = "https://login.salesforce.com";

function buildJWTAssertion(username: string): string {
  const now = Math.floor(Date.now() / 1000);
  const header = Buffer.from(JSON.stringify({ alg: "RS256" })).toString("base64url");
  const payload = Buffer.from(
    JSON.stringify({
      iss: process.env.SF_CLIENT_ID,
      sub: username,
      aud: CLAIM_URL,
      exp: now + 300,
    })
  ).toString("base64url");

  const signingInput = `${header}.${payload}`;
  const sign = createSign("RSA-SHA256");
  sign.update(signingInput);
  const signature = sign.sign(PRIVATE_KEY, "base64url");

  return `${signingInput}.${signature}`;
}

export async function getAccessTokenJWT(username: string): Promise<string> {
  const assertion = buildJWTAssertion(username);
  const res = await fetch("https://login.salesforce.com/services/oauth2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
      assertion,
    }),
  });
  if (!res.ok) throw new Error("JWT auth failed: " + await res.text());
  const { access_token } = await res.json();
  return access_token;
}

Never store Salesforce private keys in source code or environment variable files committed to version control. Store them in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) and inject them at runtime. A leaked private key gives an attacker access to your entire Salesforce org with no expiration.

Webhooks and Event Processing

Salesforce does not use webhooks in the traditional sense. Instead, it provides three mechanisms for notifying external systems of changes: Platform Events, Change Data Capture, and Outbound Messages (legacy). Platform Events are the modern standard for event-driven Salesforce integrations.

Platform Events

Platform Events are custom Salesforce objects with an __e suffix that act as a publish-subscribe message bus. Your Salesforce org publishes events, and subscribers (external systems or Apex triggers) receive them asynchronously via the CometD streaming protocol. Platform Events decouple Salesforce from downstream systems and support retry, replay, and fan-out.

events/salesforce-subscriber.ts
import jsforce from "jsforce";

const conn = new jsforce.Connection({
  instanceUrl: process.env.SF_INSTANCE_URL,
  accessToken: process.env.SF_ACCESS_TOKEN,
});

// Subscribe to a custom Platform Event
export function subscribeToPlatformEvents(eventName: string) {
  conn.streaming.topic(`/event/${eventName}`).subscribe(async (event: unknown) => {
    const payload = event as {
      data: { payload: Record<string, unknown>; event: { replayId: number } };
    };
    console.log("Platform Event received, replayId:", payload.data.event.replayId);
    await queue.add("sf-platform-event", {
      eventName,
      payload: payload.data.payload,
      replayId: payload.data.event.replayId,
    }, {
      attempts: 5,
      backoff: { type: "exponential", delay: 2000 },
    });
  });
}

// Publish a Platform Event
export async function publishPlatformEvent(
  eventName: string,
  fields: Record<string, unknown>
) {
  const sf = await getSalesforceClient();
  return sf.sobject(eventName).create(fields);
}

Change Data Capture

Change Data Capture (CDC) automatically publishes Platform Events for every create, update, delete, and undelete operation on selected Salesforce objects. Enable CDC for Accounts, Contacts, Leads, or Opportunities in Salesforce Setup and subscribe to the generated event channels to keep your application state synchronized with Salesforce without polling.

events/cdc-subscriber.ts
import jsforce from "jsforce";

export function subscribeToContactCDC(conn: jsforce.Connection) {
  conn.streaming.topic("/data/ContactChangeEvent").subscribe((event: unknown) => {
    const msg = event as {
      data: {
        payload: {
          ChangeEventHeader: { changeType: string; recordIds: string[] };
          Email?: string;
          LastName?: string;
        };
      };
    };
    const { changeType, recordIds } = msg.data.payload.ChangeEventHeader;
    queue.add("sf-contact-change", {
      changeType,   // "CREATE" | "UPDATE" | "DELETE" | "UNDELETE"
      recordIds,
      fields: msg.data.payload,
    });
  });
}

Idempotency and Replay

Platform Events include a replayId that identifies each event's position in the event bus. Store the highest processed replayId in your database. On subscriber restart, pass the stored replayId to resume from where you left off rather than replaying all events from the beginning. Implement idempotency in your event processor using the combination of objectId, changeType, and replayId as the deduplication key.

Salesforce retains Platform Events for 72 hours. If your subscriber is offline for more than 72 hours, events are permanently lost. Design your integration with a fallback polling mechanism for critical data types: run a SOQL query against SystemModstamp on recovery to catch any events missed during an extended outage.

Automation Workflows

Workflow 1: Website Lead to Salesforce and Sales Assignment

  1. 1Prospect submits demo request form on your marketing website
  2. 2Frontend POSTs to your backend /api/crm/leads
  3. 3Backend upserts the Salesforce Lead with FirstName, LastName, Email, Company, LeadSource=Website, and custom product interest fields
  4. 4Salesforce Flow triggers on Lead creation and assigns the lead to the correct sales rep based on company size and territory rules
  5. 5The assigned rep receives a Salesforce task notification and a Slack message via Salesforce-to-Slack integration
  6. 6If the rep does not contact the lead within 48 hours, a Salesforce Flow escalates the task to their manager

Workflow 2: Trial Signup to Opportunity and Follow-up

  1. 1User completes trial signup in your product
  2. 2Your backend calls Salesforce to convert the matching Lead (by email) to a Contact + Account + Opportunity, or creates new records if no Lead exists
  3. 3The Opportunity is created in the Trial Qualification stage with a CloseDate 30 days out and Amount set to the target ARR
  4. 4A Salesforce Email Alert sends a personalized trial confirmation to the new contact from the assigned sales rep's address
  5. 5When the user activates a key product feature, your backend updates the Opportunity stage to Evaluation and logs a Salesforce Activity with the activation detail
  6. 6At trial day 7 and day 14, product usage data (active days, features used) is synced to Salesforce as custom Opportunity fields via your scheduled sync job

Workflow 3: Customer Upgrade to Opportunity Update and Forecasting

events/customer-upgraded.ts
import { getSalesforceClient } from "../lib/salesforce";

export async function handleCustomerUpgrade(
  userId: string,
  newPlan: string,
  mrr: number
) {
  const user = await db.users.findUnique({ where: { id: userId } });
  const sf = await getSalesforceClient();

  // Find existing opportunity
  const opps = await sf.query<{ Id: string; StageName: string }>(
    `SELECT Id, StageName FROM Opportunity WHERE Contact_Email__c = '${user.email}' AND IsClosed = false LIMIT 1`
  );

  const annualValue = mrr * 12;

  if (opps.records.length > 0) {
    // Update existing opportunity to Closed Won
    await sf.sobject("Opportunity").update({
      Id: opps.records[0].Id,
      StageName: "Closed Won",
      Amount: annualValue,
      CloseDate: new Date().toISOString().split("T")[0],
      Plan__c: newPlan,
    });
  } else {
    // Create new closed-won opportunity
    const contact = await sf.query<{ AccountId: string; Id: string }>(
      `SELECT Id, AccountId FROM Contact WHERE Email = '${user.email}' LIMIT 1`
    );
    if (contact.records.length > 0) {
      await sf.sobject("Opportunity").create({
        Name: `${user.company} - ${newPlan} Upgrade`,
        AccountId: contact.records[0].AccountId,
        StageName: "Closed Won",
        CloseDate: new Date().toISOString().split("T")[0],
        Amount: annualValue,
        Plan__c: newPlan,
      });
    }
  }
}

Workflow 4: Support Ticket to Salesforce Case and Escalation

When a customer submits a support ticket in your product, create a corresponding Salesforce Case linked to the customer's Account and Contact. This gives your sales and customer success teams visibility into open support issues when reviewing account health in Salesforce, without requiring them to switch to a separate support tool.

events/support-ticket.ts
import { getSalesforceClient } from "../lib/salesforce";

export async function syncTicketToSalesforce(ticket: {
  customerEmail: string;
  subject: string;
  description: string;
  priority: "Low" | "Medium" | "High";
}) {
  const sf = await getSalesforceClient();
  const contacts = await sf.query<{ Id: string; AccountId: string }>(
    `SELECT Id, AccountId FROM Contact WHERE Email = '${ticket.customerEmail}' LIMIT 1`
  );
  if (!contacts.records.length) return;

  const { Id: contactId, AccountId } = contacts.records[0];
  return sf.sobject("Case").create({
    Subject: ticket.subject,
    Description: ticket.description,
    Priority: ticket.priority,
    Status: "New",
    Origin: "Product",
    ContactId: contactId,
    AccountId,
  });
}

Workflow 5: ERP Order to Salesforce and Customer Record Update

Enterprise customers frequently run Salesforce alongside an ERP such as SAP, Oracle, or NetSuite. When an order is completed in the ERP, your integration layer receives the event, creates or updates the corresponding Salesforce Opportunity to Closed Won, updates Account financial fields (total contract value, renewal date), and creates a Salesforce Activity logging the order reference number. This keeps the CRM accurate without requiring sales to manually update records when orders close in a separate system.

Production Deployment

API Limits

Salesforce API limits are per-org on a 24-hour rolling basis, not per-second. Enterprise Edition orgs receive 150,000 API calls per day by default; Unlimited Edition receives 1,000,000. Professional Edition is limited to 15,000. Monitor your daily API usage via the Salesforce API Usage dashboard or the limits endpoint and alert when consumption exceeds 70% of the daily allocation. Use batch endpoints and SOQL queries that return only required fields to minimize call count.

lib/sf-limits.ts
export async function checkAPILimits(): Promise<{ remaining: number; max: number }> {
  const sf = await getSalesforceClient();
  const limits = await sf.limits();
  const apiUsage = limits["DailyApiRequests"];
  return {
    remaining: apiUsage.Remaining,
    max: apiUsage.Max,
  };
}

Bulk API for Large Operations

Use Salesforce Bulk API v2 for any operation involving more than 200 records. A single Bulk API v2 job can process up to 100 million records. Bulk jobs do not count against the per-request API limit they use the daily bulk API limit instead, which is typically much larger. Use bulk for initial data migrations, nightly reconciliation jobs, and large-scale contact or account updates.

Caching and Performance

  • Cache Salesforce record ID lookups (email-to-Contact-ID, domain-to-Account-ID) in Redis with a 15-minute TTL to avoid repeated SOQL queries
  • Cache org-level metadata (field definitions, picklist values, pipeline stage names) with a 1-hour TTL these rarely change
  • Use SOQL projection to select only the fields you need; a SELECT * equivalent in Salesforce is expensive and increases response size unnecessarily
  • Paginate large SOQL result sets using the nextRecordsUrl in the query response; do not use OFFSET for large data sets as it degrades performance significantly

Monitoring and Observability

  • Track Salesforce API call count, error rate, and P95 latency as metrics in your monitoring stack
  • Monitor daily API consumption against org limits alert at 70%, alert critically at 90%
  • Track Platform Event subscriber lag and reconnection frequency
  • Monitor bulk job completion rates and error counts
  • Set up dead-letter queue alerts for failed Platform Event processing
  • Log every outbound Salesforce API call with the endpoint, SOQL query, response status, and duration

Common Challenges

Challenge 1: Duplicate Records

Salesforce has native duplicate management rules, but they do not catch all cases, especially when records are created via API. Solution: implement external-ID-based upserts using Email for Contacts and Leads rather than separate create calls. Define a custom External ID field on your objects (e.g., External_System_ID__c) and use it as the upsert key. This guarantees idempotency on every write regardless of whether the record already exists in Salesforce.

Challenge 2: API Limits

Exceeding daily API limits causes all API calls to return 503 errors until the 24-hour window resets. Prevention is the only viable strategy: use SOQL queries with projections, use batch endpoints, cache lookups aggressively, use Bulk API for any operation above 200 records, and implement circuit breakers that stop new API calls when remaining capacity drops below a threshold. Never design an integration that makes one API call per user action without an aggregate or batch layer.

Challenge 3: Sync Conflicts

When both your product and Salesforce users can update the same fields, conflicts occur. The last write wins, which means manual Salesforce updates can be overwritten by your sync job minutes later. Solution: partition ownership at the field level. Define which fields are owned by your product (plan, usage metrics, activation status) and which are owned by Salesforce users (deal stage, notes, account owner). Your sync job only writes product-owned fields. Sales-owned fields are synchronized back via Change Data Capture.

Challenge 4: Field Mapping Issues

Salesforce has strict data type validation. Sending a string to a currency field, a value not in a picklist's allowed values, or a date in the wrong format returns an error. Solution: build a typed field mapping layer that validates and transforms data before sending it to Salesforce. Test mapping against a developer sandbox with production-like data to catch type mismatches early. Log every 400 validation error with the full payload so field mapping problems surface immediately.

Challenge 5: Authentication Failures

OAuth access tokens expire and refresh tokens can be revoked by a Salesforce admin. An integration that does not handle token expiration fails silently until someone notices stale CRM data. Solution: use jsforce's built-in refresh event handler to persist new access tokens automatically. For JWT-based integrations, implement proactive token refresh when remaining TTL drops below 60 seconds. Add monitoring that alerts when authentication errors occur they require immediate attention because they block all CRM synchronization.

Challenge 6: Large Data Migrations

Migrating millions of records from an existing system into Salesforce via the REST API exhausts API limits before the migration completes. Solution: always use Bulk API v2 for migrations. Split the migration into object-type batches, process each batch in a background job with progress tracking, implement checkpointing so a failed job resumes from the last successful batch rather than restarting, and run the migration during off-peak hours to avoid competing with production API traffic.

Challenge 7: Performance Bottlenecks

Complex SOQL queries against large orgs with millions of records can take seconds to return. SOQL with OFFSET on large result sets is particularly slow. Solution: use indexed fields in SOQL WHERE clauses (Email, External_System_ID__c, and standard Salesforce indexed fields). Avoid OFFSET for pagination use the nextRecordsUrl cursor pattern instead. Cache the results of expensive queries in Redis. For reports that aggregate large amounts of data, use Salesforce Reports and Dashboards APIs rather than raw SOQL.

Best Practices

  1. 1Use Platform Events for all Salesforce-initiated event flows they are more reliable, replayable, and scalable than Outbound Messages
  2. 2Use external ID fields (e.g., External_System_ID__c) for all upsert operations to guarantee idempotency
  3. 3Isolate all Salesforce API calls in a dedicated service class; never scatter jsforce or HTTP calls across business logic
  4. 4Use the JWT Bearer Token flow for server-to-server integrations; avoid username-password flow in production
  5. 5Store Salesforce credentials and private keys in a secrets manager, never in environment variable files
  6. 6Implement proactive token refresh rather than reactive refresh on 401 errors
  7. 7Use SOQL projections specify only the fields you need in SELECT; never select unnecessary fields
  8. 8Use the Bulk API v2 for any operation involving more than 200 records
  9. 9Monitor daily API consumption and alert at 70% to prevent hitting limits during business hours
  10. 10Cache Salesforce record ID lookups in Redis with a short TTL to reduce SOQL query volume
  11. 11Implement exponential backoff for all transient Salesforce API errors (429, 503, connection timeouts)
  12. 12Partition data ownership at the field level to prevent sync conflicts between your product and Salesforce users
  13. 13Subscribe to Change Data Capture rather than polling for Salesforce-initiated record changes
  14. 14Store the Platform Event replayId and resume from it on subscriber restart to avoid missing events during downtime
  15. 15Run a daily SOQL reconciliation job against SystemModstamp for high-value object types as a consistency guarantee independent of event delivery

Need help designing and building a Salesforce integration, implementing enterprise automation, synchronizing Salesforce with ERP and custom software systems, or modernizing a legacy CRM architecture? Nurture Technologies builds enterprise-grade Salesforce integrations that are reliable, observable, and built to scale. We design the architecture, implement the integration layer, set up monitoring, and ensure your CRM data is accurate and synchronized in real time. Talk to us about your Salesforce integration.

Nurture Technologies

NEED HELP WITH YOUR STACK?

Nurture Technologies builds and maintains production-quality software for startups and businesses. If engineering problems are slowing you down, our team can help.

Talk to our team →
FAQ

FREQUENTLY ASKED QUESTIONS

What is Salesforce integration?+

Salesforce integration is the process of connecting Salesforce CRM to other systems your product, ERP, marketing platform, support desk, or data warehouse so that data flows automatically between them without manual data entry. A Salesforce integration typically involves creating or updating CRM records (Accounts, Contacts, Leads, Opportunities) in response to product events, and receiving CRM updates (via Platform Events or Change Data Capture) so your application state stays synchronized with Salesforce.

How does the Salesforce API work?+

The Salesforce REST API is a versioned HTTP API accessible at {your-instance}.salesforce.com/services/data/v59.0/. It supports CRUD operations on all Salesforce objects (standard and custom) via /sobjects/{ObjectType}/, SOQL queries via /query/?q={soql}, composite requests that batch multiple operations, and metadata operations for schema inspection. All requests require a valid OAuth access token in the Authorization header. Salesforce also provides a Bulk API v2 for high-volume operations and a Streaming API for real-time event delivery.

What are Salesforce Platform Events?+

Platform Events are Salesforce's publish-subscribe messaging system built on the CometD streaming protocol. You define a custom Platform Event object (with __e suffix) in Salesforce, and any Apex code, Flow, or external system can publish events to it. Subscribers connect via CometD (or jsforce's streaming client in Node.js) and receive events in real time. Events are retained for 72 hours, support replay from a specific position (replayId), and support fan-out to multiple subscribers. They are the recommended mechanism for real-time event-driven integration.

What are Salesforce API limits?+

Salesforce API limits are per-org on a 24-hour rolling basis. Enterprise Edition: 150,000 calls/day; Unlimited Edition: 1,000,000 calls/day; Professional Edition: 15,000 calls/day. Bulk API v2 operations count against a separate bulk daily limit. Concurrent API call limits cap long-running requests at 25 simultaneous per org. Exceeding limits returns 503 errors until the 24-hour window resets. Check your remaining limit via the /limits REST endpoint. Monitor and alert at 70% consumption.

How do I integrate Salesforce with a custom software product?+

The standard pattern: your backend service holds Salesforce OAuth tokens and makes all API calls. Product events (sign-up, upgrade, support ticket) trigger background jobs that create or update Salesforce records via the REST API. Salesforce-initiated changes are received via Platform Events or Change Data Capture, queued in a message broker, and processed asynchronously by a worker. Your frontend communicates only with your backend API and never touches Salesforce directly.

What is the best Salesforce SDK for Node.js?+

jsforce is the most complete Salesforce client for Node.js. It provides typed methods for all Salesforce objects, SOQL query execution, OAuth flows, Bulk API support, and Platform Event streaming via CometD all in a single package. Install with npm install jsforce. For TypeScript projects, type definitions are included. Alternatively, use the Salesforce REST API directly with fetch or axios for simpler use cases that do not need the full SDK.

What is the difference between Salesforce OAuth Web Server flow and JWT Bearer flow?+

The Web Server flow requires a user to authorize your application interactively via a browser redirect. Use it when your integration acts on behalf of a specific Salesforce user who needs to grant access. The JWT Bearer Token flow authenticates a server-side application without user interaction by signing a JWT assertion with a private key registered in your Connected App. Use JWT for server-to-server integrations, background jobs, and API services where no user authorization step is possible or appropriate.

What is Salesforce Change Data Capture?+

Change Data Capture (CDC) is a Salesforce feature that automatically publishes Platform Events for every create, update, delete, and undelete operation on selected Salesforce objects. Enable CDC for specific objects in Salesforce Setup, and subscribers on the /data/{ObjectType}ChangeEvent channel receive real-time change events with the changed field values, the type of change (CREATE, UPDATE, DELETE), and the affected record IDs. CDC is the correct way to synchronize Salesforce-initiated record changes to your application without polling.

How do I prevent duplicate records in Salesforce?+

Use external-ID-based upsert operations instead of separate check-then-create logic. Create a custom External ID field (e.g., External_System_ID__c or use Email as the external ID for Contacts and Leads) and use the upsert endpoint with that field as the matching key. A single upsert call creates the record if it does not exist and updates it if it does, atomically, without a race condition. Configure Salesforce duplicate management rules as a secondary safeguard for manual data entry.

How does Salesforce Bulk API v2 work?+

Bulk API v2 processes large volumes of records asynchronously in a job-based model. Create a job specifying the object type and operation (insert, update, upsert, delete, hardDelete). Upload a CSV file with the records to process. Salesforce processes the job in parallel and returns a results CSV when complete. A single job can handle up to 100 million records. Bulk jobs count against the daily bulk API limit (separate from the REST API daily limit), which is much larger. Always use Bulk API for migrations and any operation above 200 records.

How do I handle Salesforce API errors and retries?+

For 401 errors: refresh the OAuth access token and retry once. For 429 (rate limited) and 503 (limit exceeded) errors: implement exponential backoff with jitter, starting at 5 seconds and doubling up to a 60-second cap. For 5xx transient errors: retry up to 5 times with exponential backoff. For 400 validation errors: log the full request payload and error detail these require a code fix, not a retry. Use circuit breakers to stop sending requests to Salesforce when the error rate exceeds a threshold, giving the API time to recover.

How do I structure a Salesforce integration service in NestJS?+

Create a SalesforceModule with a SalesforceService that initializes the jsforce connection in OnModuleInit. Declare the module as @Global so any feature module can inject SalesforceService without re-importing the module. Add methods for each Salesforce operation your application needs: createLead, updateOpportunityStage, subscribeToEvents, etc. Use ConfigService for credential injection and add a Logger for observability. Register the module in AppModule and inject SalesforceService wherever CRM access is required.

How do I keep Salesforce data consistent when both my product and sales reps can update records?+

Define a clear system-of-record boundary at the field level. Product-owned fields (plan tier, MRR, usage metrics, activation events, last login) are written exclusively by your integration and locked or read-only for Salesforce users. Sales-owned fields (opportunity stage, close date, account owner, notes, custom segments) are owned by Salesforce users and synchronized back to your database via CDC. Never let both sides freely write the same field without a conflict resolution strategy.

What is a Salesforce Connected App?+

A Connected App is a Salesforce OAuth 2.0 application registration inside a Salesforce org. It defines the application's name, OAuth callback URL, allowed scopes, and IP restrictions. Every external integration with Salesforce requires a Connected App. For JWT Bearer Token flow, you also upload an X.509 certificate to the Connected App so Salesforce can verify your JWT assertions. A Connected App's Consumer Key is the OAuth client_id; the Consumer Secret is the client_secret.

What are Salesforce Outbound Messages and when should I use them?+

Outbound Messages are a legacy Salesforce automation feature that sends SOAP XML messages to an external endpoint when triggered by a workflow rule. They are still functional but are not recommended for new integrations. Use Platform Events instead: they support replay, fan-out, are easier to consume in modern languages, and do not require you to parse SOAP. Outbound Messages require responding with a SOAP acknowledgment, which adds complexity without providing meaningful advantages over Platform Events.

How do I migrate large volumes of data from a legacy system to Salesforce?+

Use Salesforce Bulk API v2. Split the migration by object type and process in this order: Accounts and Contacts first (foundational records), then Leads, then Opportunities with AccountId references, then Cases, then Activities. Use external ID fields to establish relationships during migration without needing to look up Salesforce IDs. Run the migration in a developer sandbox first to validate field mapping and catch data quality issues. Implement progress checkpointing so a failed batch resumes from where it stopped rather than restarting the entire migration.

How do I test a Salesforce integration in development?+

Create a free Salesforce Developer Edition org at developer.salesforce.com. Developer Edition includes full API access and most platform features at no cost. Create a separate Connected App for your development environment with a development redirect URI and separate credentials from production. Use SOQL queries in Salesforce's Developer Console or Workbench to inspect records created by your integration. Never point development or staging environments at production Salesforce orgs.

How do I monitor a Salesforce integration in production?+

Track API consumption versus daily limit (alert at 70%), Salesforce API error rate (alert above 2%), Platform Event subscriber lag and reconnection frequency, bulk job success and failure rates, and sync latency (time from product event to Salesforce record update). Export these metrics to your observability stack. Log every outbound Salesforce API call with the operation, object type, record ID, response status, and duration. Check Salesforce's API Usage report in Setup weekly to identify unexpected API call patterns.

How do I handle Salesforce authentication token expiration?+

OAuth access tokens expire (typically within 2 hours for web server flow tokens). Register a jsforce refresh event listener that persists the new access token to your database whenever jsforce refreshes it automatically. For JWT Bearer Token flow, access tokens expire after the JWT exp claim (typically 5 minutes); request a new token before each API call if the current token's age approaches expiration. Add monitoring that alerts on authentication errors immediately they block all synchronization and require immediate investigation.

What is the recommended architecture for a high-volume Salesforce integration?+

For high-volume integrations: use a message queue (BullMQ, SQS, RabbitMQ) to buffer all outbound Salesforce API calls so they are rate-limited and retried reliably. Use Bulk API v2 for any batch operation above 200 records. Shard the event worker pool so Platform Event processing scales independently of REST API calls. Cache Salesforce record ID lookups in Redis to reduce SOQL query volume. Implement a circuit breaker that stops queuing new Salesforce calls when the error rate is high. Monitor queue depth and worker throughput as primary integration health signals.