This card covers all four Salesforce REST API composite resource types: Composite, Composite Graph, Composite Batch, and sObject Collections/Tree. These resources allow combining multiple REST API operations into a single HTTP call, reducing round trips and API call consumption. Available in API v43.0+ for sObject Collections and v50.0+ for Composite Graph. All editions that support REST API support composite resources, but API call daily limits vary significantly by edition.
| Property | Value |
|---|---|
| Vendor | Salesforce |
| System | Salesforce Platform (Spring '26) |
| API Surface | REST — Composite Resources |
| Current API Version | v66.0 (Spring '26) |
| Editions Covered | Enterprise, Unlimited, Developer, Performance |
| Deployment | Cloud |
| API Docs | REST API Composite Resources |
| Status | GA (all four resource types) |
Salesforce provides four distinct composite resource types, each optimized for different integration patterns. The key differentiator is whether subrequests can reference each other's outputs and whether failures trigger full rollback.
| API Surface | Protocol | Best For | Max Subrequests | Reference IDs? | All-or-None? | Transactional? |
|---|---|---|---|---|---|---|
| Composite | HTTPS/JSON | Multi-step workflows needing cross-reference | 25 (max 5 queries/collections) | Yes | Optional (allOrNone param) | Yes, when allOrNone=true |
| Composite Graph | HTTPS/JSON | Large-scale related record creation; complex dependency graphs | 500 per graph | Yes (per graph only) | Implicit per graph | Yes, per graph |
| Composite Batch | HTTPS/JSON | Independent parallel operations (no dependencies) | 25 | No | No — each runs independently | No |
| sObject Collections | HTTPS/JSON | Bulk DML on single object type | 200 records | N/A | Optional (allOrNone param) | Yes, when allOrNone=true |
| sObject Tree | HTTPS/JSON | Parent-child record hierarchies | 200 records total | N/A (implicit parent-child) | Always all-or-nothing | Yes |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max subrequests (Composite) | 25 | Composite API | Max 5 can be query or sObject Collections operations |
| Max subrequests (Composite Graph) | 500 per graph | Composite Graph | Can have multiple graphs; 500 total across all graphs |
| Max subrequests (Composite Batch) | 25 | Composite Batch | Each subrequest executes independently |
| Max records (sObject Collections) | 200 | Collections create/update/delete/upsert | Per single Collections call |
| Max records (sObject Tree) | 200 total | sObject Tree | Max 5 levels deep; max 5 sObject types |
| Max query result size | 2,000 records | SOQL via Composite | Use queryMore for pagination |
| Max request body size | 50 MB | All REST API | Applies to entire composite payload |
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| API calls | 100,000 base | 24h rolling | Enterprise: 100K + (users x 1,000); Unlimited: 5M; Developer: 15K |
| Concurrent long-running requests | 25 | Per org | Applies to requests >20s |
| Composite call consumption | 1 API call per composite request | Per request | All subrequests count as 1 call against daily limit |
Governor limits apply cumulatively across all subrequests within a single composite call. This is the most common source of composite API failures.
| Limit Type | Per-Transaction Value | Notes |
|---|---|---|
| SOQL queries | 100 (sync) / 200 (async) | Shared across all subrequests — triggers consume from same pool |
| DML statements | 150 | Each insert/update/delete counts as 1 |
| Records retrieved by SOQL | 50,000 | Total across all queries in the transaction |
| Records processed by DML | 10,000 | Total across all DML operations |
| CPU time | 10,000 ms (sync) / 60,000 ms (async) | Exceeded = entire transaction aborted |
| Heap size | 6 MB (sync) / 12 MB (async) | Monitor when handling large payloads |
| Callouts | 100 | If subrequests trigger Apex with callouts |
All composite resources use the same authentication as standard Salesforce REST API. No special scopes or permissions required beyond standard API access.
| 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 | Access: 2h, Refresh: until revoked | Yes | Requires callback URL |
| OAuth 2.0 Client Credentials | Server-to-server (Spring '23+) | Access: 2h | No, request new token | Simpler than JWT |
START — Need to combine multiple Salesforce REST operations into one call
├── Do subrequests need to reference each other's output?
│ ├── YES — need reference IDs
│ │ ├── Need ≤25 subrequests? → Composite API (allOrNone=true for atomicity)
│ │ └── Need >25 subrequests? → Composite Graph API (up to 500 per graph)
│ └── NO — subrequests are independent
│ ├── All DML on same object type? → sObject Collections (200 max)
│ ├── Creating parent-child hierarchies? → sObject Tree (200 max)
│ └── Mixed independent operations? → Composite Batch (25 max)
├── Volume check
│ ├── ≤200 records of same type → sObject Collections
│ ├── ≤200 records with parent-child → sObject Tree
│ ├── ≤25 mixed operations with refs → Composite API
│ ├── ≤500 operations with refs → Composite Graph
│ └── >500 records → Bulk API 2.0
└── Error handling preference
├── All-or-nothing required → Composite (allOrNone=true), Graph, or Tree
├── Partial success OK → Batch or Collections (allOrNone=false)
└── Per-group atomicity → Composite Graph (each graph is atomic)
| Operation | Resource | Method | Endpoint | Max Records | Ref IDs? | Atomicity |
|---|---|---|---|---|---|---|
| Multi-step workflow | Composite | POST | /services/data/v66.0/composite | 25 subrequests | Yes | Optional |
| Large dependency graph | Composite Graph | POST | /services/data/v66.0/composite/graph | 500/graph | Yes (per graph) | Per graph |
| Independent parallel ops | Composite Batch | POST | /services/data/v66.0/composite/batch | 25 subrequests | No | None |
| Bulk create (single type) | sObject Collections | POST | /services/data/v66.0/composite/sobjects | 200 records | N/A | Optional |
| Bulk update (single type) | sObject Collections | PATCH | /services/data/v66.0/composite/sobjects | 200 records | N/A | Optional |
| Bulk delete (single type) | sObject Collections | DELETE | /services/data/v66.0/composite/sobjects?ids=... | 200 records | N/A | Optional |
| Parent-child tree | sObject Tree | POST | /services/data/v66.0/composite/tree/{sObject} | 200 total | N/A | Always |
Use OAuth 2.0 JWT bearer flow for server-to-server integrations. [src1]
# Input: Connected App consumer key, private key, username
# Output: Access token and instance URL
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=$(generate_jwt_assertion)"
Verify: Response contains access_token and instance_url fields. HTTP 200 = success.
Create an Account and linked Contact in a single atomic call. [src1, src4]
curl -X POST https://{instance_url}/services/data/v66.0/composite \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"allOrNone": true,
"collateSubrequests": true,
"compositeRequest": [
{
"method": "POST",
"url": "/services/data/v66.0/sobjects/Account",
"referenceId": "refAccount",
"body": {"Name": "Acme Corp", "Industry": "Technology"}
},
{
"method": "POST",
"url": "/services/data/v66.0/sobjects/Contact",
"referenceId": "refContact",
"body": {
"FirstName": "Jane", "LastName": "Doe",
"AccountId": "@{refAccount.id}"
}
}
]
}'
Verify: Response HTTP 200 with compositeResponse array. Each element has httpStatusCode: 200.
Use Composite Graph for >25 subrequests or multiple independent transactional groups. [src2, src3]
curl -X POST https://{instance_url}/services/data/v66.0/composite/graph \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"graphs": [
{
"graphId": "graph1",
"compositeRequest": [
{"method": "POST", "url": "/services/data/v66.0/sobjects/Account",
"referenceId": "ref1Acct", "body": {"Name": "Graph1 Corp"}},
{"method": "POST", "url": "/services/data/v66.0/sobjects/Contact",
"referenceId": "ref1Cont", "body": {"LastName": "Smith", "AccountId": "@{ref1Acct.id}"}}
]
},
{
"graphId": "graph2",
"compositeRequest": [
{"method": "POST", "url": "/services/data/v66.0/sobjects/Account",
"referenceId": "ref2Acct", "body": {"Name": "Graph2 Inc"}}
]
}
]
}'
Verify: Response graphs array. Each graph has isSuccessful: true/false. If graph1 fails, graph2 can still succeed.
Batch create up to 200 records of the same sObject type. [src5]
curl -X POST https://{instance_url}/services/data/v66.0/composite/sobjects \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"allOrNone": false,
"records": [
{"attributes": {"type": "Contact"}, "FirstName": "Alice", "LastName": "Johnson"},
{"attributes": {"type": "Contact"}, "FirstName": "Bob", "LastName": "Williams"}
]
}'
Verify: Response is array of SaveResult: [{"id": "003xx...", "success": true, "errors": []}, ...]
# Input: Salesforce access token, instance URL
# Output: Created Account ID and linked Contact ID
import requests # requests>=2.31.0
def composite_create(instance_url, access_token):
"""Create Account + Contact atomically via Composite API."""
url = f"{instance_url}/services/data/v66.0/composite"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
payload = {
"allOrNone": True,
"collateSubrequests": True,
"compositeRequest": [
{
"method": "POST",
"url": "/services/data/v66.0/sobjects/Account",
"referenceId": "refAccount",
"body": {"Name": "Acme Corp"}
},
{
"method": "POST",
"url": "/services/data/v66.0/sobjects/Contact",
"referenceId": "refContact",
"body": {
"LastName": "Doe",
"AccountId": "@{refAccount.id}"
}
}
]
}
resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()
results = resp.json()["compositeResponse"]
return {r["referenceId"]: r["body"]["id"] for r in results}
// Input: jsforce connection, array of records, external ID field
// Output: Array of SaveResult objects
const jsforce = require("jsforce"); // [email protected]
async function bulkCollectionsUpsert(conn, records, externalIdField) {
const CHUNK_SIZE = 200; // sObject Collections max
const results = [];
for (let i = 0; i < records.length; i += CHUNK_SIZE) {
const chunk = records.slice(i, i + CHUNK_SIZE);
const res = await conn.requestPost(
`/services/data/v66.0/composite/sobjects/${externalIdField}`,
{ allOrNone: false, records: chunk }
);
results.push(...res);
}
return results; // [{id, success, errors, created}, ...]
}
# Input: Access token, instance URL
# Output: Array of independent results
curl -X POST https://{instance_url}/services/data/v66.0/composite/batch \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"batchRequests": [
{"method": "GET", "url": "v66.0/sobjects/Account/describe"},
{"method": "GET", "url": "v66.0/query?q=SELECT+Id,Name+FROM+Account+LIMIT+5"}
]
}'
# Response: {"hasErrors": false, "results": [{statusCode: 200, result: {...}}, ...]}
| Pattern | Purpose | Example | Notes |
|---|---|---|---|
@{referenceId.id} | Use created record's ID in later subrequest | "AccountId": "@{refAccount.id}" | Most common — link child to parent |
@{referenceId.field} | Access any field from response body | @{refQuery.records[0].Id} | Works with query results |
referenceId in URL | Dynamic URL construction | /sobjects/Account/@{refAccount.id} | For PATCH/DELETE after create |
@{refId.field} notation uses JavaScript-like dot syntax but does NOT support complex expressions or transformations. [src6]collateSubrequests=true, the API may reorder independent subrequests to bulkify DML — do not rely on execution order for non-dependent subrequests. [src1]allOrNone=false returns HTTP 200 even if individual records failed — check each SaveResult's success field. [src4]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 400 | Bad Request | Malformed JSON, invalid reference ID | Validate payload structure; check referenceId names |
| 401 | Unauthorized | Expired or invalid access token | Refresh token or re-authenticate |
| PROCESSING_HALTED | Subrequest skipped | allOrNone=true and earlier subrequest failed | Fix the failing subrequest |
| INVALID_FIELD | Field doesn't exist or isn't writable | Wrong API name or insufficient FLS | Check via /sobjects/{Object}/describe |
| UNABLE_TO_LOCK_ROW | Record locked | Concurrent updates to same record | Retry with random jitter (100-500ms) |
| LIMIT_EXCEEDED | Governor limit breached | Cumulative SOQL/DML/CPU exceeded | Reduce subrequest count or split requests |
| DUPLICATE_VALUE | Unique constraint violated | Duplicate external ID | Use upsert; implement idempotency |
audit triggers with Limits class; reduce SOQL per trigger. [src5]pre-validate data client-side before sending composite request. [src4]design each graph as self-contained unit. [src2]flatten hierarchy or use Composite API with reference IDs. [src6]use explicit reference IDs; set collateSubrequests=false if order matters. [src1]ensure session timeout ≥30 min; use idempotent operations. [src2]# BAD — 25 API calls consumed, 25 round trips
for record in records[:25]:
requests.post(f"{url}/sobjects/Contact", json=record, headers=headers)
# GOOD — 1 API call consumed, 1 round trip
payload = {
"allOrNone": False,
"records": [{"attributes": {"type": "Contact"}, **r} for r in records[:200]]
}
requests.post(f"{url}/composite/sobjects", json=payload, headers=headers)
# BAD — Composite Batch has no allOrNone or reference IDs
# If subrequest 2 fails, subrequest 1 is already committed
payload = {"batchRequests": [
{"method": "POST", "url": "v66.0/sobjects/Account", "richInput": {"Name": "Acme"}},
{"method": "POST", "url": "v66.0/sobjects/Contact", "richInput": {"LastName": "Doe"}}
]}
# GOOD — allOrNone=true ensures both succeed or both roll back
payload = {
"allOrNone": True,
"compositeRequest": [
{"method": "POST", "url": "/services/data/v66.0/sobjects/Account",
"referenceId": "refAcct", "body": {"Name": "Acme"}},
{"method": "POST", "url": "/services/data/v66.0/sobjects/Contact",
"referenceId": "refCont", "body": {"LastName": "Doe", "AccountId": "@{refAcct.id}"}}
]
}
# BAD — Composite allows max 5 query/collection subrequests
payload = {"compositeRequest": [
{"method": "GET", "url": f"/services/data/v66.0/query?q=SELECT...", "referenceId": f"q{i}"}
for i in range(10) # 10 queries exceeds the 5-query cap
]}
# GOOD — Composite Batch has no query-count restriction within 25 subrequests
payload = {"batchRequests": [
{"method": "GET", "url": f"v66.0/query?q=SELECT...{table}"}
for table in tables[:25]
]}
success field. [src4]# Check remaining API limits for this org
curl -H "Authorization: Bearer {token}" \
https://{instance_url}/services/data/v66.0/limits
# Verify composite resource availability
curl -H "Authorization: Bearer {token}" \
https://{instance_url}/services/data/v66.0/
# Describe an sObject to check field API names
curl -H "Authorization: Bearer {token}" \
https://{instance_url}/services/data/v66.0/sobjects/Account/describe
# Test a minimal composite batch request
curl -X POST -H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
https://{instance_url}/services/data/v66.0/composite/batch \
-d '{"batchRequests":[{"method":"GET","url":"v66.0/limits"}]}'
# Check available API versions
curl https://{instance_url}/services/data/
| API Version | Release | Status | Key Composite Features | Notes |
|---|---|---|---|---|
| v66.0 | Spring '26 (Feb 2026) | Current | No new composite features | Legacy hostname redirects removed |
| v63.0 | Spring '25 (Feb 2025) | Supported | None | — |
| v50.0 | Winter '21 (Oct 2020) | Supported | Composite Graph API introduced | 500 subrequests per graph |
| v43.0 | Summer '18 (Jun 2018) | Supported | sObject Collections introduced | 200-record DML |
| v38.0 | Winter '17 (Oct 2016) | Supported | Composite API introduced | 25 subrequests with reference IDs |
| v34.0 | Summer '15 (Jun 2015) | Supported | Composite Batch introduced | 25 independent subrequests |
| v30.0 and below | Pre-2014 | Retired (Jun 2025) | N/A | Minimum version: v31.0 |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Creating related records atomically (parent + children) | Bulk data migration (>1,000 records) | Bulk API 2.0 |
| Need to reference output of one operation in a subsequent operation | Operations are completely independent | Composite Batch |
| Reducing API call consumption (25 ops = 1 API call) | Processing >500 related records | Bulk API 2.0 with external IDs |
| Building multi-step workflows (query, transform, upsert) | Simple single-object CRUD on one record | Standard REST API |
| Creating up to 200 records of same type efficiently | Need real-time event notifications | Platform Events / CDC |
collateSubrequests parameter only affects DML ordering optimization — it does not change allOrNone semantics.