SfSkills Domains Docs GitHub

Home / integration / rest-api-patterns

rest-api-patterns

Use when designing, implementing, or reviewing Salesforce REST API integration — covering CRUD operations on sObjects, SOQL-based queries, paginated result sets, Composite requests, Composite Batch, and sObject Tree. Triggers: 'Salesforce REST API', 'composite API', 'nextRecordsUrl', 'sObject Tree', 'REST CRUD', 'REST pagination', 'API limits', 'OAuth Bearer token'. NOT for loading or extracting large record volumes as a job — use integration/bulk-api-2-patterns. NOT for GraphQL queries or the GraphQL-vs-REST choice — use integration/graphql-api-patterns.

Open the skill folder on GitHub


REST API Patterns

Use this skill when the integration task involves calling the Salesforce REST API to read, create, update, or delete records, or when you need to compose multiple REST operations efficiently. This skill covers the standard sObject resources, SOQL query resources, the Composite family of resources, and the pagination and error handling patterns that hold an integration together in production.


Before Starting

Gather this context before working on anything in this domain:

Questions to Ask Before Configuring

QuestionWhy it mattersWhat a good answer addsWhat proper configuration adds over just doing it
"How many records per run, and how often?"REST suits interactive and modest volumes; Composite caps at 25 subrequests, sObject Tree at 200 records; larger loads belong to Bulk API 2.0The resource per volume bandAn integration that stays inside its allocation as data grows
"Which edition and how many licenses, and what else consumes API calls?"The 24-hour allocation is shared across all integrations in the orgA call budget per integrationNo midday lockout because another job used the allocation
"Must related writes succeed or fail together?"Composite allOrNone and sObject Tree are atomic; Composite Batch is notThe transaction boundary per operationPartial failures that match the business rule
"Does the source system own a stable key for each record?"External ID upsert avoids query-then-write races; a key that matches two records returns 300The External ID field and its uniquenessIdempotent retries without duplicates
"How will the client handle 401, 403 REQUEST_LIMIT_EXCEEDED, 410, and per-subrequest errors?"Each means a different action: re-authenticate, back off, upgrade the version, or fix one recordAn error-handling tableFailures that recover or alert instead of losing data
"Will any call run longer than 20 seconds?"More than 25 concurrent long-running calls (5 in Developer Edition) are refused; REST calls time out after 10 minutesA concurrency cap and a timeout planPolling and exports that do not starve each other

What a proper configuration adds over "just calling the endpoint": the resource matches the volume, the call budget is planned against the shared allocation, transaction boundaries are explicit, and every documented error has a defined response.


Core Concepts

Base URL and Versioning

All REST API resources are relative to the instance base URL and include the API version:

https://<instance>.salesforce.com/services/data/v63.0/

Always pin the version explicitly. Unversioned or pinned-to-old-version calls risk hitting deprecated behavior after a Salesforce release upgrade.

Authentication: OAuth Bearer Token

Every REST request must include the access token in the HTTP Authorization header:

Authorization: Bearer <access_token>

The token is obtained via an OAuth 2.0 flow (client credentials, JWT bearer, web server, or device). The username-password flow is blocked by default in orgs created Summer '23 or later; see integration/oauth-flows-and-connected-apps. The app must carry the appropriate OAuth scope (api or a more targeted scope). Tokens expire; integrations must handle 401 responses and refresh or re-authenticate.

sObject CRUD Resources

Standard create/read/update/delete operations target the /sobjects/ resource family:

OperationHTTP VerbEndpoint
Describe an sObjectGET/services/data/v63.0/sobjects/{SObject}/
Read by IDGET/services/data/v63.0/sobjects/{SObject}/{id}
Read by External IDGET/services/data/v63.0/sobjects/{SObject}/{ExternalIdField}/{value}
CreatePOST/services/data/v63.0/sobjects/{SObject}/
UpdatePATCH/services/data/v63.0/sobjects/{SObject}/{id}
Upsert by External IDPATCH/services/data/v63.0/sobjects/{SObject}/{ExternalIdField}/{value}
DeleteDELETE/services/data/v63.0/sobjects/{SObject}/{id}

A successful create returns HTTP 201 with a JSON body containing {"id": "...", "success": true, "errors": []}. A successful update or delete returns HTTP 204 with no body.

SOQL Query Resource and Pagination

Execute SOQL via the /query/ resource:

GET /services/data/v63.0/query/?q=SELECT+Id%2CName+FROM+Account+LIMIT+200

If the result set is larger than the batch size (default 2,000 records; the Sforce-Query-Options: batchSize= header accepts 200 to 2,000, and the platform may return fewer for large records), the response includes "done": false and a "nextRecordsUrl" field:

{
  "totalSize": 5800,
  "done": false,
  "nextRecordsUrl": "/services/data/v63.0/query/01gXXXXXXXXXXXX-2000",
  "records": [...]
}

Fetch subsequent pages by making a GET request to the absolute path given in nextRecordsUrl (prepend the instance host). Continue until "done": true. Do not attempt to reconstruct the query locator URL manually — use the value exactly as returned.

Composite Resource

The /composite/ resource executes up to 25 subrequests in a single HTTP round trip, and the whole series counts as one call toward API limits. Up to 5 of the 25 can be sObject Collections or query operations (Query and QueryAll). Composite graphs raise the subrequest limit to 500. Subrequests are ordered, run as the same user, and results from earlier subrequests can be referenced in later subrequests using @{referenceId.fieldName} syntax.

{
  "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",
        "AccountId": "@{NewAccount.id}"
      }
    }
  ]
}

When allOrNone is true, any single subrequest failure rolls back all subrequests in the call. When false, each subrequest result is independent.

Composite Batch Resource

The /composite/batch/ resource executes up to 25 independent subrequests. Unlike /composite/, subrequests cannot reference each other's results. With haltOnError: true, a 4xx or 5xx subrequest stops the batch and every later subrequest returns 412 BATCH_PROCESSING_HALTED, while the top-level response is still 200 with hasErrors: true.

Use Composite Batch when:

sObject Tree Resource

The /composite/tree/{SObject}/ resource inserts up to 200 records in a single call, including parent-child related record trees:

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

Up to five levels of nesting and up to five different record types are supported. If any record fails, the entire request fails and the response names only the failing reference ID. Records across all trees count toward the 200-record limit, the request counts as one API call, and triggers and flows fire separately for each level (all root records, then all second-level records of the same type, and so on).

Error Response Format

When a request fails, Salesforce returns an array of error objects:

[
  {
    "message": "Required fields are missing: [Name]",
    "errorCode": "REQUIRED_FIELD_MISSING",
    "fields": ["Name"]
  }
]

Composite responses embed a httpStatusCode per subrequest alongside the error array. Always inspect individual subrequest bodies when using Composite or Composite Batch — an HTTP 200 outer response does not mean all subrequests succeeded.


Common Patterns

When to use: You need to create a parent Account and a child Contact in the same transaction without two round trips and without knowing the Account ID in advance.

How it works: Use /composite/ with allOrNone: true. Set a referenceId on the Account subrequest and reference it in the Contact subrequest using @{refId.id}. The platform resolves the reference server-side before executing the Contact insert.

Why not the alternative: Two separate REST calls require the caller to persist the Account ID between calls and handle partial failure (Account created, Contact failed) with compensating logic. The composite approach keeps both in a single atomic transaction.

Paginated SOQL Query Loop

When to use: Any query against a large data set where the result count may exceed 2,000 records.

How it works: Issue the initial /query/ request. If done is false, read nextRecordsUrl from the response and issue a GET to that URL (prefixed with the instance hostname). Repeat until done is true. Buffer or stream records per page — do not assume all records fit in memory.

Why not the alternative: Setting an arbitrarily large LIMIT in SOQL does not bypass the batch size; the API still paginates. Ignoring nextRecordsUrl silently drops records — a hard-to-detect data integrity problem.

Upsert by External ID

When to use: An external system owns a record identifier and you want to create-or-update without first querying for the Salesforce ID.

How it works: PATCH to /sobjects/{SObject}/{ExternalIdField}/{value}. If no record with that external ID exists, Salesforce creates it (HTTP 201, "created": true). If one exists, Salesforce updates it (HTTP 200 with "created": false in API 46.0 and later; 204 with no body in 45.0 and earlier). If the value matches more than one record, the API returns 300 with the matching records and changes nothing. Add ?updateOnly=true to update without ever creating. The External ID field must be defined on the object.

Why not the alternative: A query-then-insert-or-update pattern doubles round trips and introduces a race condition between the read and the write.


Decision Guidance

SituationRecommended ApproachReason
Single record CRUD/sobjects/{SObject}/Simplest, no overhead
Create parent + children with ID wiring/composite/ with allOrNone: trueAtomic, cross-reference support
Multiple independent updates in one call/composite/batch/Reduces round trips without requiring ordering
Bulk parent-child tree insert (≤ 200 records)/composite/tree/{SObject}/Atomic tree, fewer API calls
> 2,000 record inserts or bulk queriesBulk API 2.0REST API is not designed for large-volume jobs
Query result set larger than 2,000 recordsSOQL + nextRecordsUrl loopPlatform-managed pagination, no manual cursor math
Create-or-update with external system keyUpsert via External ID PATCHNo pre-query needed, atomic

  1. Size the job: records per run, runs per day, and the org's 24-hour allocation (GET /services/data/vXX.X/limits/ or the Sforce-Limit-Info response header); move anything over a few thousand records per run to Bulk API 2.0.
  2. Pick the resource per operation from the Decision Guidance table, and decide each transaction boundary (allOrNone, sObject Tree, or independent batch).
  3. Pin one recent API version in configuration, not in code, and record its retirement horizon.
  4. Implement the client: OAuth token handling (401 re-auth), pagination with nextRecordsUrl until done, per-subrequest status inspection, and the documented error codes (403 REQUEST_LIMIT_EXCEEDED, 300 for ambiguous external IDs, 410 for retired versions, 412 for halted batches).
  5. Cap concurrency so no more than 25 calls (5 in Developer Edition) run longer than 20 seconds at once, and keep composite requests well under the 10-minute timeout. Worked requests are in 0.
  6. Run python3 skills/integration/rest-api-patterns/scripts/check_rest_api_patterns.py --manifest-dir <integration source> and load-test against a sandbox before go-live.

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. HTTP 200 outer response masks subrequest failures in Composite — When using /composite/ or /composite/batch/, the outer HTTP response code is 200 even if individual subrequests return 4xx. Integrations that only check the outer status silently discard errors.
  2. nextRecordsUrl is not a full URL — it is a path — The value returned in nextRecordsUrl is a relative path like /services/data/v63.0/query/01gXXX-2000. You must prepend the instance hostname. Treating it as a complete URL or re-constructing it from the query locator ID causes 404s or incorrect page fetches.
  3. API version retirement is permanent: Salesforce supports each version for at least 3 years and gives at least a year's notice; requests to a retired version get 410 GONE. Versions 7.0 through 30.0 are retired as of Summer '25. Keep the version in configuration and upgrade on a schedule.
  4. Concurrent API request limits are separate from daily limits: Production orgs and sandboxes allow 25 concurrent requests that run 20 seconds or longer (Developer Edition and trial orgs allow 5); further long-running requests get REQUEST_LIMIT_EXCEEDED. Polling integrations and exports that hold long calls open can hit this ceiling while the daily allocation looks healthy.
  5. sObject Tree is all-or-nothing at 200 records — Unlike Composite with allOrNone: false, the sObject Tree resource does not support partial success. One invalid record in the tree rolls back the entire payload. Validate records client-side before sending large trees.

Output Artifacts

ArtifactDescription
Endpoint selection guideCorrect REST resource URL, HTTP verb, and request body shape for the requested operation
Composite request scaffoldJSON request body with subrequests, referenceIds, and allOrNone setting
Pagination loop designLogic for following nextRecordsUrl until done: true with record buffering
Error handling strategyPer-subrequest error inspection pattern and 401 refresh logic
API limit estimateDaily and concurrent limit projection based on org edition and call volume

Package files