While Bulk API 2.0 handles the heavy lifting of record-level data transfer, the Salesforce REST API plays a complementary role throughout the migration lifecycle: pre-migration schema queries, post-migration validation, real-time status checks, and low-volume supplementary data loads where the Bulk API’s asynchronous model would be operationally inconvenient.
This article covers three REST API patterns that belong in every migration: the Composite API for atomic parent-child inserts, the SObject Tree API for full relationship hierarchies, and the SOQL Query API for pre- and post-migration validation. None of these replaces Bulk API 2.0. Each one solves a problem that Bulk API cannot.
Table of Contents
Why Bulk API 2.0 Isn’t Enough on Its Own
Bulk API 2.0 is asynchronous by design. You upload a CSV file, mark the job as UploadComplete, and poll until it processes. That model is perfect when you’re loading 500,000 Account records with no cross-object dependencies. It creates real headaches when records need each other.
Take a common scenario: migrating Accounts and their associated Contacts. With Bulk API, the typical approach is to run the Account job first, wait for it to complete, parse the successfulResults CSV to extract the newly assigned Salesforce IDs, map those IDs back to your Contact rows, then run the Contact job. That sequence works — but it’s slow, it requires state management between jobs, and any failure in the mapping step creates orphaned Contacts with no AccountId.
The Composite REST API exists for exactly this situation.
Composite REST API: Orchestrating Multi-Object Inserts
The Composite REST API allows a client to submit up to 25 subrequests in a single HTTP call, with each subrequest able to reference the result of a previous subrequest using a reference ID syntax. This capability is invaluable for migrating parent-child record hierarchies (e.g., Account, then Contact, then Case) where the child record’s parent ID is not known until the parent record has been inserted.
// Composite REST API: Migrating Account + Contact in a single round-trip
POST /services/data/v59.0/composite
// Request Body (JSON):
{
"allOrNone": true,
"compositeRequest": [
{
"method" : "POST",
"url" : "/services/data/v59.0/sobjects/Account",
"referenceId" : "NewAccount",
"body" : {
"Name" : "Acme Corporation",
"Industry" : "Technology",
"External_ID__c" : "EXT-ACC-00456"
}
},
{
"method" : "POST",
"url" : "/services/data/v59.0/sobjects/Contact",
"referenceId" : "NewContact",
"body" : {
"LastName" : "Doe",
"FirstName" : "Jane",
"Email" : "jane.doe@acme.com",
"AccountId" : "@{NewAccount.id}",
"External_ID__c" : "EXT-CON-00789"
}
}
]
}
// @{NewAccount.id} resolves to the Salesforce ID of the just-created Account
// allOrNone: true means both records rollback if either fails
When to choose Composite over Bulk API: If you’re migrating a few hundred records with tight parent-child dependencies and atomic insert requirements, Composite is the right tool. Once you’re above a few thousand records, the overhead of 25-subrequest batches becomes a bottleneck, and you should split the objects into separate Bulk API jobs with the dependency ordering handled at the orchestration layer.
SObject Tree API: Hierarchical Data in One Call
The SObject Tree API extends the Composite concept specifically to object relationship trees, allowing you to insert a root record and all of its children in a single API call. This is particularly useful for migrating complex opportunity hierarchies or case structures with embedded comments.
// SObject Tree: Insert Account with multiple Contacts in one call
POST /services/data/v59.0/composite/tree/Account
{
"records": [
{
"attributes" : { "type": "Account", "referenceId": "AccRef1" },
"Name" : "Global Dynamics Ltd",
"External_ID__c" : "EXT-ACC-01001",
"Contacts" : {
"records": [
{
"attributes" : { "type": "Contact", "referenceId": "ConRef1" },
"LastName" : "Reynolds",
"Email" : "reynolds@globaldynamics.com",
"External_ID__c" : "EXT-CON-02001"
}
]
}
}
]
}
// Response: { hasErrors: false, results: [{ referenceId, id }] }
// Up to 200 records per SObject Tree request
SOQL Query API for Pre- and Post-Migration Validation
The SOQL Query API is easy to underestimate in a migration context. Most people think of it as a lookup mechanism, but it performs two of the most critical functions in the entire migration lifecycle: scoping your job sizes before anything moves, and proving your data landed correctly after it does.
# Pre-migration: Count source records per object to plan job sizing
GET /services/data/v59.0/query/
?q=SELECT+COUNT()+FROM+Account+WHERE+CreatedDate=LAST_N_YEARS:5
# Post-migration: Validate record counts match the source system
GET /services/data/v59.0/query/
?q=SELECT+COUNT()+FROM+Account+WHERE+External_ID__c!=null
# Cross-object validation: ensure all Contacts have parent Accounts
GET /services/data/v59.0/query/
?q=SELECT+COUNT()+FROM+Contact+WHERE+AccountId=null
+AND+External_ID__c!=null
# Monitor API limits in real time
GET /services/data/v59.0/limits/
# Returns: { DailyApiRequests: { Max: 5000000, Remaining: 4893221 }, ... }
Quick Reference: Key REST API Endpoints
| Operation | Method | Endpoint | Notes |
|---|---|---|---|
| Composite request | POST | /services/data/v59.0/composite | Up to 25 subrequests; supports allOrNone |
| SObject Tree insert | POST | /services/data/v59.0/composite/tree/{Object} | Up to 200 records; two nesting levels |
| SOQL query | GET | /services/data/v59.0/query/?q={SOQL} | URL-encode the query string |
| API limit check | GET | /services/data/v59.0/limits/ | Returns all governor limit categories |
| Object describe | GET | /services/data/v59.0/sobjects/{Object}/describe/ | Field metadata for mapping prep |
Putting It Together
The REST API plays a specific, non-negotiable role in a well-designed migration. It is not a fallback for when the Bulk API is unavailable. It handles the situations where Bulk API’s async model doesn’t fit: tight parent-child inserts that need to be atomic, full relationship hierarchies that should be written in one call, count queries that tell you what you’re dealing with before you start, and validation queries that determine whether your cutover gets a green light.
Think of Bulk API 2.0 as the freight operation that moves the volume. The REST API is the team doing the careful, relationship-aware work on either side of it — and the inspector who confirms everything arrived correctly before the doors open.
In the next article, we cover what happens when things go wrong during the migration: exponential back-off with jitter for retrying failed calls, the External ID upsert pattern that guarantees idempotency across retry cycles, the Dead-Letter Queue for capturing records that fail repeatedly, and the checkpoint-and-resume approach that makes long migration windows survivable.
Official reference: Salesforce Rest API Developer Guide
Reference Architecture Diagram
End-to-End Salesforce Data Migration Reference Architecture
+------------------------------------------------------------------------+
| SOURCE SYSTEM LAYER |
| +---------------+ +--------------+ +------------------------------+ |
| | Legacy CRM | | ERP System | | Flat File / Data Lake | |
| | (MSSQL/PG) | | (SAP/SF) | | (CSV / Parquet / JSON) | |
| +-------+-------+ +------+-------+ +-------------+----------------+ |
+----------+-----------------+-----------------------+-+------------------+
| | |
+--------+--------+-----------+-----------+
| |
v v
+------------------------------------------------------------------------+
| EXTRACTION AND TRANSFORMATION LAYER |
| +------------------------------------------------------------------+ |
| | Migration Orchestrator (Node.js / Python) | |
| | | |
| | Extract -> Cleanse -> Transform -> Map -> Validate -> Stage | |
| | JDBC Null-fill Field remap User map ExtID-key CSV | |
| +--+-----------------------------------+---+-----+---+-------------+ |
| | | | | |
| +--+-------------+ +------------------+-+ +--+---+----------+ |
| | Checkpoint Mgr | | Dead-Letter Queue | | Audit Logger | |
| | (resume state) | | (failed records) | | (structured) | |
| +----------------+ +--------------------+ +----------------+ |
+----------------------------------------+-------------------------------+
| OAuth 2.0 / Named Cred
v
+------------------------------------------------------------------------+
| SALESFORCE PLATFORM LAYER |
| +------------------+ +--------------+ +----------------------------+ |
| | Bulk API 2.0 | | REST API | | Composite / SObject Tree | |
| | (mass inserts) | | (validation) | | (hierarchy inserts) | |
| +---------+--------+ +------+-------+ +----------+-----------------+ |
| +------------------+-----------+----------+ |
| v |
| +--------------------------------------------------------------------+ |
| | Salesforce Database Layer | |
| | Account -> Contact -> Opportunity -> OpportunityLineItem | |
| | Case -> CaseComment -> ContentVersion (Files) | |
| +--------------------------------------------------------------------+ |
+------------------------------------------------------------------------+
Frequently Asked Questions (FAQ)
The Composite REST API lets you bundle up to 25 subrequests into a single HTTP POST. Each subrequest can reference the result of a previous one using a referenceId, so you can insert a parent record and immediately pass its new Salesforce ID to a child record, all in one call, no polling required.
Bulk API 2.0 is asynchronous and built for volume, the right choice when loading hundreds of thousands of records. The REST API is synchronous and built for precision: atomic parent-child inserts, hierarchical data loads, pre-migration count queries, and post-migration validation. The two complement each other; neither replaces the other.
When allOrNone is true, the entire Composite request rolls back if any single subrequest fails. You get clean atomic behavior — either everything commits or nothing does. Setting it to false allows partial commits, which can leave orphaned child records in your org that are much harder to track down than a clean failure.
The SObject Tree API is a specialised Composite variant built for relationship hierarchies. You define a root object, say, an Account, with nested child records in a single payload, and Salesforce inserts the entire structure in one call. It supports up to 200 records per request across two nesting levels.
The SObject Tree API supports up to 200 records per request, shared across the root object and all its children combined. It also supports a maximum of two nesting levels. An Account with 150 Contacts consumes 151 of that budget. Plan your batch sizes before the migration window, not during it.
The most reliable approach filters on External_ID__c != null in your COUNT() queries — that way you’re only counting records your migration loaded, not pre-existing data. For relationship integrity, check Contacts where AccountId = null AND External_ID__c != null. Any result above zero means orphaned records to fix before go-live.
The /limits/ endpoint returns your real-time consumption across every Salesforce governor limit — daily API requests remaining, Bulk API batch counts, concurrent request usage, and more. Polling it every 15 to 30 minutes during a long migration window gives you early warning before you exhaust a limit mid-run.
A referenceId is the label you assign to a subrequest in a Composite call. Other subrequests reference its output using the syntax @{referenceId.fieldName}. In practice, this lets you use a newly created Account’s Salesforce ID as the AccountId on a Contact in the same request — no separate query needed.
The key is combining an External ID field with the upsert endpoint. When you include an External_ID__c value on every record and route retries through the upsert endpoint rather than a plain insert, Salesforce matches on that External ID instead of creating a duplicate. This is the standard idempotency pattern for REST API migrations.
Yes, working with the REST API directly requires at least basic development experience. You need to construct HTTP requests, manage Bearer tokens, parse JSON responses, and handle errors. If you don’t have a developer on the project, tools like MuleSoft, Jitterbit, or Data Loader abstract most of this and are more practical options.

Kiran Sreeram Prathi
I’m Kiran Sreeram Prathi, a Salesforce Developer dedicated to building scalable, intelligent, and user-focused CRM solutions. Over the past five years, I’ve delivered Salesforce implementations across healthcare, finance, and service industries—focusing on both technical precision and user experience. My expertise spans Lightning Web Components (LWC), Apex, OmniStudio, and Experience Cloud, along with CI/CD automation using GitHub Actions and integrations with platforms such as DocuSign, Conga, and Zpaper. I take pride in transforming complex workflows into seamless digital journeys and implementing clean DevOps strategies that reduce downtime and accelerate delivery. Recognized by organizations like Novartis, WILCO, and Deloitte, I enjoy solving problems that make Salesforce work smarter and scale better. I’m always open to connecting with professionals who are passionate about process transformation, architecture design, and continuous innovation in the Salesforce ecosystem.
- This author does not have any more posts.

