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 (separate API) or MuleSoft Anypoint (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 | REST API Developer Guide |
| Status | GA (General Availability) |
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 | 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 |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per SOQL query page | 2,000 | REST API | Use queryMore or nextRecordsUrl for pagination |
| Max SOQL query rows per transaction | 50,000 | All APIs triggering Apex | Aggregate across all queries in one transaction |
| 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 |
| Max SObject Collection records | 200 | SObject Collections | For create/update/delete in a single request |
| Max Bulk API 2.0 job file size | 150 MB | Bulk API 2.0 | Base64 encoding adds ~50% — limit to 100 MB pre-encoding |
| 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 |
| 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 |
| Bulk API 2.0 records processed | 100,000,000 | 24h | Shared across all editions |
| Bulk API batches | 15,000 | 24h | Shared between Bulk API 1.0 and 2.0 |
| Concurrent long-running API requests | 25 | Per org | Developer: 5 concurrent; requests >20s count as long-running |
| Streaming API events | 50K–1M | 24h | Enterprise: 200K; Unlimited/Performance: 1M |
| Platform Events + CDC deliveries | 50,000 | 24h (standard) | Add-on license: 100K extra, enforced monthly (4.5M/month) |
| Platform Events publishes | 250,000 | Per hour | Rarely a bottleneck |
| Sandbox API calls | 5,000,000 | 24h | Fixed, independent of edition |
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 |
| Total SOQL query rows returned | 50,000 | 50,000 | Aggregate across all queries in the transaction |
| DML statements | 150 | 150 | Each insert/update/delete/upsert counts as 1 |
| DML rows | 10,000 | 10,000 | Total records across all DML operations |
| CPU time | 10,000 ms | 60,000 ms | Exceeded = transaction abort with System.LimitException |
| Heap size | 6 MB | 12 MB | Watch for large query results stored in memory |
| Callouts (HTTP requests) | 100 | 100 | External service calls within a transaction |
| Callout timeout | 120,000 ms total | 120,000 ms total | Per-callout max: 120s; total across all callouts: 120s |
| Future method invocations | 50 | 0 (not allowed) | Cannot call @future from @future |
| Queueable jobs queued | 50 | 50 | Per transaction |
| Email invocations | 10 | 10 | SingleEmailMessage sends per transaction |
| SOSL searches | 20 | 20 | Per transaction |
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 JWT Bearer | Server-to-server, no user context | Session timeout (default 2h) | No — issue new JWT per request | Recommended for integrations; requires connected app + X.509 certificate |
| 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 |
| OAuth 2.0 Client Credentials | Server-to-server (Spring '23+), simpler than JWT | Session timeout | No refresh token | No user context |
| Username-Password | Testing only, legacy | Session timeout | No | Do NOT use in production — no MFA support |
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: SObject Collections (multi-record) or Composite (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
| |-- Batch/Bulk (scheduled, high volume)
| | |-- Data volume < 2,000 records?
| | | |-- YES --> REST API SObject Collections (simpler, no batch overhead)
| | | +-- NO --> Bulk API 2.0 (single job <100K; chunking for >100K)
| | +-- Need real-time progress tracking?
| | |-- YES --> Bulk API 2.0 (poll job status endpoint)
| | +-- NO --> Bulk API 2.0 with completion notification
| |-- 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
| | +-- NO --> Object-specific triggers + Platform Events
| +-- File-based (CSV import)
| +-- Use Bulk API 2.0 with CSV upload
|-- Which direction?
| |-- Inbound (writing to SF) --> check daily API limits + governor limits for triggers
| |-- Outbound (reading from SF) --> check query row limits (50K/transaction)
| +-- Bidirectional --> design conflict resolution FIRST (external ID + last-modified wins)
+-- Error tolerance?
|-- Zero-loss --> implement idempotency (external ID upsert) + dead letter queue
+-- Best-effort --> fire-and-forget with exponential backoff retry
| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Create record | POST | /services/data/v66.0/sobjects/{Object} | JSON | Returns record ID on 201 success |
| Read record | GET | /services/data/v66.0/sobjects/{Object}/{id} | N/A | Specify fields with ?fields= |
| Update record | PATCH | /services/data/v66.0/sobjects/{Object}/{id} | JSON | Partial update — only changed fields |
| Delete record | DELETE | /services/data/v66.0/sobjects/{Object}/{id} | N/A | Returns 204 No Content |
| Upsert by external ID | PATCH | /services/data/v66.0/sobjects/{Object}/{ExtIdField}/{ExtIdValue} | JSON | Idempotent create/update |
| Query (SOQL) | GET | /services/data/v66.0/query?q={SOQL} | N/A | Max 2,000 records per page |
| Query more | GET | /services/data/v66.0/query/{queryLocator} | N/A | Continue paginating |
| Composite | POST | /services/data/v66.0/composite | JSON | Up to 25 subrequests |
| SObject Collections | POST | /services/data/v66.0/composite/sobjects | JSON array | Up to 200 records per call |
| Describe object | GET | /services/data/v66.0/sobjects/{Object}/describe | N/A | Full object metadata |
| Check limits | GET | /services/data/v66.0/limits | N/A | Remaining allocations for all limits |
| Search (SOSL) | GET | /services/data/v66.0/search?q={SOSL} | N/A | Full-text search across objects |
Use the JWT bearer flow for server-to-server integrations. Create a connected app in Salesforce Setup, upload your X.509 certificate, and configure pre-authorized profiles. [src2]
# Generate JWT and exchange for access token
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
Use the query endpoint for structured reads. Always paginate — default page size is 2,000 records. [src2]
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
Use external ID upsert for idempotent writes — prevents duplicates on retry. [src2]
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"}'
Verify: 201 (created) or 204 (updated)
When done is false, use nextRecordsUrl to fetch the next page. [src2]
import requests
def query_all(instance_url, access_token, soql):
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
Handle 429 (rate limit), 503 (service unavailable), and expired tokens with exponential backoff. [src4]
import time, requests
def sf_api_call(method, url, headers, json=None, max_retries=5):
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, json=json)
if response.status_code == 429:
time.sleep(min(2 ** attempt * 2, 60))
continue
elif response.status_code == 503:
time.sleep(min(2 ** attempt * 5, 120))
continue
elif response.status_code == 401:
headers["Authorization"] = f"Bearer {refresh_access_token()}"
continue
return response
raise Exception(f"Max retries exceeded for {url}")
Verify: Function returns valid response and retries on 429/503
# Input: Salesforce credentials (JWT), list of account records
# Output: Upsert results with success/failure counts
import requests, jwt, time
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives import serialization
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 = self.instance_url = None
def authenticate(self):
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 upsert_collection(self, sobject, ext_id_field, records):
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()
def _headers(self):
return {"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"}
// Input: Salesforce access token, instance URL
// Output: Composite response with all subrequest results
async function compositeOperation(instanceUrl, accessToken) {
const url = `${instanceUrl}/services/data/v66.0/composite`;
const body = {
allOrNone: true,
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}" } }
]
};
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();
}
# Input: Valid access token, instance URL
# Output: Remaining API quota
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: Daily API: 95432/100000
| API Field | Type | API Writable? | Max Length | Gotcha |
|---|---|---|---|---|
Id | ID (18-char) | No (auto-generated) | 18 | Always use 18-char in integrations |
Name | String | Yes | 255 (Account) | Auto-generated for some objects |
CreatedDate | DateTime | No (system field) | N/A | Cannot be set via API; query only |
SystemModstamp | DateTime | No (system field) | N/A | More reliable than LastModifiedDate for sync |
External_ID__c | String/Number | Yes | Custom | Use for idempotent upserts; must be unique + external ID flagged |
| Formula fields | Varies | No (calculated) | N/A | Cannot write — agents must not suggest writing |
| Multi-select picklist | String | Yes | 4,099 | Values separated by semicolons (;), not commas |
2026-03-02T14:30:00.000+0000); displays in user timezone in UI. [src2]"Value1;Value2") but comma-delimited in UI. [src2]true/false (lowercase JSON). Sending "True" (string) or 1 (integer) fails. [src2]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 | INVALID_SESSION_ID | Access token expired or revoked | Re-authenticate; generate new JWT token |
| 403 | REQUEST_LIMIT_EXCEEDED | Daily API call limit exceeded | Wait for 24h window reset; check /limits; purchase add-on calls |
| 404 | NOT_FOUND | Record ID doesn't exist or was deleted | Verify record ID; check recycle bin |
| 429 | Too Many Requests | Concurrent request limit exceeded | Exponential backoff: wait 2^n seconds, max 5 retries |
| 503 | Service Unavailable | Salesforce maintenance or rate limit | Retry with backoff; check trust.salesforce.com |
| UNABLE_TO_LOCK_ROW | Record locked | Concurrent updates to same record | Retry with random jitter; implement locking strategy |
| INVALID_FIELD | Field inaccessible | Wrong API version or missing FLS | Check field-level security; verify API version |
| DUPLICATE_VALUE | Unique constraint violation | External ID already exists | Use upsert instead of insert |
Implement static boolean flags to prevent trigger re-entry. [src3]Strip BOM from CSV; use encoding='utf-8-sig' in Python. [src5]Use JWT bearer flow or implement weekly token refresh cron. [src4]null instead of error when integration user lacks FLS. Fix: Test with integration user profile; use describe calls to verify. [src2]Automate via Metadata API; document post-refresh runbook. [src4]Grant "View All" or "Modify All" on required objects. [src2]# BAD -- Fetches entire object, wastes API calls
all_accounts = sf.query("SELECT Id, Name FROM Account")
# Then compare locally... O(n) on every sync
# GOOD -- Only fetches records modified since last sync
changed = sf.query(
f"SELECT Id, Name FROM Account "
f"WHERE SystemModstamp > {last_sync}"
)
# BAD -- 1,000 API calls for 1,000 records
for record in records:
sf.Account.create(record) # 1 API call each
# GOOD -- 5 API calls for 1,000 records (200 per call)
for chunk in chunks(records, 200):
sf.composite.sobjects.create("Account", chunk)
// BAD -- Callout in a loop hits 100-callout limit
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');
h.send(req);
}
}
// 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);
}
Use full-copy sandbox with realistic data volumes. [src4]Pin version in base URL (e.g., /services/data/v66.0/). [src2]Create dedicated permission set; use describe calls. [src2]allOrNone: false returns mixed results. Fix: Check each result's 'success' flag individually. [src2]Always use instance_url from OAuth response. [src6]Reduce batch size to 200 when complex triggers exist. [src3]# 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
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\"]}')
for f in obj['fields'][:10]:
print(f' {f[\"name\"]}: createable={f[\"createable\"]}')
"
# Monitor Bulk API 2.0 job status
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"$INSTANCE_URL/services/data/v66.0/jobs/ingest/$JOB_ID"
| 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 URLs to My Domain; use External Client App |
| 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 |
| v63.0 | Spring '25 (Feb 2025) | Supported | — | — |
| v62.0 | Winter '25 (Oct 2024) | Supported | — | — |
| v58.0 | Spring '24 (Feb 2024) | Supported | — | Min version for newer composite features |
Deprecation Policy: Salesforce supports API versions for a minimum of 3 years (~9 releases). Versions retired in groups: 7.0-20.0 in 2022, 21.0-30.0 in June 2025. Pin your version and upgrade deliberately. [API End-of-Life Policy]
| 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 CDC |
| 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 |
REQUEST_LIMIT_EXCEEDED if sustained./limits endpoint and current release notes.