SfSkills Domains Docs GitHub

Home / integration / composite-api-patterns

composite-api-patterns

Use when designing, implementing, or troubleshooting Salesforce Composite API requests — covering the /composite/ resource with cross-subrequest referenceId wiring, /composite/sobjects/ bulk CRUD, /composite/tree/ hierarchical inserts, and /composite/batch/ independent subrequest bundling.

Open the skill folder on GitHub


Composite API Patterns

Use this skill when an integration needs to group multiple Salesforce REST API operations into a single HTTP round trip. The Composite family of resources — /composite/, /composite/sobjects/, /composite/tree/, and /composite/batch/ — each serve distinct use cases and carry distinct limits. Choosing the wrong resource wastes API calls, violates atomicity requirements, or silently drops errors.

This skill focuses on the four Composite resources, their request/response shapes, limit boundaries, dependency management via referenceId, and the allOrNone flag behavior. It does not cover single-resource REST CRUD, SOQL pagination, Bulk API 2.0, or GraphQL.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The Four Composite Resources

Salesforce exposes four distinct resources under the /composite/ path. They are not interchangeable.

/composite/ — Ordered, dependent subrequests

POST /services/data/v63.0/composite

Executes up to 25 subrequests in sequence within a single HTTP call. The key differentiator: results from earlier subrequests can be referenced in later subrequests using @{referenceId.fieldName} syntax. Salesforce resolves references server-side before executing the dependent subrequest. Supports allOrNone: true (atomic rollback on any failure) or allOrNone: false (partial success, each result independent).

/composite/sobjects/ — Bulk CRUD on same-type records

POST   /services/data/v63.0/composite/sobjects          (create up to 200)
PATCH  /services/data/v63.0/composite/sobjects          (update up to 200)
DELETE /services/data/v63.0/composite/sobjects?ids=...  (delete up to 200)
GET    /services/data/v63.0/composite/sobjects?ids=...  (read up to 2000)

The sObject Collection resource processes up to 200 records of a uniform type in a single call. Records are not required to be the same object type for reads, but DML operations (create, update, delete) require uniform type. Supports allOrNone flag. No cross-record referencing.

/composite/tree/{SObject}/ — Hierarchical parent-child inserts

POST /services/data/v63.0/composite/tree/Account/

Inserts a parent record with its related child records in one atomic call. Up to 200 total records across up to 5 nesting levels. Always atomic — no partial success. All records in the tree use temporary referenceId labels for response correlation, but child records reference parents via the attributes.referenceId + nested structure, not the @{} syntax used in /composite/.

/composite/batch/ — Bundled independent subrequests

POST /services/data/v63.0/composite/batch

Executes up to 25 subrequests that cannot reference each other. Each subrequest is a complete, standalone REST call. Results are independent. The haltOnError flag stops processing on the first failure but does not roll back completed subrequests. Use when you want to reduce round trips for logically unrelated operations.

referenceId and Cross-Subrequest Dependency

In /composite/ requests, each subrequest carries a referenceId string. Later subrequests in the same call can embed @{referenceId.fieldPath} anywhere in their request body or URL. Field paths follow dot notation for nested fields.

"body": {
  "AccountId": "@{NewAccount.id}"
}

referenceId values are case-sensitive. A typo in the reference token causes the subrequest to receive a literal string rather than the resolved value, which typically results in a validation error that is reported in the per-subrequest response body — not in the outer HTTP status.

References can also appear in URL segments for GET subrequests:

"url": "/services/data/v63.0/sobjects/Contact/@{NewContact.id}"

allOrNone Flag and Error Behavior

The allOrNone flag controls atomicity:

For /composite/tree/, atomicity is always enforced — there is no allOrNone option. One invalid record rolls back the entire tree.

Governor Limits and API Call Accounting

The entire Composite request counts as one API call toward the org's daily REST API limit. However, each subrequest's DML operations count individually against DML governor limits and row limits. Key limits to track:


Common Patterns

When to use: You need to create a parent Account and one or more child Contacts and/or Opportunities in a single transaction without knowing the Account ID in advance and without separate round trips.

How it works: Use /composite/ with allOrNone: true. Assign a referenceId to the Account subrequest and reference @{refId.id} in each child subrequest's relationship field. Order subrequests so the parent appears before any child that references it.

{
  "allOrNone": true,
  "compositeRequest": [
    {
      "method": "POST",
      "url": "/services/data/v63.0/sobjects/Account/",
      "referenceId": "NewAccount",
      "body": { "Name": "Acme Corp" }
    },
    {
      "method": "POST",
      "url": "/services/data/v63.0/sobjects/Contact/",
      "referenceId": "NewContact",
      "body": {
        "LastName": "Smith",
        "FirstName": "Jane",
        "AccountId": "@{NewAccount.id}"
      }
    },
    {
      "method": "POST",
      "url": "/services/data/v63.0/sobjects/Opportunity/",
      "referenceId": "NewOpportunity",
      "body": {
        "Name": "Acme Corp — Q3 Deal",
        "AccountId": "@{NewAccount.id}",
        "StageName": "Prospecting",
        "CloseDate": "2025-12-31"
      }
    }
  ]
}

Why not the alternative: Three separate REST calls require the caller to store the Account ID between calls, manage partial failure compensation (Account inserted, Opportunity failed — now you have an orphaned Account), and make three HTTP round trips. The composite approach is atomic and uses one API call.

Pattern 2: Bulk Upsert Same-Type Records

When to use: An external system sends a batch of 200 or fewer records of the same object type that need to be created or updated in Salesforce. Record volume is too large for individual calls but too small to warrant Bulk API 2.0 async job overhead.

How it works: Use /composite/sobjects/ with a PATCH request and an externalIdFieldName for upsert semantics. All records in the request body go into records[]. Set allOrNone: false to allow partial success, then inspect per-record results.

PATCH /services/data/v63.0/composite/sobjects/Contact/External_Id__c
{
  "allOrNone": false,
  "records": [
    {
      "attributes": { "type": "Contact" },
      "External_Id__c": "EXT-001",
      "LastName": "Adams",
      "Email": "adams@example.com"
    },
    {
      "attributes": { "type": "Contact" },
      "External_Id__c": "EXT-002",
      "LastName": "Brown",
      "Email": "brown@example.com"
    }
  ]
}

Why not the alternative: Individual upsert calls against /sobjects/{SObject}/{ExternalIdField}/{value} for 150 records = 150 HTTP round trips and 150 API calls against the daily limit. The sObject Collection reduces this to 1 API call and 1 round trip.

Pattern 3: Hierarchical Account-Contact Tree Insert

When to use: You need to insert a set of Accounts each with their own Contacts, and the parent Account IDs are not known in advance. The total record count is 200 or fewer and the tree is 5 levels or fewer deep.

How it works: Use /composite/tree/Account/. Each Account in the records[] array can include a Contacts key whose records[] hold the child Contact objects. Assign a referenceId to each record for response correlation.

POST /services/data/v63.0/composite/tree/Account/
{
  "records": [
    {
      "attributes": { "type": "Account", "referenceId": "AcctRef1" },
      "Name": "Globex Corp",
      "Contacts": {
        "records": [
          {
            "attributes": { "type": "Contact", "referenceId": "ContRef1" },
            "LastName": "Simpson",
            "FirstName": "Homer"
          }
        ]
      }
    }
  ]
}

Why not the alternative: Using /composite/ with multiple parent-then-child subrequests is limited to 25 subrequests total. sObject Tree supports up to 200 records across the entire hierarchy in one call, with a cleaner request shape for deeply nested trees.


Decision Guidance

SituationRecommended ApproachReason
Create parent + children, IDs unknown, must be atomic/composite/ with allOrNone: true and referenceIdOnly resource with cross-subrequest ID wiring
Insert hierarchical parent-child tree (≤ 200 records, ≤ 5 levels)/composite/tree/{SObject}/Cleaner shape for trees, one atomic call
Bulk create/update/upsert same object type (≤ 200 records)/composite/sobjects/Highest record throughput per API call
Bundle independent requests to reduce round trips/composite/batch/No ordering dependency, simpler response handling
Mixed object type bulk DML with dependencies/composite/ (up to 25 subrequests)Only option for cross-object, cross-reference operations
Volume > 200 records per operationBulk API 2.0Composite resources are not designed for large volumes
Single record CRUD/sobjects/{SObject}/No overhead; Composite adds complexity without benefit

  1. Clarify the dependency structure — Determine whether any subrequest needs a result (typically an ID) from a preceding subrequest. If yes, only /composite/ supports this via @{referenceId.field}.
  2. Select the correct Composite resource — Apply the Decision Guidance table above. Confirm record count and nesting depth are within resource limits.
  3. Design the atomicity strategy — Choose allOrNone: true for all-or-nothing transactions, or allOrNone: false and build per-subrequest error handling into the caller.
  4. Build and validate the request body — Confirm all referenceId values are unique, case-correct, and referenced before they are needed. Verify all record types, required fields, and field API names.
  5. Implement response parsing — Do not rely on the outer HTTP status. Always iterate compositeResponse[] or per-record result arrays and inspect each httpStatusCode.
  6. Assess governor impact — Calculate DML rows, CPU time, and API call count. Confirm the pattern fits within org limits before testing at scale.
  7. Run the checker script — Execute scripts/check_composite_api.py against integration files to catch common structural issues before code review.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. Outer HTTP 200 does not mean success — All four Composite resources return HTTP 200 on the outer response even when individual subrequests or records fail. Any integration that checks only the outer status code silently discards errors. Always parse compositeResponse[].httpStatusCode or per-record result arrays.
  2. referenceId is case-sensitive — If a subrequest sets "referenceId": "NewAccount" but a later subrequest references @{newaccount.id} (lowercase), Salesforce does not resolve the reference. The literal string @{newaccount.id} is passed as the field value, which then fails validation with a confusing error message.
  3. allOrNone: true rolls back ALL subrequests, including successful ones — When any single subrequest fails with allOrNone: true, Salesforce rolls back every subrequest in the call, including those that had already succeeded. Rolled-back subrequests report httpStatusCode: 400 and errorCode: PROCESSING_HALTED in their individual response — not a 2xx. This can be misread as a failure of those subrequests rather than a rollback.
  4. DML governor limits are not waived per subrequest — 25 composite subrequests, each triggering 200 DML rows, equals 5,000 DML rows in a single transaction. Org-wide DML row limits (150 DML statements, unlimited rows but SOQL and CPU caps) can still be hit. Apex triggers, validation rules, and workflow rules fired by composite subrequests count against the same governor limits as any other transaction.
  5. sObject Tree is always atomic — partial success is not available — Unlike /composite/ with allOrNone: false, the tree resource has no partial-success mode. One invalid record in a 200-record tree rolls back the entire payload. Validate records client-side (required fields, field lengths, relationship IDs) before sending large trees.

Output Artifacts

ArtifactDescription
Composite resource selection guideWhich resource (composite/sobjects/tree/batch) fits the use case and why
JSON request body scaffoldComplete request with subrequests, referenceIds, allOrNone, and record arrays
Response parsing logicPer-subrequest iteration pattern with httpStatusCode inspection
Governor limit estimateDML rows, API calls, and subrequest count projection
Integration review findingsAssessment of an existing Composite API implementation against this skill

Package files