---
# === IDENTITY ===
id: business/erp-integration/salesforce-rest-api-capabilities/2026
canonical_question: "What are the Salesforce REST API capabilities, rate limits, and governor limits by edition?"
aliases:
  - "Salesforce API rate limits by edition"
  - "Salesforce REST API governor limits"
  - "Salesforce API call limits Enterprise vs Unlimited"
  - "Salesforce API request limits and quotas"
entity_type: erp_integration
domain: business > erp-integration > salesforce-rest-api-capabilities
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Salesforce"
    vendor: "Salesforce"
    version: "API v66.0 (Spring '26)"
    edition: "Professional, Enterprise, Unlimited, Performance, Developer"
    deployment: cloud
    api_surface: "REST, SOAP, Bulk API 2.0, Streaming, Composite, Connect"

# === VERIFICATION ===
last_verified: 2026-03-02
confidence: 0.92
version: 1.0
first_published: 2026-03-02

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "v66.0 (Spring '26) — legacy hostname redirections removed, connected app creation disabled by default"
  next_review: 2026-08-29
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Daily API call limit: 100,000 base + per-user additions (1,000/user Enterprise; 5,000/user Unlimited) — shared across REST, SOAP, Bulk, and Connect APIs"
  - "Governor limits are per-transaction, not per-API-call — triggers cascade and consume from the same pool (100 SOQL queries, 150 DML statements, 10,000 DML rows per transaction)"
  - "Bulk API 2.0: 150 MB per job upload, 100 million records per 24h processing limit, max 25 concurrent jobs"
  - "Developer Edition: only 15,000 API calls/24h and 5 concurrent requests — do not use for load testing"
  - "Professional Edition: API access requires add-on purchase; not included by default"
  - "Platform Events + CDC share a combined 50,000 daily event delivery allocation (standard); add-on license required for high volume"
  - "OAuth refresh tokens expire after 90 days of non-use — unattended integrations must implement token keep-alive"
  - "API versions 21.0-30.0 retired June 2025; minimum supported version is now v31.0"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs to move more than 150 MB of data per job or process millions of records"
    use_instead: "business/erp-integration/salesforce-bulk-api/2026"
  - condition: "User needs real-time event-driven integration (CDC, Platform Events)"
    use_instead: "business/erp-integration/salesforce-platform-events/2026"
  - condition: "User needs Salesforce SOAP API specifically for metadata operations"
    use_instead: "business/erp-integration/salesforce-soap-api/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to Salesforce)"
      - "outbound (reading from Salesforce)"
      - "bidirectional sync"
  - key: edition
    question: "Which Salesforce edition?"
    type: choice
    options:
      - "Professional"
      - "Enterprise"
      - "Unlimited"
      - "Performance"
      - "Developer"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/salesforce-rest-api-capabilities/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-02)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to: []
  solves: []
  alternative_to: []
  often_confused_with: []

# === SOURCES ===
sources:
  - id: src1
    title: "API Request Limits and Allocations — Salesforce Developer Limits and Allocations Quick Reference"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_api.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src2
    title: "REST API Developer Guide — Version 66.0, Spring '26"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src3
    title: "Execution Governors and Limits — Apex Developer Guide"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src4
    title: "API Limits and Monitoring Your API Usage"
    author: Salesforce
    url: https://developer.salesforce.com/blogs/2024/11/api-limits-and-monitoring-your-api-usage
    type: technical_blog
    published: 2024-11-01
    reliability: high
  - id: src5
    title: "Bulk API 2.0 and Bulk API Limits and Allocations"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_bulkapi.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src6
    title: "Salesforce Spring '26 Release API Version 66.0 Updates"
    author: conemis
    url: https://www.conemis.com/news/salesforce-spring-26-release-api-version-66-0
    type: technical_blog
    published: 2026-01-15
    reliability: moderate_high
  - id: src7
    title: "Salesforce API Rate Limits and How to Prevent Hitting Them"
    author: Coefficient
    url: https://coefficient.io/salesforce-api/salesforce-api-rate-limits
    type: community_resource
    published: 2025-06-01
    reliability: moderate_high
---

# Salesforce REST API Capabilities, Rate Limits & Governor Limits by Edition

## TL;DR

- **Bottom line**: Salesforce REST API (v66.0, Spring '26) handles real-time CRUD for individual records and small batches (<2,000 records); switch to Bulk API 2.0 for anything larger. Daily API call limits vary dramatically by edition.
- **Key limit**: 100,000 base API calls/24h + per-user additions (Enterprise: +1,000/user; Unlimited: +5,000/user). This pool is shared across REST, SOAP, Bulk, and Connect APIs. [src1]
- **Watch out for**: Governor limits are per-transaction, not per-API-call — a single API request can trigger Apex code that cascades through 100 SOQL queries, 150 DML statements, and 10,000 DML rows before hitting the wall. [src3]
- **Best for**: Real-time CRM operations under 2,000 records per request — account sync, opportunity management, custom object CRUD, and composite multi-object operations.
- **Authentication**: OAuth 2.0 JWT bearer flow for server-to-server (recommended); web server flow for user-context; username-password flow deprecated for production. [src2]

## System Profile

This card covers the Salesforce REST API across all major commercial editions (Professional, Enterprise, Unlimited, Performance) and the free Developer Edition. Rate limits and governor limits differ significantly by edition — always check which edition your org runs before designing an integration. This card does not cover Salesforce Marketing Cloud (which has its own separate API) or MuleSoft Anypoint (a separate product).

| Property | Value |
|---|---|
| **Vendor** | Salesforce |
| **System** | Salesforce CRM / Platform |
| **API Surface** | REST (primary), plus SOAP, Bulk 2.0, Streaming, Composite, Connect |
| **Current API Version** | v66.0 (Spring '26, released February 2026) |
| **Editions Covered** | Professional (with API add-on), Enterprise, Unlimited, Performance, Developer |
| **Deployment** | Cloud (multi-tenant SaaS) |
| **API Docs URL** | [REST API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest) |
| **Status** | GA (General Availability) |

## API Surfaces & Capabilities

Salesforce exposes multiple API surfaces for different use cases. Choose based on data volume, latency requirements, and direction. [src1, src2]

| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| REST API | HTTPS/JSON | Individual record CRUD, queries <2K records | 2,000 (SOQL query), 200 (composite) | Shared daily pool (see below) | Yes | No |
| Bulk API 2.0 | HTTPS/CSV | ETL, data migration, >2K records | 150 MB per job file | 100M records/24h; 25 concurrent jobs | No | Yes |
| SOAP API | HTTPS/XML | Metadata operations, legacy integrations | 2,000 records | Shared daily pool with REST | Yes | No |
| Composite API | HTTPS/JSON | Multi-object operations in one call | 25 subrequests | Shared daily pool (counts as 1 API call) | Yes | No |
| Streaming API | Bayeux/CometD | Real-time push notifications | N/A | 50K-1M events/day (edition-dependent) | Yes | N/A |
| Platform Events | HTTPS/JSON | Event-driven architecture, pub/sub | N/A | 250K publishes/hour; 50K deliveries/day (standard) | Yes | N/A |
| Change Data Capture | Bayeux/CometD | Track record changes in real-time | N/A | Shared with Platform Events allocation | Yes | N/A |
| Connect API | HTTPS/JSON | Chatter, files, communities, UI-specific | Varies | Separate per-user-per-app-per-hour limit | Yes | No |

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per SOQL query page | 2,000 | REST API | Use `queryMore` or `nextRecordsUrl` for pagination [src2] |
| Max SOQL query rows per transaction | 50,000 | All APIs triggering Apex | Aggregate across all queries in one transaction [src3] |
| Max request body size | 50 MB | REST API | For single record operations |
| Max composite subrequests | 25 | Composite API | All-or-nothing by default; `allOrNone` flag controls rollback [src2] |
| Max SObject Collection records | 200 | SObject Collections | For create/update/delete in a single request [src2] |
| Max Bulk API 2.0 job file size | 150 MB | Bulk API 2.0 | Base64 encoding adds ~50% — limit to 100 MB pre-encoding [src5] |
| API call timeout | 600,000 ms (10 min) | REST and SOAP API | Except query calls which have no timeout |
| Max fields in SOQL SELECT | 200 | REST API queries | Use multiple queries for wide objects |

[src1, src2]

### Rolling / Daily Limits

| Limit Type | Base Value | Window | Edition Differences |
|---|---|---|---|
| Total API calls | 100,000 | 24h rolling | Professional: 100K + 1,000/user; Enterprise: 100K + 1,000/user; Unlimited/Performance: 100K + 5,000/user; Developer: 15,000 total [src1] |
| Bulk API 2.0 records processed | 100,000,000 | 24h | Shared across all editions [src5] |
| Bulk API batches | 15,000 | 24h | Shared between Bulk API 1.0 and 2.0 [src7] |
| Concurrent long-running API requests | 25 | Per org | Developer: 5 concurrent; requests >20s count as long-running [src1] |
| Streaming API events | 50K-1M | 24h | Enterprise: 200K; Unlimited/Performance: 1M [src1] |
| Platform Events + CDC deliveries | 50,000 | 24h (standard) | Add-on license: 100K extra, enforced monthly (4.5M/month) [src1] |
| Platform Events publishes | 250,000 | Per hour | Rarely a bottleneck [src1] |
| Sandbox API calls | 5,000,000 | 24h | Fixed, independent of edition [src4] |

[src1, src4]

### Transaction / Governor Limits

These limits apply per Apex transaction — a single API request can trigger Apex code (triggers, flows, process builders) that must stay within these boundaries. This is the #1 area where agents hallucinate. [src3]

| Limit Type | Synchronous Value | Asynchronous Value | Notes |
|---|---|---|---|
| SOQL queries | 100 | 200 | Includes queries from triggers — cascading triggers consume from same pool [src3] |
| Total SOQL query rows returned | 50,000 | 50,000 | Aggregate across all queries in the transaction [src3] |
| DML statements | 150 | 150 | Each insert/update/delete/upsert counts as 1, regardless of record count [src3] |
| DML rows | 10,000 | 10,000 | Total records across all DML operations in the transaction [src3] |
| CPU time | 10,000 ms | 60,000 ms | Exceeded = transaction abort with `System.LimitException` [src3] |
| Heap size | 6 MB | 12 MB | Watch for large query results stored in memory [src3] |
| Callouts (HTTP requests) | 100 | 100 | External service calls within a transaction [src3] |
| Callout timeout | 120,000 ms total | 120,000 ms total | Per-callout max: 120s; total across all callouts: 120s [src3] |
| Future method invocations | 50 | 0 (not allowed) | Cannot call @future from @future [src3] |
| Queueable jobs queued | 50 | 50 | Per transaction [src3] |
| Email invocations | 10 | 10 | SingleEmailMessage sends per transaction [src3] |
| SOSL searches | 20 | 20 | Per transaction [src3] |

## Authentication

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 JWT Bearer | Server-to-server, no user context needed | Session timeout (default 2h, admin-configurable) | No — issue new JWT per request | Recommended for integrations; requires connected app + X.509 certificate [src2] |
| OAuth 2.0 Web Server | User-context operations, interactive apps | Access: 2h; Refresh: until revoked or 90 days of non-use | Yes | Requires callback URL; standard for user-facing apps [src2] |
| OAuth 2.0 Client Credentials | Server-to-server (Spring '23+), simpler than JWT | Session timeout | No refresh token | Requires connected app; no user context [src2] |
| Username-Password | Testing only, legacy | Session timeout | No | Do NOT use in production — no MFA support, credential exposure risk [src2] |

### Authentication Gotchas

- JWT bearer flow requires a connected app with digital certificate — self-signed certificates work for development, but CA-signed certificates are required for production. The certificate must be uploaded to the connected app in Setup. [src2]
- Access tokens from JWT flow are org-scoped — all API calls execute with the integration user's permissions and profile. The integration user must have "API Enabled" permission and appropriate object/field-level security. [src2]
- Session timeout is admin-configurable (15 min to 24h) — never hardcode 2 hours. Check the session settings in Setup > Session Settings. [src2]
- OAuth refresh tokens expire after 90 days of non-use — unattended integrations must implement a token refresh keep-alive mechanism or use JWT bearer flow instead. [src4]
- Spring '26 change: Connected app creation is now disabled by default. Admins must explicitly enable it, or use the new External Client App feature. [src6]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **Professional Edition requires API add-on**: API access is not included in Professional Edition by default. The add-on must be purchased separately. Do not recommend API integrations for Professional Edition without confirming the add-on is active. [src1]
- **Daily API pool is shared**: REST, SOAP, Bulk, and Connect API calls all draw from the same daily pool. A heavy Bulk API job can starve real-time REST integrations of their quota. [src1]
- **Governor limits cascade through triggers**: A single REST API upsert can fire before/after triggers, which fire workflow rules, which fire more triggers — all sharing the same 100-SOQL, 150-DML, 10,000-row transaction budget. [src3]
- **Composite API subrequests count as 1 API call**: The 25-subrequest composite call consumes only 1 from your daily API quota, but still subject to governor limits per transaction. [src2]
- **No API for formula field writes**: Formula fields, rollup summary fields, and system fields (CreatedDate, LastModifiedDate) cannot be written via API. Agents must not suggest writing to these fields. [src2]
- **Sandbox limits differ from production**: Sandbox orgs have a fixed 5M API calls/24h regardless of edition, but may have lower storage limits and incomplete data. [src4]
- **API version minimum is v31.0**: Versions 21.0-30.0 were retired in June 2025. Any integration pinned below v31.0 will fail. [src6]
- **Platform Events require careful capacity planning**: The standard allocation of 50,000 daily event deliveries is shared between Platform Events and CDC. High-volume event-driven integrations require the add-on license. [src1]

## Integration Pattern Decision Tree

```
START — User needs to integrate with Salesforce REST API
|-- What's the integration pattern?
|   |-- Real-time (individual records, <1s latency)
|   |   |-- Data volume < 200 records/operation?
|   |   |   |-- YES --> REST API: use SObject Collections for multi-record, Composite for multi-object
|   |   |   +-- NO --> REST API with chunking (200-record batches) + async processing
|   |   +-- Need notifications/webhooks?
|   |       |-- YES --> Platform Events or Change Data Capture
|   |       +-- NO --> REST API polling with SystemModstamp filter (not recommended >1K records)
|   |-- Batch/Bulk (scheduled, high volume)
|   |   |-- Data volume < 2,000 records?
|   |   |   |-- YES --> REST API SObject Collections (simpler, no batch overhead)
|   |   |   +-- NO (next question)
|   |   |-- Data volume < 100,000 records?
|   |   |   |-- YES --> Bulk API 2.0 (single job)
|   |   |   +-- NO --> Bulk API 2.0 with job chunking (150 MB per job)
|   |   +-- Need real-time progress tracking?
|   |       |-- YES --> Bulk API 2.0 (poll job status endpoint)
|   |       +-- NO --> Bulk API 2.0 with webhook notification on completion
|   |-- Event-driven (CDC, real-time change tracking)
|   |   |-- Need guaranteed delivery?
|   |   |   |-- YES --> Platform Events with replay (72h retention)
|   |   |   +-- NO --> Streaming API / PushTopics
|   |   +-- Need cross-object change tracking?
|   |       |-- YES --> Change Data Capture (tracks all field changes)
|   |       +-- NO --> Object-specific triggers + Platform Events
|   +-- File-based (CSV import)
|       +-- Use Bulk API 2.0 with CSV upload (Salesforce has no native file-based import API)
|-- Which direction?
|   |-- Inbound (writing to Salesforce) --> check daily API limits + governor limits for triggers
|   |-- Outbound (reading from Salesforce) --> check query row limits (50K/transaction)
|   +-- Bidirectional --> design conflict resolution strategy FIRST (external ID + last-modified wins)
+-- Error tolerance?
    |-- Zero-loss required --> implement idempotency (external ID upsert) + dead letter queue
    +-- Best-effort acceptable --> fire-and-forget with exponential backoff retry
```

## Quick Reference

### Key REST API Endpoints

| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Create record | POST | `/services/data/v66.0/sobjects/{Object}` | JSON | Returns record ID on 201 success [src2] |
| Read record | GET | `/services/data/v66.0/sobjects/{Object}/{id}` | N/A | Specify fields with `?fields=` param [src2] |
| Update record | PATCH | `/services/data/v66.0/sobjects/{Object}/{id}` | JSON | Partial update — only send changed fields [src2] |
| Delete record | DELETE | `/services/data/v66.0/sobjects/{Object}/{id}` | N/A | Returns 204 No Content on success [src2] |
| Upsert by external ID | PATCH | `/services/data/v66.0/sobjects/{Object}/{ExtIdField}/{ExtIdValue}` | JSON | Creates or updates — idempotent [src2] |
| Query (SOQL) | GET | `/services/data/v66.0/query?q={SOQL}` | N/A | Max 2,000 records per page; use `nextRecordsUrl` [src2] |
| Query more | GET | `/services/data/v66.0/query/{queryLocator}` | N/A | Continue paginating through results [src2] |
| Composite | POST | `/services/data/v66.0/composite` | JSON | Up to 25 subrequests; reference previous results [src2] |
| SObject Collections | POST | `/services/data/v66.0/composite/sobjects` | JSON array | Up to 200 records per call; partial success supported [src2] |
| Describe object | GET | `/services/data/v66.0/sobjects/{Object}/describe` | N/A | Returns full object metadata and field definitions [src2] |
| Check limits | GET | `/services/data/v66.0/limits` | N/A | Returns remaining allocations for all limits [src2] |
| Search (SOSL) | GET | `/services/data/v66.0/search?q={SOSL}` | N/A | Full-text search across multiple objects [src2] |

## Step-by-Step Integration Guide

### 1. Authenticate and obtain access token

Use the JWT bearer flow for server-to-server integrations. Create a connected app in Salesforce Setup, upload your X.509 certificate, and configure the pre-authorized profiles or permission sets. [src2]

```bash
# Generate JWT and exchange for access token
# JWT payload: iss=<consumer_key>, sub=<username>, aud=https://login.salesforce.com, exp=<now+300>

curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=<your_signed_jwt>"
```

**Verify**: Response contains `access_token` and `instance_url` --> expected: `{"access_token": "00D...", "instance_url": "https://yourorg.my.salesforce.com", ...}`

### 2. Query records with SOQL

Use the query endpoint for structured reads. Always paginate — the default page size is 2,000 records. [src2]

```bash
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/query?q=SELECT+Id,Name,Industry+FROM+Account+WHERE+LastModifiedDate+>+2026-01-01T00:00:00Z+LIMIT+100"
```

**Verify**: Response includes `totalSize`, `done`, and `records` array --> expected: `{"totalSize": N, "done": true/false, "records": [...]}`

### 3. Create or update records with upsert

Use external ID upsert for idempotent writes — this prevents duplicates on retry. [src2]

```bash
curl -X PATCH \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  "$INSTANCE_URL/services/data/v66.0/sobjects/Account/External_ID__c/EXT-001" \
  -d '{"Name": "Acme Corp", "Industry": "Technology", "Website": "https://acme.com"}'
```

**Verify**: 201 (created) or 204 (updated) --> expected: `{"id": "001...", "success": true}` on create; empty body on update

### 4. Handle pagination for large result sets

When `done` is `false` in the query response, use `nextRecordsUrl` to fetch the next page. [src2]

```python
import requests

def query_all(instance_url, access_token, soql):
    """Fetch all records from a SOQL query, handling pagination."""
    headers = {"Authorization": f"Bearer {access_token}"}
    url = f"{instance_url}/services/data/v66.0/query"
    params = {"q": soql}
    all_records = []

    response = requests.get(url, headers=headers, params=params)
    response.raise_for_status()
    data = response.json()
    all_records.extend(data["records"])

    while not data["done"]:
        next_url = f"{instance_url}{data['nextRecordsUrl']}"
        response = requests.get(next_url, headers=headers)
        response.raise_for_status()
        data = response.json()
        all_records.extend(data["records"])

    return all_records
```

**Verify**: `len(all_records) == totalSize` from first response

### 5. Implement error handling and retry logic

Handle 429 (rate limit), 503 (service unavailable), and INVALID_SESSION_ID (expired token) with exponential backoff. [src4]

```python
import time
import requests

def sf_api_call(method, url, headers, json=None, max_retries=5):
    """Make Salesforce API call with exponential backoff retry."""
    for attempt in range(max_retries):
        response = requests.request(method, url, headers=headers, json=json)

        if response.status_code == 429:  # Rate limited
            wait = min(2 ** attempt * 2, 60)
            time.sleep(wait)
            continue
        elif response.status_code == 503:  # Service unavailable
            wait = min(2 ** attempt * 5, 120)
            time.sleep(wait)
            continue
        elif response.status_code == 401:  # Token expired
            headers["Authorization"] = f"Bearer {refresh_access_token()}"
            continue
        else:
            return response

    raise Exception(f"Max retries exceeded for {url}")
```

**Verify**: Function returns valid response for 200/201/204 status codes and retries on 429/503

## Code Examples

### Python: Query and upsert accounts with rate limit handling

```python
# Input:  Salesforce credentials (JWT or OAuth), list of account records
# Output: Upsert results with success/failure counts

import requests
import time
import jwt  # PyJWT>=2.8.0
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives import serialization  # cryptography>=42.0.0

class SalesforceClient:
    def __init__(self, consumer_key, username, private_key_path,
                 login_url="https://login.salesforce.com"):
        self.consumer_key = consumer_key
        self.username = username
        self.login_url = login_url
        with open(private_key_path, "rb") as f:
            self.private_key = serialization.load_pem_private_key(f.read(), password=None)
        self.access_token = None
        self.instance_url = None

    def authenticate(self):
        """Authenticate via JWT bearer flow."""
        now = datetime.now(timezone.utc)
        payload = {
            "iss": self.consumer_key,
            "sub": self.username,
            "aud": self.login_url,
            "exp": int((now + timedelta(minutes=5)).timestamp()),
        }
        token = jwt.encode(payload, self.private_key, algorithm="RS256")
        resp = requests.post(f"{self.login_url}/services/oauth2/token", data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "assertion": token,
        })
        resp.raise_for_status()
        data = resp.json()
        self.access_token = data["access_token"]
        self.instance_url = data["instance_url"]

    def _headers(self):
        return {"Authorization": f"Bearer {self.access_token}",
                "Content-Type": "application/json"}

    def upsert_collection(self, sobject, ext_id_field, records):
        """Upsert up to 200 records using SObject Collections."""
        # SObject Collections max: 200 records per call
        url = (f"{self.instance_url}/services/data/v66.0"
               f"/composite/sobjects/{sobject}/{ext_id_field}")
        resp = requests.patch(url, headers=self._headers(),
                              json={"allOrNone": False, "records": records})
        resp.raise_for_status()
        return resp.json()
```

### JavaScript/Node.js: Composite API multi-object operation

```javascript
// Input:  Salesforce access token, instance URL
// Output: Composite response with all subrequest results
// Requires: node-fetch@3.3.0 or built-in fetch (Node 18+)

async function compositeOperation(instanceUrl, accessToken) {
  const url = `${instanceUrl}/services/data/v66.0/composite`;
  const body = {
    allOrNone: true,  // Rollback all if any fails
    compositeRequest: [
      {
        method: "POST",
        url: "/services/data/v66.0/sobjects/Account",
        referenceId: "newAccount",
        body: { Name: "Acme Corp", Industry: "Technology" }
      },
      {
        method: "POST",
        url: "/services/data/v66.0/sobjects/Contact",
        referenceId: "newContact",
        body: {
          FirstName: "Jane",
          LastName: "Doe",
          AccountId: "@{newAccount.id}"  // Reference previous result
        }
      }
    ]
  };

  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${accessToken}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  });

  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
```

### cURL: Check API limits and remaining quota

```bash
# Input:  Valid access token, instance URL
# Output: JSON with all limit types, maximum, and remaining values

# Check all API limits in your org
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/limits" | \
  python3 -c "import sys,json; d=json.load(sys.stdin); \
  print(f'Daily API: {d[\"DailyApiRequests\"][\"Remaining\"]}/{d[\"DailyApiRequests\"][\"Max\"]}')"

# Expected output: Daily API: 95432/100000

# Describe an object's schema (verify field accessibility)
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/sobjects/Account/describe" | \
  python3 -c "import sys,json; fields=json.load(sys.stdin)['fields']; \
  [print(f'{f[\"name\"]}: {f[\"type\"]} (createable={f[\"createable\"]})') for f in fields[:5]]"
```

## Data Mapping

### Field Mapping Reference

| API Field | Type | API Writable? | Max Length | Gotcha |
|---|---|---|---|---|
| `Id` | ID (18-char) | No (auto-generated) | 18 | Case-insensitive 15-char form exists; always use 18-char in integrations |
| `Name` | String | Yes | 255 (Account), varies by object | Auto-generated for some objects (Cases, Opportunities with auto-number) |
| `CreatedDate` | DateTime | No (system field) | N/A | Cannot be set via API; use `CreatedDate` in queries only |
| `LastModifiedDate` | DateTime | No (system field) | N/A | Use for incremental sync: `WHERE LastModifiedDate > {last_sync}` |
| `SystemModstamp` | DateTime | No (system field) | N/A | More reliable than LastModifiedDate — includes system updates |
| `External_ID__c` | String/Number | Yes | Custom-defined | Use for idempotent upserts; must be unique + external ID flagged |
| `RecordTypeId` | ID | Yes | 18 | Must match a valid RecordType for the object; query RecordType first |
| `OwnerId` | ID | Yes | 18 | Must be an active user; transfers trigger assignment rules |
| Formula fields | Varies | No (calculated) | N/A | API returns current value; cannot write — agents must not suggest writing |
| Multi-select picklist | String | Yes | 4,099 | Values separated by semicolons in API (`;`), not commas |

### Data Type Gotchas

- Salesforce DateTime is always UTC in the API (format: `2026-03-02T14:30:00.000+0000`), but displays in user timezone in the UI. Always store and compare in UTC. [src2]
- Currency fields store in the org's default currency. Multi-currency orgs require checking `CurrencyIsoCode` field and converting at the appropriate exchange rate. The exchange rate used depends on `DatedConversionRate` if advanced currency management is enabled. [src2]
- Multi-select picklist fields serialize as semicolon-delimited in the API (`"Value1;Value2;Value3"`) but display as comma-delimited in the UI. Agents must use semicolons when writing. [src2]
- Boolean fields accept `true`/`false` (lowercase) in JSON. Sending `"True"` (string) or `1` (integer) will fail with INVALID_FIELD_VALUE. [src2]
- Lookup/Master-Detail fields require the 18-character Salesforce ID. 15-character IDs work in some contexts but are unreliable in API calls. [src2]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 | INVALID_SESSION_ID | Access token expired or revoked | Re-authenticate; for JWT flow, generate new token [src2] |
| 403 | REQUEST_LIMIT_EXCEEDED | Daily API call limit exceeded | Wait for 24h window to reset; check `/limits` endpoint; purchase add-on API calls [src1] |
| 404 | NOT_FOUND | Record ID doesn't exist or was deleted | Verify record ID; check recycle bin; ensure object is accessible [src2] |
| 429 | Too Many Requests | Concurrent request limit exceeded | Exponential backoff: wait 2^n seconds, max 5 retries [src4] |
| 503 | Service Unavailable | Salesforce maintenance or Connect API rate limit | Retry with backoff; check trust.salesforce.com for incidents [src4] |
| FIELD_INTEGRITY_EXCEPTION | Validation rule or required field violation | Missing required field or failed custom validation rule | Check validation rules on the object; ensure all required fields are populated [src2] |
| UNABLE_TO_LOCK_ROW | Record locked by another transaction | Concurrent updates to the same record | Retry with random jitter; implement record-level locking strategy [src3] |
| INVALID_FIELD | Field doesn't exist or isn't accessible | Wrong API version, missing FLS, or field doesn't exist on the object | Check field-level security; verify API version supports the field [src2] |
| DUPLICATE_VALUE | Unique field constraint violation | External ID or unique field already has this value | Use upsert instead of insert; check for existing records first [src2] |
| STRING_TOO_LONG | Field value exceeds max length | String longer than field definition allows | Truncate before sending; check `describe` for max lengths [src2] |

### Failure Points in Production

- **Trigger recursion causes governor limit breach**: A REST API upsert fires a before trigger, which updates a related record, which fires another trigger, consuming SOQL and DML from the same pool. Fix: `Implement static boolean flags to prevent trigger re-entry; use the recursion handler pattern`. [src3]
- **Bulk API jobs silently fail on BOM characters**: CSV files with UTF-8 BOM (`\xEF\xBB\xBF`) cause Bulk API 2.0 to reject the first column header. Fix: `Strip BOM from CSV files before upload; use encoding='utf-8-sig' in Python`. [src5]
- **OAuth refresh token expires after 90 days of non-use**: Integrations that run less frequently than quarterly lose their refresh token. Fix: `Use JWT bearer flow for unattended integrations, or implement a weekly token refresh cron job`. [src4]
- **Sandbox refresh resets API limits and connected apps**: After a sandbox refresh, connected apps may need to be re-authorized and API limits reset. Fix: `Automate connected app deployment via Metadata API; document post-refresh runbook`. [src4]
- **Field-level security blocks API access silently**: A field accessible in the UI may not be accessible via API if the integration user's profile lacks FLS. The field simply returns `null` instead of an error. Fix: `Always test with the integration user's profile; use describe calls to verify field accessibility`. [src2]
- **Sharing rules restrict record visibility**: The integration user may not see all records due to org-wide defaults and sharing rules. Fix: `Grant "View All" or "Modify All" on required objects, or use a system admin profile for the integration user`. [src2]

## Anti-Patterns

### Wrong: Querying all records to detect changes

```python
# BAD — Fetches entire object, wastes API calls and hits governor limits
all_accounts = sf.query("SELECT Id, Name, Industry FROM Account")
# Then compare locally to find changes... O(n) on every sync
```

### Correct: Use SystemModstamp filter for incremental sync

```python
# GOOD — Only fetches records modified since last sync
last_sync = "2026-03-01T00:00:00Z"
changed = sf.query(
    f"SELECT Id, Name, Industry FROM Account "
    f"WHERE SystemModstamp > {last_sync}"
)
```

### Wrong: Individual API calls in a loop

```python
# BAD — 1,000 API calls for 1,000 records; hits daily limit fast
for record in records:
    sf.Account.create(record)  # 1 API call each
```

### Correct: Use SObject Collections for batch operations

```python
# GOOD — 5 API calls for 1,000 records (200 per call)
for chunk in chunks(records, 200):
    sf.composite.sobjects.create("Account", chunk)
```

### Wrong: Synchronous callouts inside triggers for every record

```apex
// BAD — Trigger fires callout for each record; hits 100-callout limit instantly
trigger AccountSync on Account (after insert) {
    for (Account a : Trigger.new) {
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://external.api/sync');
        req.setMethod('POST');
        h.send(req);  // Callout in a loop!
    }
}
```

### Correct: Collect records and process asynchronously

```apex
// GOOD — Collect IDs, process in @future or Queueable
trigger AccountSync on Account (after insert) {
    Set<Id> accountIds = new Set<Id>();
    for (Account a : Trigger.new) {
        accountIds.add(a.Id);
    }
    AccountSyncService.syncAsync(accountIds);  // @future or Queueable
}
```

## Common Pitfalls

- **Sandbox != Production API limits**: Sandbox has a flat 5M API calls/day but may not have production data volumes. Load tests pass in sandbox but fail in production when real trigger logic runs. Fix: `Use a full-copy sandbox with production data for integration testing; test with realistic record volumes`. [src4]
- **Not pinning API version**: Omitting the version in API calls defaults to the latest, which can break integrations when a new release changes behavior. Fix: `Always pin API version in your base URL (e.g., /services/data/v66.0/); update deliberately after testing`. [src2]
- **Ignoring field-level security**: Integration works in dev with admin profile but fails in production with a restricted integration user. Fields return null instead of error. Fix: `Create a dedicated permission set for the integration user; use describe calls to verify FLS`. [src2]
- **Not handling partial success in collection operations**: SObject Collections with `allOrNone: false` can return a mix of successes and failures. Code that assumes all-or-nothing misses partial failures silently. Fix: `Always check each result in the response array for the 'success' flag and handle individual failures`. [src2]
- **Hardcoding org URLs**: Using `na1.salesforce.com` instead of My Domain URL. Salesforce has removed legacy hostname redirections in Spring '26. Fix: `Always use the instance_url from your OAuth response; never hardcode instance-specific hostnames`. [src6]
- **Exceeding 10,000 DML rows in batch triggers**: A Bulk API batch of 10,000 records fires triggers that update related records, pushing total DML rows over the 10,000 limit. Fix: `Reduce Bulk API batch size to 200 records when complex triggers exist; use @future for cascading updates`. [src3]

## Diagnostic Commands

```bash
# Check API usage / remaining limits
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/limits" | \
  python3 -c "
import sys, json
limits = json.load(sys.stdin)
for key in ['DailyApiRequests','ConcurrentAsyncGetReportInstances','DailyBulkV2QueryJobs']:
    l = limits.get(key, {})
    print(f'{key}: {l.get(\"Remaining\",\"?\")}/{l.get(\"Max\",\"?\")}')
"

# Test authentication (verify token is valid)
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/"
# Expected: 200

# Verify object/field accessibility for integration user
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/sobjects/Account/describe" | \
  python3 -c "
import sys, json
obj = json.load(sys.stdin)
print(f'Object: {obj[\"name\"]}, Queryable: {obj[\"queryable\"]}, Createable: {obj[\"createable\"]}')
for f in obj['fields'][:10]:
    print(f'  {f[\"name\"]}: createable={f[\"createable\"]}, updateable={f[\"updateable\"]}')
"

# Check integration user permissions
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/query?q=SELECT+Id,Name,ProfileId,IsActive+FROM+User+WHERE+Username='integration@yourorg.com'"

# Monitor Bulk API 2.0 job status
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$INSTANCE_URL/services/data/v66.0/jobs/ingest/$JOB_ID" | \
  python3 -c "
import sys, json
job = json.load(sys.stdin)
print(f'State: {job[\"state\"]}, Records Processed: {job[\"numberRecordsProcessed\"]}, Failed: {job[\"numberRecordsFailed\"]}')
"
```

## Version History & Compatibility

| API Version | Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| v66.0 | Spring '26 (Feb 2026) | Current | Legacy hostname redirections removed; connected app creation disabled by default | Update all hardcoded URLs to My Domain; use External Client App for new integrations [src6] |
| v65.0 | Winter '26 (Oct 2025) | Supported | None significant | -- |
| v64.0 | Summer '25 (Jun 2025) | Supported | API versions 21.0-30.0 retired | Minimum version is now v31.0; update integrations pinned to old versions |
| v63.0 | Spring '25 (Feb 2025) | Supported | -- | -- |
| v62.0 | Winter '25 (Oct 2024) | Supported | -- | -- |
| v58.0 | Spring '24 (Feb 2024) | Supported | -- | Minimum version for some newer composite features |

### Deprecation Policy

Salesforce supports API versions for a minimum of 3 years (approximately 9 major releases). Versions are retired in groups — Salesforce retired versions 7.0-20.0 in 2022 and versions 21.0-30.0 in June 2025. Always check the [API End-of-Life Policy](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/api_rest_eol.htm) before pinning a version. Pin your integration to a specific version and upgrade deliberately after testing. [src2, src6]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time individual record CRUD (<200 records/operation) | Data migration or ETL >2,000 records | Bulk API 2.0 |
| Multi-object operations needing atomicity (Composite API) | Scheduled batch processing of >10K records | Bulk API 2.0 with scheduled jobs |
| Interactive user-facing apps needing immediate response | Real-time event notification without polling | Platform Events or Change Data Capture |
| Exploring/testing API with simple cURL commands | Metadata deployment (field creation, layouts) | Metadata API or Tooling API |
| Small to medium integrations with <50K API calls/day | High-frequency polling (>1 call/second sustained) | Streaming API or CDC |

## Important Caveats

- Rate limits vary dramatically by Salesforce edition. Developer Edition has only 15,000 API calls/24h and 5 concurrent requests — never use it for load testing or production workloads. [src1]
- Sandbox API limits (5M/day) differ from production and reset on a different schedule. An integration that works in sandbox may hit limits in production with lower edition limits. [src4]
- The daily API call limit is a soft limit — Salesforce allows exceeding it temporarily but will enforce a hard block with `REQUEST_LIMIT_EXCEEDED` if sustained overuse is detected. [src4]
- Platform Events, CDC, and Streaming API have separate allocation pools, but Platform Events and CDC share a combined 50K/day delivery allocation in standard tiers. High-volume event-driven architectures require the add-on license. [src1]
- Governor limits (SOQL, DML, CPU) are per-transaction and apply regardless of API used — even Bulk API triggers Apex code subject to governor limits on each batch of records. [src3]
- All rate limit numbers in this card reflect Salesforce documentation as of March 2026 (API v66.0, Spring '26). Salesforce may adjust limits with each release — always verify against the `/limits` endpoint and current release notes. [src1, src2]
- EU instances may have different data residency constraints. Cross-region API calls (e.g., US app calling EU instance) are allowed but may have additional latency. Check your org's data residency requirements. [src2]

## Related Units
<!-- Generated from related_kos frontmatter -->
