Salesforce Bulk API 2.0 Capabilities
What are the Salesforce Bulk API 2.0 capabilities, batch limits, and chunking requirements?
TL;DR
- Bottom line: Bulk API 2.0 is Salesforce's recommended API for high-volume data operations (insert, update, upsert, delete, hardDelete, query, queryAll). It handles batching automatically and processes up to 100 million records per 24-hour period.
- Key limit: 150 MB max upload per job (base64 encoded); Salesforce internally chunks data into 10,000-record batches; max 25 concurrent jobs.
- Watch out for: No serial processing mode exists in Bulk API 2.0 — all jobs run in parallel, which can cause record lock contention in orgs with extensive triggers and flows. Use Bulk API 1.0 if you need serial processing.
- Best for: Scheduled ETL, data migrations, and batch processing of 2,000+ records where sub-second latency is not required.
- Authentication: OAuth 2.0 (all standard flows) — same as Salesforce REST API; no legacy session ID authentication like Bulk API 1.0.
System Profile
Salesforce Bulk API 2.0 is the asynchronous, high-volume data processing API for Salesforce CRM and Platform. It was introduced to simplify the developer experience of the original Bulk API (1.0) by eliminating manual batch management — you submit data and Salesforce handles the chunking, batching, retries, and parallel processing internally. It shares the same REST API framework and OAuth authentication as the Salesforce REST API.
This card covers Bulk API 2.0 as available in API v62.0 (Spring '26) for Enterprise, Unlimited, Performance, and Developer editions. Professional edition has limited Bulk API access. Essentials edition does not include Bulk API. The limits documented here apply to Salesforce Production and Sandbox orgs, though sandbox orgs may have lower limits.
| Property | Value |
|---|---|
| Vendor | Salesforce |
| System | Salesforce CRM / Platform (API v62.0, Spring '26) |
| API Surface | Bulk API 2.0 |
| Current API Version | v62.0 |
| Editions Covered | Enterprise, Unlimited, Performance, Developer |
| Deployment | Cloud |
| API Docs | Bulk API 2.0 Developer Guide |
| Status | GA (Generally Available) |
API Surfaces & Capabilities
Bulk API 2.0 supports two job types — ingest (write operations) and query (read operations). Here is where Bulk API 2.0 fits within the Salesforce API ecosystem:
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| REST API | HTTPS/JSON | Individual record CRUD, <2K records | 200 (composite), 2,000 (query) | 100K calls/24h (Enterprise) | Yes | No |
| Bulk API 2.0 | HTTPS/CSV or JSON | ETL, data migration, >2K records | 150M records per file | 100M records/24h | No (async) | Yes |
| Bulk API 1.0 | HTTPS/CSV, XML, JSON | Serial processing, XML requirement | 10,000 per batch | 15,000 batches/24h | No (async) | Yes |
| SOAP API | HTTPS/XML | Metadata operations, legacy systems | 2,000 per call | Shared with REST | Yes | No |
| Composite API | HTTPS/JSON | Multi-object transactions | 25 subrequests | Shared with REST | Yes | No |
| Streaming API | Bayeux/CometD | Real-time notifications (CDC, PushTopics) | N/A | Edition-dependent | Yes | N/A |
Supported Operations (Ingest)
| Operation | Description | External ID Required? | Notes |
|---|---|---|---|
insert | Creates new records | No | Fails on duplicate if no external ID |
update | Modifies existing records | No | Requires Salesforce record ID in CSV |
upsert | Insert or update based on external ID | Yes | Specify externalIdFieldName on job creation |
delete | Moves records to Recycle Bin | No | Requires Salesforce record ID |
hardDelete | Permanently deletes (bypasses Recycle Bin) | No | Requires "Bulk API Hard Delete" permission |
Supported Operations (Query)
| Operation | Description | Notes |
|---|---|---|
query | Executes SOQL, returns active records | Standard query semantics |
queryAll | Executes SOQL, includes soft-deleted and archived records | Includes Recycle Bin records (up to 15-day retention) |
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max upload size per job | 150 MB (base64 encoded) | Ingest jobs | Keep unencoded CSV/JSON under 100 MB |
| Max records per file | 150,000,000 | Ingest jobs | Practical limit — files rarely approach this |
| Internal batch size | 10,000 records | Ingest jobs (internal) | Salesforce auto-chunks; not configurable |
| Query result chunk size | 100,000-250,000 records | Query jobs (internal) | Salesforce auto-chunks query output |
| Max query result size | 15 GB | Query jobs | Per single query job |
| Query result expiry | 7 days | Query jobs | Must download results within 7 days of job completion |
| Batch processing timeout | 5 minutes | Per internal batch | Batch paused/requeued if exceeded; retried up to 10 times |
| Max fields per record | 5,000 | All operations | Standard Salesforce object limit |
Rolling / Daily Limits
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| Total records processed | 100,000,000 | 24h rolling | Same across all editions with Bulk API access |
| Concurrent ingest jobs | 25 | Per org | Shared between Bulk API 2.0 and 1.0 |
| Concurrent query jobs | 25 | Per org | Shared between Bulk API 2.0 and 1.0 |
| Total jobs in any state | 100,000 | Per org | Delete completed/aborted jobs to free capacity |
| Bulk API 1.0 batches (if using v1) | 15,000 | 24h rolling | Only applies to Bulk API 1.0 |
Transaction / Governor Limits
Each internal batch of 10,000 records processes in transactions of 200 records. Standard Apex governor limits apply per 200-record chunk if triggers or flows fire:
| Limit Type | Per-Transaction Value | Notes |
|---|---|---|
| SOQL queries | 100 | Includes queries from triggers — cascading triggers consume from same pool |
| DML statements | 150 | Each insert/update/delete counts as 1 |
| Callouts | 100 | HTTP requests to external services within a transaction |
| CPU time | 10,000 ms (sync), 60,000 ms (async) | Exceeded = transaction abort |
| Heap size | 6 MB (sync), 12 MB (async) | Large record processing can hit this |
| Total email invocations | 10 | Workflow email actions per transaction |
Authentication
Bulk API 2.0 uses the same authentication as Salesforce REST API — all standard OAuth 2.0 flows are supported. This is a key improvement over Bulk API 1.0, which required SOAP-based session ID authentication.
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 JWT Bearer | Server-to-server, no user context | Session timeout (default 2h) | New JWT per request | Recommended for integrations |
| OAuth 2.0 Web Server | User-context operations, interactive apps | Access: 2h, Refresh: until revoked | Yes | Requires callback URL |
| OAuth 2.0 Client Credentials | Machine-to-machine (no user) | Session timeout | Yes | Available since Winter '23 |
| Username-Password | Legacy, testing only | Session timeout | No | Do NOT use in production — no MFA support |
Authentication Gotchas
- Bulk API 2.0 does not accept SOAP session IDs — you must use OAuth access tokens. If migrating from Bulk API 1.0, update your auth flow. [src4]
- Access tokens are org-scoped for JWT flow — all Bulk API calls run with the connected app's integration user permissions, not the end user's. [src4]
- Session timeout is admin-configurable — do not hardcode 2-hour token lifetimes. Check
Sforce-Limit-Inforesponse header for remaining API calls. [src4]
Constraints
- No serial mode: Bulk API 2.0 only processes batches in parallel. If your org has complex triggers/flows causing UNABLE_TO_LOCK_ROW errors, use Bulk API 1.0 with
concurrencyMode: serial. - No XML support: Only CSV and JSON formats are accepted. If your pipeline produces XML, use Bulk API 1.0 or transform to CSV first.
- Fixed 10,000-record batches: You cannot control batch size. In orgs with heavy automation, 10,000-record batches may cause governor limit breaches. Bulk API 1.0 allows smaller custom batch sizes.
- No UI-based result access: Failed/successful record results are only available via API calls. Bulk API 1.0 results are visible in Setup > Bulk Data Load Jobs.
- hardDelete requires permission: The "Bulk API Hard Delete" permission must be explicitly enabled on the integration user's profile.
- Professional edition limitations: Bulk API is available on Professional edition only through certain connected app configurations, with reduced daily limits.
Integration Pattern Decision Tree
START — User needs to bulk-process data in Salesforce
├── What's the operation?
│ ├── Ingest (insert/update/upsert/delete/hardDelete)
│ │ ├── Data volume < 2,000 records?
│ │ │ ├── YES → Use REST API (simpler, synchronous)
│ │ │ └── NO ↓
│ │ ├── Need serial processing (record lock issues)?
│ │ │ ├── YES → Use Bulk API 1.0 (concurrencyMode: serial)
│ │ │ └── NO ↓
│ │ ├── Data volume < 150 MB per file?
│ │ │ ├── YES → Single Bulk API 2.0 job
│ │ │ └── NO → Split into multiple jobs (each ≤100 MB unencoded)
│ │ └── Need XML format?
│ │ ├── YES → Use Bulk API 1.0
│ │ └── NO → Bulk API 2.0 with CSV or JSON
│ └── Query (extract data)
│ ├── Data volume < 2,000 records?
│ │ ├── YES → Use REST API SOQL query
│ │ └── NO ↓
│ ├── Need soft-deleted/archived records?
│ │ ├── YES → Bulk API 2.0 queryAll operation
│ │ └── NO → Bulk API 2.0 query operation
│ └── Result set > 15 GB?
│ ├── YES → Split SOQL with WHERE clause date ranges
│ └── NO → Single Bulk API 2.0 query job
├── CSV delimiter requirements?
│ ├── Standard comma → Default (no config needed)
│ └── Other (pipe, semicolon, tab, caret, backquote) → Set columnDelimiter
└── Error tolerance?
├── Zero-loss → Poll status + retrieve failedResults + reprocess
└── Best-effort → Poll status, log failures, move on
Quick Reference
Ingest Job API Endpoints
| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Create ingest job | POST | /services/data/v62.0/jobs/ingest | JSON (job config) | Returns id and contentUrl |
| Upload data | PUT | /services/data/v62.0/jobs/ingest/{jobId}/batches | CSV or JSON data | Content-Type: text/csv |
| Close job (start processing) | PATCH | /services/data/v62.0/jobs/ingest/{jobId} | {"state":"UploadComplete"} | Triggers async processing |
| Check job status | GET | /services/data/v62.0/jobs/ingest/{jobId} | N/A | Returns state + record counts |
| Get successful results | GET | /services/data/v62.0/jobs/ingest/{jobId}/successfulResults | N/A | CSV with sf__Id, sf__Created |
| Get failed results | GET | /services/data/v62.0/jobs/ingest/{jobId}/failedResults | N/A | CSV with sf__Error column |
| Get unprocessed records | GET | /services/data/v62.0/jobs/ingest/{jobId}/unprocessedrecords | N/A | Records not attempted |
| Abort job | PATCH | /services/data/v62.0/jobs/ingest/{jobId} | {"state":"Aborted"} | Stops processing |
| Delete job | DELETE | /services/data/v62.0/jobs/ingest/{jobId} | N/A | Frees job count quota |
| List all ingest jobs | GET | /services/data/v62.0/jobs/ingest | N/A | Filter by isPkChunkingEnabled, jobType |
Job States
| State | Meaning | Transitions To |
|---|---|---|
Open | Accepting data uploads | UploadComplete, Aborted |
UploadComplete | Data received, queued for processing | InProgress |
InProgress | Salesforce is processing internal batches | JobComplete, Failed, Aborted |
JobComplete | All records processed (some may have failed individually) | — |
Failed | Job-level failure (e.g., 10 batch retries exhausted) | — |
Aborted | Manually or automatically aborted | — |
Step-by-Step Integration Guide
1. Authenticate and obtain an OAuth access token
Obtain an access token using the JWT Bearer flow for server-to-server integration. [src4]
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=${JWT_TOKEN}"
Verify: curl -H "Authorization: Bearer ${ACCESS_TOKEN}" ${INSTANCE_URL}/services/data/v62.0/limits → expected: JSON with DailyBulkV2QueryJobs field.
2. Create a Bulk API 2.0 ingest job
Define the job parameters: object, operation, content type. [src1]
curl -X POST ${INSTANCE_URL}/services/data/v62.0/jobs/ingest \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"object":"Contact","operation":"upsert","externalIdFieldName":"External_ID__c","contentType":"CSV"}'
Verify: Response includes "state": "Open" and a valid id field.
3. Upload CSV data
Send CSV data as the request body. First line must be field API names. [src1]
curl -X PUT ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID}/batches \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: text/csv" \
--data-binary @contacts.csv
Verify: HTTP 201 Created response.
4. Close the job to start processing
Signal to Salesforce that all data has been uploaded. [src1]
curl -X PATCH ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "UploadComplete"}'
Verify: Response shows "state": "UploadComplete".
5. Poll for job completion
Check status periodically. Recommended: 30s for <100K records, 60s for larger jobs. [src1]
curl ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
# Response: {"state":"JobComplete","numberRecordsProcessed":50000,"numberRecordsFailed":12,...}
Verify: state is JobComplete. Check numberRecordsFailed.
6. Retrieve failed records and reprocess
Download failed records CSV, fix issues, submit new job with corrected records. [src1]
curl ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID}/failedResults \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-o failed_records.csv
Verify: Open failed_records.csv — each row includes sf__Error column.
Code Examples
Python: Bulk upsert with job monitoring and retry
# Input: CSV file path, Salesforce credentials (instance_url, access_token)
# Output: Job completion summary with success/failure counts
import requests
import time
def bulk_upsert(instance_url, access_token, object_name, csv_path,
external_id_field, api_version="v62.0"):
base = f"{instance_url}/services/data/{api_version}/jobs/ingest"
headers = {"Authorization": f"Bearer {access_token}"}
# Create job
job_resp = requests.post(base, headers={**headers, "Content-Type": "application/json"},
json={"object": object_name, "operation": "upsert",
"externalIdFieldName": external_id_field,
"contentType": "CSV", "lineEnding": "LF"})
job_resp.raise_for_status()
job_id = job_resp.json()["id"]
# Upload CSV
with open(csv_path, "rb") as f:
requests.put(f"{base}/{job_id}/batches",
headers={**headers, "Content-Type": "text/csv"}, data=f).raise_for_status()
# Close job
requests.patch(f"{base}/{job_id}",
headers={**headers, "Content-Type": "application/json"},
json={"state": "UploadComplete"}).raise_for_status()
# Poll until complete
while True:
info = requests.get(f"{base}/{job_id}", headers=headers).json()
if info["state"] in ("JobComplete", "Failed", "Aborted"):
break
time.sleep(30)
# Retrieve failed records if any
failed = info.get("numberRecordsFailed", 0)
if failed > 0:
failed_csv = requests.get(f"{base}/{job_id}/failedResults", headers=headers).text
print(f"{failed} failed records:\n{failed_csv[:2000]}")
return {"job_id": job_id, "state": info["state"],
"processed": info.get("numberRecordsProcessed", 0), "failed": failed}
JavaScript/Node.js: Bulk query with result pagination
// Input: Salesforce credentials, SOQL query string
// Output: Array of CSV result chunks (handles locator-based pagination)
async function bulkQuery(instanceUrl, accessToken, soql, apiVersion = 'v62.0') {
const base = `${instanceUrl}/services/data/${apiVersion}/jobs/query`;
const headers = { 'Authorization': `Bearer ${accessToken}` };
// Create query job
const jobResp = await fetch(base, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ operation: 'query', query: soql })
});
const { id: jobId } = await jobResp.json();
// Poll for completion
let state = 'UploadComplete';
while (!['JobComplete', 'Failed', 'Aborted'].includes(state)) {
await new Promise(r => setTimeout(r, 10000));
const info = await (await fetch(`${base}/${jobId}`, { headers })).json();
state = info.state;
}
if (state !== 'JobComplete') throw new Error(`Query job ${state}`);
// Retrieve results with locator-based pagination
let allRecords = [], locator = null;
do {
const url = locator ? `${base}/${jobId}/results?locator=${locator}`
: `${base}/${jobId}/results`;
const resp = await fetch(url, { headers });
locator = resp.headers.get('Sforce-Locator');
if (locator === 'null') locator = null;
allRecords.push(await resp.text());
} while (locator);
return allRecords;
}
cURL: Quick ingest job test
# Input: Valid access token and instance URL
# Output: Job ID and processing status
# 1. Create job
JOB_ID=$(curl -s -X POST ${INSTANCE_URL}/services/data/v62.0/jobs/ingest \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"object":"Account","operation":"insert","contentType":"CSV"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# 2. Upload CSV
curl -X PUT ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID}/batches \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: text/csv" \
-d 'Name,Industry,NumberOfEmployees
Acme Corp,Technology,500
Beta Inc,Finance,200'
# 3. Close job and 4. Check status
curl -X PATCH ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" -d '{"state":"UploadComplete"}'
Data Mapping
CSV Format Requirements
| Property | Value | Notes |
|---|---|---|
| Header row | Required — field API names | Not display labels |
| Encoding | UTF-8 | BOM characters cause job failure |
| Line endings | LF or CRLF | Configurable via lineEnding param |
| Column delimiter | Comma (default) | Options: COMMA, BACKQUOTE, CARET, PIPE, SEMICOLON, TAB |
| Null values | #N/A | Empty string clears field (not null) |
| Boolean values | true / false | Case-insensitive |
| Date format | YYYY-MM-DD | ISO 8601 |
| DateTime format | YYYY-MM-DDThh:mm:ss.sssZ | UTC recommended |
| Number format | No thousands separator | 1234.56 not 1,234.56 |
| Escaping | Double-quote fields with delimiters | Escape quotes by doubling: "" |
Data Type Gotchas
- Salesforce datetime fields are stored in UTC — always use explicit timezone offset or Z suffix to avoid silent conversion errors. [src1]
- Multi-select picklist values must be semicolon-separated within the CSV field, even when using a non-comma delimiter. [src1]
- Currency fields with multi-currency: the currency ISO code comes from CurrencyIsoCode field, not from the amount value. [src1]
- Relationship fields: for insert, use external IDs via Relationship.ExternalIdField column naming (e.g., Account.External_ID__c). [src1]
Error Handling & Failure Points
Common Error Codes
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| DUPLICATE_VALUE | Duplicate external ID or unique field | Record with same value exists | Use upsert instead of insert |
| REQUIRED_FIELD_MISSING | Mandatory field not in CSV | Required field omitted or null | Ensure all required fields have values |
| FIELD_CUSTOM_VALIDATION_EXCEPTION | Validation rule failed | Data violates custom validation rule | Fix data or deactivate rule during migration |
| UNABLE_TO_LOCK_ROW | Row lock contention | Concurrent updates to same record | Use Bulk API 1.0 serial mode; add retry with jitter |
| INVALID_FIELD | Field doesn't exist or isn't writable | Wrong API name or field-level security | Verify field API names and permissions |
| STORAGE_LIMIT_EXCEEDED | Org data storage full | Org exceeded storage allocation | Free storage or purchase additional |
| REQUEST_LIMIT_EXCEEDED | Daily Bulk API limit hit | 100M record daily limit exceeded | Wait for 24h rolling window |
| InvalidBatch | Job-level failure after 10 retries | Internal batch failed repeatedly | Review org automation complexity |
Failure Points in Production
- BOM characters in CSV: Jobs silently fail or produce corrupt data with UTF-8 BOM. Fix:
Strip BOM before upload — in Python: open(file, encoding='utf-8-sig'). [src7] - Governor limit breaches from triggers: 10,000-record internal batches process in 200-record chunks. Complex triggers/flows can breach limits. Fix:
Disable non-critical triggers during bulk loads using a custom setting flag. [src6] - OAuth token expiry during long-running jobs: Jobs processing millions of records may take hours. Fix:
Implement token refresh logic in polling loop. [src4] - Unprocessed records after abort: Some records may remain unprocessed without error messages. Fix:
Always call GET /unprocessedrecords after any non-JobComplete state. [src1] - Column mismatch: Extra CSV columns not matching field API names cause entire job failure. Fix:
Validate headers against describe result before uploading. [src1]
Anti-Patterns
Wrong: Polling job status every second
# BAD — hammers the API, consumes quota, does not speed up processing
while True:
status = check_status(job_id)
if status['state'] == 'JobComplete': break
time.sleep(1) # 3,600 API calls/hour wasted
Correct: Exponential backoff polling
# GOOD — starts at 10s, backs off to 60s max
wait = 10
while True:
status = check_status(job_id)
if status['state'] in ('JobComplete', 'Failed', 'Aborted'): break
time.sleep(min(wait, 60))
wait = min(wait * 1.5, 60)
Wrong: Uploading one giant 500 MB file
# BAD — exceeds 150 MB limit, job creation fails
with open('huge_export.csv', 'rb') as f:
requests.put(f"{base}/{job_id}/batches", data=f)
Correct: Chunking files at 90 MB boundaries
# GOOD — split at ~90 MB (safe margin below 150 MB base64 limit)
def chunk_csv(input_path, max_bytes=90_000_000):
chunks, current_chunk, current_size = [], [], 0
with open(input_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
header = ','.join(next(reader)) + '\n'
for row in reader:
line = ','.join(row) + '\n'
if current_size + len(line.encode()) > max_bytes and current_chunk:
chunks.append(header + ''.join(current_chunk))
current_chunk, current_size = [], 0
current_chunk.append(line)
current_size += len(line.encode())
if current_chunk: chunks.append(header + ''.join(current_chunk))
return chunks # Submit each as separate job
Wrong: Ignoring failed records after JobComplete
# BAD — assumes JobComplete means 100% success
if status['state'] == 'JobComplete':
print("All done!") # Could have thousands of failed records
Correct: Always check failed record count
# GOOD — explicitly handle partial success
if status['state'] == 'JobComplete':
failed = status.get('numberRecordsFailed', 0)
if failed > 0:
failed_csv = get_failed_results(job_id)
save_for_retry(failed_csv)
alert_team(f"Bulk job {job_id}: {failed} records failed")
Common Pitfalls
- Sandbox limits differ from production: A bulk load that works in sandbox may hit limits in production due to different automation. Fix:
Load-test against a full-copy sandbox with production data volumes. [src2] - Numbers with thousands separators:
1,234in CSV is parsed as two columns by the comma delimiter. Fix:Never use thousands separators — format as 1234; or use PIPE delimiter. [src1] - Ignoring Sforce-Limit-Info header: Every response includes remaining API calls. Fix:
Parse and log on every response; alert when under 20% remaining. [src2] - Too many jobs without cleanup: The 100,000 job limit is per-org — old jobs count. Fix:
DELETE completed jobs after retrieving results. [src2] - Using insert when you need upsert: Accidental duplicate submission creates duplicates. Fix:
Define an external ID field and use upsert for idempotent operations. [src1] - Not pinning API version: Omitting version in URLs does not resolve to latest. Fix:
Pin API version (e.g., v62.0); test new versions before upgrading. [src1]
Diagnostic Commands
# Check API usage / remaining Bulk API limits
curl -s ${INSTANCE_URL}/services/data/v62.0/limits \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
| python3 -c "import sys,json; d=json.load(sys.stdin); [print(f'{k}: {v}') for k,v in d.items() if 'Bulk' in k or 'Daily' in k]"
# List all ingest jobs (most recent first)
curl -s "${INSTANCE_URL}/services/data/v62.0/jobs/ingest" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
# Check specific job status
curl -s ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
-H "Authorization: Bearer ${ACCESS_TOKEN}" | python3 -m json.tool
# Test authentication
curl -s -o /dev/null -w "%{http_code}" ${INSTANCE_URL}/services/data/v62.0/ \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
# Expected: 200
# Describe target object fields
curl -s ${INSTANCE_URL}/services/data/v62.0/sobjects/Contact/describe \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
| python3 -c "import sys,json; [print(f'{f[\"name\"]:40} {f[\"type\"]:15}') for f in json.load(sys.stdin)['fields']]"
# Delete a completed job to free quota
curl -X DELETE ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Version History & Compatibility
| API Version | Release | Status | Key Changes | Notes |
|---|---|---|---|---|
| v62.0 | Spring '26 (Feb 2026) | Current | No breaking changes | Latest GA version |
| v61.0 | Winter '26 (Oct 2025) | Supported | No breaking changes | — |
| v60.0 | Summer '25 (Jun 2025) | Supported | No breaking changes | — |
| v56.0 | Spring '23 (Feb 2023) | Supported | Bulk API 2.0 became default in Data Loader | Major adoption milestone |
| v47.0 | Winter '20 (Oct 2019) | Supported | Query operations added | Previously ingest-only |
| v41.0 | Winter '18 (Oct 2017) | Minimum for Bulk API 2.0 | Initial GA release | Ingest operations only |
Deprecation Policy
Salesforce supports API versions for a minimum of 3 years. Versions are retired in groups — typically 10+ versions at once, with at least 1 year advance notice. No Bulk API 2.0-era versions have been retired as of Spring '26.
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Data migration or ETL of 2,000+ records | Real-time individual record operations needing <1s response | REST API |
| Scheduled nightly/hourly batch synchronization | You need serial processing to avoid lock contention | Bulk API 1.0 (serial mode) |
| Initial data load for new Salesforce org | You need XML format for legacy integration | Bulk API 1.0 (XML support) |
| Large SOQL query exports (>2,000 records) | You need immediate query results (sub-second) | REST API SOQL query |
| Extracting soft-deleted records (queryAll) | You need real-time change notifications | Streaming API / CDC |
| Idempotent bulk upserts via external ID | You need all-or-nothing transactional behavior | Composite API |
Important Caveats
- Limits reflect API v62.0 (Spring '26) — Salesforce may adjust limits each release. Always verify against the current Limits Quick Reference.
- The 100 million records/24h limit is a rolling window, not a calendar-day reset.
- Sandbox orgs have independent limits that are often lower than production.
- Professional edition has limited Bulk API access — verify your edition's capabilities before architecting a solution.
- Bulk API 2.0 does not support PK Chunking for ingest jobs (only Bulk API 1.0 does).
- All times and dates in the Bulk API are UTC regardless of user timezone settings.