---
# === IDENTITY ===
id: business/erp-integration/salesforce-bulk-api-capabilities/2026
canonical_question: "What are the Salesforce Bulk API 2.0 capabilities, batch limits, and chunking requirements?"
aliases:
  - "Salesforce Bulk API 2.0 limits and batch sizes"
  - "How does Salesforce Bulk API 2.0 handle large data loads?"
  - "Salesforce Bulk API 2.0 vs Bulk API 1.0 differences"
  - "What are the CSV format requirements for Salesforce Bulk API 2.0?"
entity_type: erp_integration
domain: business > erp-integration > salesforce-bulk-api-capabilities
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Salesforce"
    vendor: "Salesforce"
    version: "API v62.0 (Spring '26)"
    edition: "Enterprise, Unlimited, Performance, Developer"
    deployment: cloud
    api_surface: "Bulk API 2.0"

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

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "v56.0 (Spring '23) — Bulk API 2.0 became default in Data Loader"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Daily record limit: 100 million records per 24-hour rolling window across all Bulk API 2.0 jobs"
  - "Max upload size: 150 MB per job (base64 encoded); keep unencoded files under 100 MB"
  - "Concurrent jobs: max 25 ingest or query jobs running simultaneously (shared with Bulk API 1.0)"
  - "No serial processing mode — Bulk API 2.0 only supports parallel mode; use Bulk API 1.0 if serial is required"
  - "Internal batch size: fixed at 10,000 records per internal batch — not configurable"
  - "Query result retrieval: results expire after 7 days; max 15 GB per query job"
  - "CSV and JSON only — XML format not supported (unlike Bulk API 1.0)"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs real-time individual record operations with <1s latency"
    use_instead: "business/erp-integration/salesforce-rest-api-capabilities/2026"
  - condition: "User needs serial processing mode to avoid record lock contention"
    use_instead: "business/erp-integration/salesforce-bulk-api-v1-capabilities/2026"
  - condition: "User needs event-driven integration with change notifications"
    use_instead: "business/erp-integration/salesforce-platform-events-cdc/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"

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

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/salesforce-rest-api-capabilities/2026"
      label: "Salesforce REST API capabilities for real-time individual record operations"
  solves: []
  alternative_to:
    - id: "business/erp-integration/salesforce-bulk-api-v1-capabilities/2026"
      label: "Bulk API 1.0 — serial mode, XML support, manual batch control"
  often_confused_with:
    - id: "business/erp-integration/salesforce-rest-api-capabilities/2026"
      label: "REST API is for real-time CRUD; Bulk API 2.0 is for async high-volume ETL"

# === SOURCES ===
sources:
  - id: src1
    title: "Bulk API 2.0 and Bulk API Developer Guide (v66.0, Spring '26)"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src2
    title: "Bulk API and Bulk API 2.0 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: src3
    title: "What's the Difference Between Bulk API 2.0 and Bulk API?"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_common_diff_two_versions.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src4
    title: "Step 2: Authentication — Bulk API 2.0"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/authentication.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src5
    title: "Salesforce Bulk API v1 vs. v2: A General Guide"
    author: Salesforce Watch
    url: https://sf.watch/articles/147/salesforce-bulk-api-v1-vs-salesforce-bulk-api-v2-a-general-guide/
    type: technical_blog
    published: 2025-06-15
    reliability: moderate_high
  - id: src6
    title: "Bulk API 1 is more useful than Bulk API 2"
    author: codeulike
    url: https://codeulike.com/posts/2024/bulkapi1vs2
    type: technical_blog
    published: 2024-09-12
    reliability: moderate_high
  - id: src7
    title: "Processing Large Amounts of Data in Salesforce with Bulk API 2.0"
    author: Ballard Software
    url: https://ballardsoftware.com/processing-large-amounts-of-data-in-salesforce-with-bulk-api-2-0/
    type: technical_blog
    published: 2025-03-20
    reliability: moderate_high
  - id: src8
    title: "General Guidelines for Data Loads"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_planning_guidelines.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
---

# Salesforce Bulk API 2.0 Capabilities

## 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. [src1]
- **Key limit**: 150 MB max upload per job (base64 encoded); Salesforce internally chunks data into 10,000-record batches; max 25 concurrent jobs. [src2]
- **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. [src6]
- **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. [src4]

## 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. [src1]

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. [src2]

| 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 URL** | [Bulk API 2.0 Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm) |
| **Status** | GA (Generally Available) |

## API Surfaces & Capabilities

Bulk API 2.0 supports two job types — ingest (write operations) and query (read operations). Compared to the broader Salesforce API ecosystem, here is where Bulk API 2.0 fits: [src1, src3]

| 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 |

[src1]

### 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) |

[src1]

## 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 to account for base64 overhead [src7] |
| Max records per file | 150,000,000 | Ingest jobs | Practical limit — files rarely approach this [src5] |
| Internal batch size | 10,000 records | Ingest jobs (internal) | Salesforce auto-chunks; not configurable [src6] |
| Query result chunk size | 100,000-250,000 records | Query jobs (internal) | Salesforce auto-chunks query output [src3] |
| Max query result size | 15 GB | Query jobs | Per single query job [src2] |
| Query result expiry | 7 days | Query jobs | Must download results within 7 days of job completion [src2] |
| Batch processing timeout | 5 minutes | Per internal batch | Batch paused/requeued if exceeded; retried up to 10 times [src6] |
| Max fields per record | 5,000 | All operations | Standard Salesforce object limit |

[src2]

### 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 [src2] |
| Concurrent ingest jobs | 25 | Per org | Shared between Bulk API 2.0 and 1.0 [src2] |
| Concurrent query jobs | 25 | Per org | Shared between Bulk API 2.0 and 1.0 [src2] |
| Total jobs in any state | 100,000 | Per org | Delete completed/aborted jobs to free capacity [src2] |
| Bulk API 1.0 batches (if using v1) | 15,000 | 24h rolling | Only applies to Bulk API 1.0 [src5] |

[src2]

### 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: [src6]

| 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 |

[src2]

## 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. [src4]

| 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 — no interactive login needed [src4] |
| OAuth 2.0 Web Server | User-context operations, interactive apps | Access: 2h, Refresh: until revoked | Yes | Requires callback URL; most common for connected apps [src4] |
| OAuth 2.0 Client Credentials | Machine-to-machine (no user) | Session timeout | Yes | Available since Winter '23; requires connected app setup |
| Username-Password | Legacy, testing only | Session timeout | No | Do NOT use in production — no MFA support, IP restrictions apply [src4] |

### 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. Ensure the integration user has "API Enabled" and "Bulk API Hard Delete" (if needed). [src4]
- Session timeout is admin-configurable (Setup > Session Settings) — do not hardcode 2-hour token lifetimes. Check `Sforce-Limit-Info` response header for remaining API calls. [src4]

## Constraints
<!-- Agents: read this section before recommending Bulk API 2.0. These are hard constraints. -->

- **No serial mode**: Bulk API 2.0 only processes batches in parallel. If your org has complex triggers/flows that cause UNABLE_TO_LOCK_ROW errors on concurrent updates to related records, use Bulk API 1.0 with `concurrencyMode: serial` instead. [src6]
- **No XML support**: Only CSV and JSON formats are accepted. If your integration pipeline produces XML, use Bulk API 1.0 or transform to CSV first. [src5]
- **Fixed 10,000-record batches**: You cannot control batch size. In orgs with heavy automation (flows, triggers, validation rules), 10,000-record batches may cause governor limit breaches. Bulk API 1.0 allows smaller custom batch sizes. [src6]
- **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. [src6]
- **hardDelete requires permission**: The "Bulk API Hard Delete" permission must be explicitly enabled on the integration user's profile. Without it, hardDelete operations silently fail. [src1]
- **Professional edition limitations**: Bulk API is available on Professional edition only through certain connected app configurations, with reduced daily limits. [src2]

## 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, immediate response)
│   │   │   └── 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 (only version supporting XML)
│   │       └── 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 query 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 on job creation
└── Error tolerance?
    ├── Zero-loss required → Poll job status + retrieve failedResults + reprocess failed records
    └── Best-effort → Poll job 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` or `application/json` |
| 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` |

[src1]

### Query Job API Endpoints

| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Create query job | POST | `/services/data/v62.0/jobs/query` | JSON (SOQL + operation) | operation: `query` or `queryAll` |
| Check query job status | GET | `/services/data/v62.0/jobs/query/{jobId}` | N/A | Returns state + record counts |
| Get query results | GET | `/services/data/v62.0/jobs/query/{jobId}/results` | N/A | Use `locator` param for pagination |
| Abort query job | PATCH | `/services/data/v62.0/jobs/query/{jobId}` | `{"state": "Aborted"}` | |
| Delete query job | DELETE | `/services/data/v62.0/jobs/query/{jobId}` | N/A | |

[src1]

### 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 | — |

[src1]

## 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. This is the recommended approach for unattended Bulk API operations. [src4]

```bash
# Generate JWT assertion and exchange for access token
# Prerequisites: connected app with digital certificate, integration user

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}"

# Response: {"access_token":"00D...","instance_url":"https://yourorg.my.salesforce.com",...}
```

**Verify**: `curl -H "Authorization: Bearer ${ACCESS_TOKEN}" ${INSTANCE_URL}/services/data/v62.0/limits` → expected: JSON with `DailyBulkV2QueryJobs`, `DailyBulkV2QueryFileStorageMB` fields showing remaining quota.

### 2. Create a Bulk API 2.0 ingest job

Define the job parameters: object, operation, content type, and optional column delimiter or line ending. [src1]

```bash
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",
    "columnDelimiter": "COMMA",
    "lineEnding": "LF"
  }'

# Response: {"id":"7502x000003eGr8AAE","state":"Open","contentUrl":"services/data/v62.0/jobs/ingest/7502x000003eGr8AAE/batches",...}
```

**Verify**: Response includes `"state": "Open"` and a valid `id` field.

### 3. Upload CSV data to the job

Send your CSV data as the request body. The first line must be the header row with field API names. Keep file size under 100 MB (unencoded) to stay safely within the 150 MB base64 limit. [src1, src7]

```bash
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

# contacts.csv format:
# External_ID__c,FirstName,LastName,Email
# EXT-001,Jane,Doe,jane.doe@example.com
# EXT-002,John,Smith,john.smith@example.com
```

**Verify**: HTTP 201 Created response.

### 4. Close the job to start processing

Signal to Salesforce that all data has been uploaded by setting the state to `UploadComplete`. [src1]

```bash
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"}'

# Response: {"id":"7502x000003eGr8AAE","state":"UploadComplete",...}
```

**Verify**: Response shows `"state": "UploadComplete"`.

### 5. Poll for job completion

Check job status periodically until the state is `JobComplete` or `Failed`. Recommended polling interval: 30 seconds for jobs under 100K records, 60 seconds for larger jobs. [src1]

```bash
curl ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

# Response when complete:
# {"id":"7502x000003eGr8AAE","state":"JobComplete",
#  "numberRecordsProcessed":50000,"numberRecordsFailed":12,...}
```

**Verify**: `state` is `JobComplete`. Check `numberRecordsFailed` — a value of 0 means full success.

### 6. Retrieve failed records and reprocess

If `numberRecordsFailed > 0`, download the failed records CSV, fix the issues, and submit a new job with only the corrected records. [src1]

```bash
# Get failed records
curl ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID}/failedResults \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -o failed_records.csv

# CSV includes sf__Error and sf__Id columns explaining each failure
# Common errors: DUPLICATE_VALUE, REQUIRED_FIELD_MISSING, FIELD_CUSTOM_VALIDATION_EXCEPTION
```

**Verify**: Open `failed_records.csv` — each row includes an `sf__Error` column with the specific error message.

## Code Examples

### Python: Bulk upsert with job monitoring and retry

```python
# 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"):
    """Execute a Bulk API 2.0 upsert with polling and error retrieval."""
    base = f"{instance_url}/services/data/{api_version}/jobs/ingest"
    headers = {"Authorization": f"Bearer {access_token}"}

    # Step 1: 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",
            "columnDelimiter": "COMMA"
        })
    job_resp.raise_for_status()
    job_id = job_resp.json()["id"]
    print(f"Created job: {job_id}")

    # Step 2: Upload CSV data
    with open(csv_path, "rb") as f:
        upload_resp = requests.put(
            f"{base}/{job_id}/batches",
            headers={**headers, "Content-Type": "text/csv"},
            data=f)
    upload_resp.raise_for_status()
    print("Data uploaded")

    # Step 3: Close job
    requests.patch(f"{base}/{job_id}",
        headers={**headers, "Content-Type": "application/json"},
        json={"state": "UploadComplete"}).raise_for_status()

    # Step 4: Poll until complete
    while True:
        status_resp = requests.get(f"{base}/{job_id}", headers=headers)
        status_resp.raise_for_status()
        info = status_resp.json()
        state = info["state"]
        print(f"  State: {state} | Processed: {info.get('numberRecordsProcessed', 0)} "
              f"| Failed: {info.get('numberRecordsFailed', 0)}")
        if state in ("JobComplete", "Failed", "Aborted"):
            break
        time.sleep(30)  # Poll every 30 seconds

    # Step 5: Retrieve failed records if any
    failed_count = info.get("numberRecordsFailed", 0)
    if failed_count > 0:
        failed_resp = requests.get(f"{base}/{job_id}/failedResults", headers=headers)
        print(f"\n{failed_count} failed records:\n{failed_resp.text[:2000]}")

    return {
        "job_id": job_id,
        "state": state,
        "processed": info.get("numberRecordsProcessed", 0),
        "failed": failed_count
    }
```

### JavaScript/Node.js: Bulk query with result pagination

```javascript
// Input:  Salesforce credentials, SOQL query string
// Output: Array of all result records (handles multi-locator pagination)
// Requires: node-fetch v3+ (npm install node-fetch@3)

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();
  console.log(`Query job created: ${jobId}`);

  // Poll for completion
  let state = 'UploadComplete';
  while (!['JobComplete', 'Failed', 'Aborted'].includes(state)) {
    await new Promise(r => setTimeout(r, 10000)); // 10s poll interval
    const statusResp = await fetch(`${base}/${jobId}`, { headers });
    const info = await statusResp.json();
    state = info.state;
    console.log(`  State: ${state} | Records: ${info.numberRecordsProcessed || 0}`);
  }
  if (state !== 'JobComplete') throw new Error(`Query job ${state}`);

  // Retrieve results with pagination via locator
  let allRecords = [];
  let 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;
    const csvText = await resp.text();
    allRecords.push(csvText);
  } while (locator);

  return allRecords; // Array of CSV chunks
}
```

### cURL: Quick ingest job test (insert 3 records)

```bash
# 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'])")

echo "Job ID: ${JOB_ID}"

# 2. Upload CSV inline
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
Gamma LLC,Healthcare,150'

# 3. Close job
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"}'

# 4. Check status (repeat until JobComplete)
curl -s ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'State: {d[\"state\"]} | Processed: {d.get(\"numberRecordsProcessed\",0)} | Failed: {d.get(\"numberRecordsFailed\",0)}')"
```

## Data Mapping

### CSV Format Requirements

| Property | Value | Notes |
|---|---|---|
| Header row | Required — first row must contain field API names | Not display labels (e.g., `FirstName` not `First Name`) |
| Encoding | UTF-8 | BOM characters will cause job failure |
| Line endings | LF (`\n`) or CRLF (`\r\n`) | Configurable via `lineEnding` param (default: LF) |
| Column delimiter | Comma (default) | Options: `COMMA`, `BACKQUOTE`, `CARET`, `PIPE`, `SEMICOLON`, `TAB` [src3] |
| Null values | `#N/A` | Sets field to null; empty string clears the field to empty (not null) |
| Boolean values | `true` / `false` | Case-insensitive |
| Date format | `YYYY-MM-DD` | ISO 8601 |
| DateTime format | `YYYY-MM-DDThh:mm:ss.sss+hh:mm` or `YYYY-MM-DDThh:mm:ss.sssZ` | UTC recommended to avoid timezone issues |
| Number format | No thousands separator | `1234.56` not `1,234.56` — commas break CSV parsing |
| Escaping | Double-quote fields containing delimiters, newlines, or quotes | Quote character is `"` — escape by doubling: `""` |

[src1]

### Data Type Gotchas

- Salesforce datetime fields are stored in UTC regardless of user timezone. When uploading, always use explicit timezone offset or UTC (`Z` suffix) to avoid silent timezone conversion errors. [src1]
- Multi-select picklist values must be semicolon-separated within the CSV field, even when using a non-comma delimiter: `"Value1;Value2;Value3"`. [src1]
- Currency fields: if multi-currency is enabled, the currency ISO code comes from the record's `CurrencyIsoCode` field, not from the amount value. Upload amounts as plain decimals. [src1]
- Relationship fields (lookups): for insert operations, you can use external IDs on related objects via `Relationship.ExternalIdField` column naming (e.g., `Account.External_ID__c` instead of `AccountId`). [src1]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| `DUPLICATE_VALUE` | Duplicate external ID or unique field | Record with same unique value already exists | Check for existing records first; use upsert instead of insert |
| `REQUIRED_FIELD_MISSING` | Mandatory field not in CSV | Required field omitted or null in CSV row | Ensure all required fields have values; check page layout requirements |
| `FIELD_CUSTOM_VALIDATION_EXCEPTION` | Validation rule failed | Record data violates a custom validation rule | Check active validation rules; fix data or deactivate rule during migration |
| `UNABLE_TO_LOCK_ROW` | Row lock contention | Concurrent updates to same record or parent | Reduce concurrency; use Bulk API 1.0 serial mode; add retry with jitter [src6] |
| `INVALID_FIELD` | Field doesn't exist or isn't writable | Wrong API name or field-level security | Verify field API names; check integration user's field-level security |
| `STORAGE_LIMIT_EXCEEDED` | Org data storage full | Org has exceeded its data storage allocation | Free storage or purchase additional; do not retry until resolved |
| `REQUEST_LIMIT_EXCEEDED` | Daily Bulk API limit hit | 100M record daily limit exceeded | Wait for 24h window to roll; check `/services/data/v62.0/limits` |
| `InvalidBatch` | Job-level failure after 10 retries | Internal batch failed processing repeatedly | Review org automation complexity; consider smaller data volumes |

[src1, src2]

### Failure Points in Production

- **BOM characters in CSV**: Bulk API 2.0 jobs silently fail or produce corrupt data when CSV files contain a UTF-8 BOM (byte order mark). Fix: `Strip BOM before upload — in Python: open(file, encoding='utf-8-sig')`. [src7]
- **Governor limit breaches from triggers**: Each 10,000-record internal batch processes in 200-record transaction chunks. Complex triggers and flows can breach governor limits within a single chunk, failing the entire batch. Fix: `Disable non-critical triggers during bulk loads using a custom setting flag; or use Bulk API 1.0 with smaller batch sizes`. [src6]
- **OAuth token expiry during long-running jobs**: Jobs processing millions of records may take hours. If the access token expires mid-poll, subsequent API calls fail with 401. Fix: `Implement token refresh logic in polling loop; check token validity before each status request`. [src4]
- **Unprocessed records after job abort**: If a job is aborted or fails, some records may remain unprocessed without error messages. Fix: `Always call GET /unprocessedrecords after any non-JobComplete state to identify records that were never attempted`. [src1]
- **Column mismatch between CSV and Salesforce**: Extra columns in CSV that don't match Salesforce field API names cause the entire job to fail — not individual records. Fix: `Validate CSV headers against the target object's describe result before uploading`. [src1]

## Anti-Patterns

### Wrong: Polling job status every second

```python
# BAD — hammers the API, consumes API call quota, does not speed up processing
while True:
    status = check_status(job_id)
    if status['state'] == 'JobComplete':
        break
    time.sleep(1)  # 1-second polling burns ~3,600 API calls/hour
```

### Correct: Exponential backoff polling

```python
# GOOD — starts at 10s, backs off to 60s max, respects API limits
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)  # Backoff to 60s max
```

### Wrong: Uploading one giant 500 MB file

```python
# BAD — exceeds 150 MB limit, job creation fails immediately
with open('huge_export.csv', 'rb') as f:
    requests.put(f"{base}/{job_id}/batches", data=f)
```

### Correct: Chunking files at 90 MB boundaries

```python
# GOOD — split files at ~90 MB (safe margin below 150 MB base64 limit)
import csv
import os

def chunk_csv(input_path, max_bytes=90_000_000):
    """Split CSV into chunks, each under max_bytes. Preserves header row."""
    chunks = []
    current_chunk = []
    current_size = 0
    with open(input_path, 'r', encoding='utf-8-sig') as f:
        reader = csv.reader(f)
        header = next(reader)
        header_line = ','.join(header) + '\n'
        for row in reader:
            line = ','.join(row) + '\n'
            if current_size + len(line.encode()) > max_bytes and current_chunk:
                chunks.append(header_line + ''.join(current_chunk))
                current_chunk = []
                current_size = 0
            current_chunk.append(line)
            current_size += len(line.encode())
    if current_chunk:
        chunks.append(header_line + ''.join(current_chunk))
    return chunks  # Submit each chunk as a separate job
```

### Wrong: Ignoring failed records after JobComplete

```python
# BAD — assumes JobComplete means 100% success
status = check_status(job_id)
if status['state'] == 'JobComplete':
    print("All done!")  # Could have thousands of failed records
```

### Correct: Always check failed record count and retrieve details

```python
# GOOD — explicitly handle partial success
status = check_status(job_id)
if status['state'] == 'JobComplete':
    failed = status.get('numberRecordsFailed', 0)
    processed = status.get('numberRecordsProcessed', 0)
    print(f"Processed: {processed} | Failed: {failed}")
    if failed > 0:
        failed_csv = get_failed_results(job_id)
        save_for_retry(failed_csv)  # Log and schedule retry
        alert_team(f"Bulk job {job_id}: {failed}/{processed} records failed")
```

## Common Pitfalls

- **Sandbox limits differ from production**: Sandbox orgs may have lower API call limits and less storage. A bulk load that works in sandbox may hit limits in production due to different trigger/flow configurations. Fix: `Load-test against a full-copy sandbox with production data volumes and automation enabled`. [src2]
- **Not pinning API version**: Using `/services/data/v62.0/` is correct; using `/services/data/latest/` does not exist. If you upgrade your org, old API versions still work but may have different behavior. Fix: `Pin API version in your integration configuration; test new versions before upgrading`. [src1]
- **Numbers in CSV with thousands separators**: `1,234` in a CSV will be parsed as two columns by the comma delimiter. Fix: `Never use thousands separators in CSV — format as 1234; or use PIPE delimiter if commas are unavoidable`. [src1]
- **Ignoring Sforce-Limit-Info header**: Every Bulk API response includes a `Sforce-Limit-Info` header showing remaining API calls. Ignoring this means you learn about limit exhaustion only when calls start failing with 403. Fix: `Parse and log Sforce-Limit-Info on every response; alert when under 20% remaining`. [src2]
- **Creating too many jobs without cleanup**: The 100,000 job limit is per-org. Old completed/aborted jobs count against this. Fix: `DELETE completed jobs after retrieving results; implement a job cleanup routine`. [src2]
- **Using insert when you need upsert**: If the same CSV is accidentally submitted twice, insert creates duplicate records. Fix: `Always define an external ID field and use upsert for idempotent operations`. [src1]

## Diagnostic Commands

```bash
# 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?isPkChunkingEnabled=false" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | python3 -c "import sys,json; [print(f'{j[\"id\"]} | {j[\"state\"]} | {j[\"object\"]} | {j[\"operation\"]}') for j in json.load(sys.stdin)['records']]"

# Check specific job status with detailed counts
curl -s ${INSTANCE_URL}/services/data/v62.0/jobs/ingest/${JOB_ID} \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | python3 -m json.tool

# Test authentication (quick sanity check)
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 (verify field API names before upload)
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} {\"writable\" if f[\"createable\"] else \"read-only\"}') for f in json.load(sys.stdin)['fields']]"

# Monitor query job status
curl -s ${INSTANCE_URL}/services/data/v62.0/jobs/query/${QUERY_JOB_ID} \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | python3 -m json.tool

# 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 | — |
| v59.0 | Spring '25 (Feb 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 to Bulk API 2.0 | Previously ingest-only |
| v41.0 | Winter '18 (Oct 2017) | Minimum for Bulk API 2.0 | Bulk API 2.0 initial GA release | Ingest operations only |

[src1, src3]

### Deprecation Policy

Salesforce supports API versions for a minimum of 3 years after release. Versions are retired in groups — typically 10+ versions at once. Salesforce announces retirements at least 1 year in advance via release notes and Trust notifications. As of Spring '26, no Bulk API 2.0-era versions have been retired. Always check the [Salesforce API Version Status](https://help.salesforce.com/s/articleView?id=000381744&type=1) page before planning upgrades. [src1]

## 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 for audit (queryAll) | You need real-time change notifications | Streaming API / Change Data Capture |
| Idempotent bulk upserts via external ID | You need transactional all-or-nothing behavior | Composite API (all-or-nothing mode) |

## Important Caveats

- Limits documented here reflect API v62.0 (Spring '26). Salesforce may adjust limits with each release — always verify against the current [Limits Quick Reference](https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_bulkapi.htm). [src2]
- The 100 million records/24h limit is a rolling window, not a calendar-day reset. If you load 80M records at 11 PM, you only have 20M capacity until 11 PM the next day. [src2]
- Sandbox orgs have independent limits that are often lower than production. Full-copy sandboxes inherit production's data volume but may have reduced API limits. [src2]
- Professional edition has limited Bulk API access — verify your specific edition's capabilities before architecting a solution. [src2]
- Bulk API 2.0 does not support PK Chunking for ingest jobs (only Bulk API 1.0 does). For query jobs, Bulk API 2.0 auto-chunks results without PK Chunking. [src1]
- All times and dates in the Bulk API are UTC. The `createdDate` and `systemModstamp` fields on job objects are always UTC regardless of user timezone settings. [src1]

## Related Units

- [Salesforce REST API Capabilities](/business/erp-integration/salesforce-rest-api-capabilities/2026) — real-time individual record operations
- [Salesforce Bulk API 1.0](/business/erp-integration/salesforce-bulk-api-v1-capabilities/2026) — serial mode, XML support, manual batch control
- [Salesforce Platform Events & CDC](/business/erp-integration/salesforce-platform-events-cdc/2026) — event-driven real-time integrations
